Release Healer Man 0.1.24
This commit is contained in:
1 parent
c4346751a9
commit
bac0da9300
118 files changed
+857
-289
No files matched your search
@@ -1,10 +1,14 @@
|
||||
import {
|
||||
AdditiveBlending,
|
||||
BufferAttribute,
|
||||
BufferGeometry,
|
||||
DoubleSide,
|
||||
Float32BufferAttribute,
|
||||
Group,
|
||||
Mesh,
|
||||
MeshBasicMaterial,
|
||||
MeshStandardMaterial,
|
||||
NormalBlending,
|
||||
} from "three";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
@@ -12,6 +16,16 @@ import {
|
||||
collisionTrimeshes,
|
||||
prepareCollision,
|
||||
} from "./productionDungeonCollision";
|
||||
import { prepareDungeonMaterial } from "./productionDungeonMaterials";
|
||||
|
||||
const EMPTY_MATERIAL_ROLES = {
|
||||
cutoutPatterns: [],
|
||||
blendPatterns: [],
|
||||
additivePatterns: [],
|
||||
cutoutNames: [],
|
||||
blendNames: [],
|
||||
additiveNames: [],
|
||||
};
|
||||
|
||||
describe("streamed dungeon collision preparation", () => {
|
||||
it("dequantizes normalized Meshopt position buffers before Rapier consumes them", () => {
|
||||
@@ -94,3 +108,43 @@ describe("streamed dungeon collision preparation", () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("streamed dungeon material preparation", () => {
|
||||
it("preserves alpha blending declared by the source GLB", () => {
|
||||
const source = new MeshStandardMaterial({ transparent: true, opacity: 0.2 });
|
||||
|
||||
const prepared = prepareDungeonMaterial(source, EMPTY_MATERIAL_ROLES);
|
||||
|
||||
expect(prepared).not.toBe(source);
|
||||
expect(prepared.transparent).toBe(true);
|
||||
expect(prepared.opacity).toBe(0.2);
|
||||
expect(prepared.depthWrite).toBe(false);
|
||||
expect(prepared.blending).toBe(NormalBlending);
|
||||
expect(prepared.side).toBe(DoubleSide);
|
||||
});
|
||||
|
||||
it("preserves alpha masking declared by the source GLB", () => {
|
||||
const source = new MeshStandardMaterial({ alphaTest: 0.37, transparent: false });
|
||||
|
||||
const prepared = prepareDungeonMaterial(source, EMPTY_MATERIAL_ROLES);
|
||||
|
||||
expect(prepared.transparent).toBe(false);
|
||||
expect(prepared.opacity).toBe(1);
|
||||
expect(prepared.alphaTest).toBe(0.37);
|
||||
expect(prepared.depthWrite).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps explicit dungeon presentation roles authoritative", () => {
|
||||
const source = new MeshStandardMaterial({ name: "light-volume", alphaTest: 0.5 });
|
||||
const roles = {
|
||||
...EMPTY_MATERIAL_ROLES,
|
||||
additiveNames: ["LIGHT-VOLUME"],
|
||||
};
|
||||
|
||||
const prepared = prepareDungeonMaterial(source, roles);
|
||||
|
||||
expect(prepared.transparent).toBe(true);
|
||||
expect(prepared.depthWrite).toBe(false);
|
||||
expect(prepared.blending).toBe(AdditiveBlending);
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,7 @@
|
||||
import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { RigidBody, TrimeshCollider } from "@react-three/rapier";
|
||||
import {
|
||||
AdditiveBlending,
|
||||
DoubleSide,
|
||||
Mesh,
|
||||
NormalBlending,
|
||||
type InstancedMesh,
|
||||
type Material,
|
||||
type Object3D,
|
||||
@@ -24,49 +21,13 @@ import {
|
||||
import { disposeObject3DResources } from "./threeResourceDisposal";
|
||||
import { instanceRepeatedStaticMeshes } from "./staticSceneInstancing";
|
||||
import { useGameGLTF } from "./useGameGLTF";
|
||||
import { prepareDungeonMaterial } from "./productionDungeonMaterials";
|
||||
import {
|
||||
DUNGEON_COLLISION_LOAD_BATCH_SIZE,
|
||||
DUNGEON_VISUAL_LOAD_BATCH_SIZE,
|
||||
advanceDungeonAssetCount,
|
||||
} from "./dungeonLoadSequencing";
|
||||
|
||||
function matchesRole(name: string, patterns: readonly string[], names: readonly string[]): boolean {
|
||||
const lower = name.toLowerCase();
|
||||
return names.some((candidate) => candidate.toLowerCase() === lower)
|
||||
|| patterns.some((pattern) => new RegExp(pattern, "i").test(name));
|
||||
}
|
||||
|
||||
function prepareMaterial(source: Material, roles: DungeonMaterialRoles): Material {
|
||||
const material = source.clone();
|
||||
const name = material.name;
|
||||
material.side = DoubleSide;
|
||||
|
||||
if (matchesRole(name, roles.cutoutPatterns, roles.cutoutNames)) {
|
||||
material.transparent = false;
|
||||
material.opacity = 1;
|
||||
material.alphaTest = 0.5;
|
||||
material.depthWrite = true;
|
||||
material.blending = NormalBlending;
|
||||
} else if (matchesRole(name, roles.additivePatterns, roles.additiveNames)) {
|
||||
material.transparent = true;
|
||||
material.depthWrite = false;
|
||||
material.blending = AdditiveBlending;
|
||||
} else if (matchesRole(name, roles.blendPatterns, roles.blendNames)) {
|
||||
material.transparent = true;
|
||||
material.depthWrite = false;
|
||||
material.blending = NormalBlending;
|
||||
} else {
|
||||
material.transparent = false;
|
||||
material.opacity = 1;
|
||||
material.alphaTest = 0;
|
||||
material.depthWrite = true;
|
||||
material.blending = NormalBlending;
|
||||
}
|
||||
|
||||
material.needsUpdate = true;
|
||||
return material;
|
||||
}
|
||||
|
||||
function prepareVisual(root: Object3D, roles: DungeonMaterialRoles): Object3D {
|
||||
const clone = root.clone(true);
|
||||
const clonedMaterials = new Map<string, Material>();
|
||||
@@ -77,7 +38,7 @@ function prepareVisual(root: Object3D, roles: DungeonMaterialRoles): Object3D {
|
||||
const prepared = sources.map((source) => {
|
||||
const cached = clonedMaterials.get(source.uuid);
|
||||
if (cached) return cached;
|
||||
const next = prepareMaterial(source, roles);
|
||||
const next = prepareDungeonMaterial(source, roles);
|
||||
clonedMaterials.set(source.uuid, next);
|
||||
return next;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
AdditiveBlending,
|
||||
DoubleSide,
|
||||
NormalBlending,
|
||||
type Material,
|
||||
} from "three";
|
||||
import type { DungeonMaterialRoles } from "../game/dungeonTypes";
|
||||
|
||||
function matchesRole(name: string, patterns: readonly string[], names: readonly string[]): boolean {
|
||||
const lower = name.toLowerCase();
|
||||
return names.some((candidate) => candidate.toLowerCase() === lower)
|
||||
|| patterns.some((pattern) => new RegExp(pattern, "i").test(name));
|
||||
}
|
||||
|
||||
export function prepareDungeonMaterial(source: Material, roles: DungeonMaterialRoles): Material {
|
||||
const material = source.clone();
|
||||
const name = material.name;
|
||||
material.side = DoubleSide;
|
||||
|
||||
if (matchesRole(name, roles.cutoutPatterns, roles.cutoutNames)) {
|
||||
material.transparent = false;
|
||||
material.opacity = 1;
|
||||
material.alphaTest = 0.5;
|
||||
material.depthWrite = true;
|
||||
material.blending = NormalBlending;
|
||||
} else if (matchesRole(name, roles.additivePatterns, roles.additiveNames)) {
|
||||
material.transparent = true;
|
||||
material.depthWrite = false;
|
||||
material.blending = AdditiveBlending;
|
||||
} else if (matchesRole(name, roles.blendPatterns, roles.blendNames)) {
|
||||
material.transparent = true;
|
||||
material.depthWrite = false;
|
||||
material.blending = NormalBlending;
|
||||
} else if (material.alphaTest > 0) {
|
||||
// Preserve a source GLB's MASK semantics when no presentation override exists.
|
||||
material.transparent = false;
|
||||
material.opacity = 1;
|
||||
material.depthWrite = true;
|
||||
material.blending = NormalBlending;
|
||||
} else if (material.transparent) {
|
||||
// Preserve a source GLB's BLEND semantics when no presentation override exists.
|
||||
material.depthWrite = false;
|
||||
material.blending = NormalBlending;
|
||||
} else {
|
||||
material.transparent = false;
|
||||
material.opacity = 1;
|
||||
material.alphaTest = 0;
|
||||
material.depthWrite = true;
|
||||
material.blending = NormalBlending;
|
||||
}
|
||||
|
||||
material.needsUpdate = true;
|
||||
return material;
|
||||
}
|
||||
Reference in new issue
Block a user