Release Healer Man 0.1.30
This commit is contained in:
1 parent
544c1bee91
commit
7d7fd567f0
845 files changed
+2040
-87
No files matched your search
@@ -0,0 +1,202 @@
|
||||
import { memo, useEffect, useMemo, useRef, type MutableRefObject } from "react";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import {
|
||||
AdditiveBlending, BufferAttribute, BufferGeometry, ClampToEdgeWrapping, DoubleSide,
|
||||
Group, Matrix4, MeshBasicMaterial, MultiplyBlending, NormalBlending, Quaternion,
|
||||
RepeatWrapping, Texture, Vector3,
|
||||
} from "three";
|
||||
import { sampleOriginalCombatMeshTrack } from "./originalCombatMeshRuntime";
|
||||
import { updateWowBoneMatrices, wowModelFrame, type OriginalWowModelGraph, type WowRibbon } from "./originalWowModelRuntime";
|
||||
|
||||
const blend = (mode: number) => [3, 4, 7].includes(mode) ? AdditiveBlending
|
||||
: [5, 6].includes(mode) ? MultiplyBlending : NormalBlending;
|
||||
|
||||
export const OriginalWowModel = memo(function OriginalWowModel({ graph, textures, active, playbackRef, loop }: {
|
||||
readonly graph: OriginalWowModelGraph;
|
||||
readonly textures: ReadonlyMap<number, Texture>;
|
||||
readonly active: boolean;
|
||||
readonly playbackRef: MutableRefObject<{ elapsedMs: number; decaying: boolean }>;
|
||||
readonly loop: boolean;
|
||||
}) {
|
||||
const root = useRef<Group>(null);
|
||||
const scratch = useMemo(() => ({
|
||||
bones: graph.bones.map(() => new Matrix4()), position: new Vector3(), vertex: new Vector3(),
|
||||
normal: new Vector3(), normalSum: new Vector3(), camera: new Quaternion(), world: new Quaternion(),
|
||||
billboardAxis: new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), -Math.PI / 2),
|
||||
}), [graph]);
|
||||
const geometry = useMemo(() => {
|
||||
if (!graph.mesh) return null;
|
||||
const geometry = new BufferGeometry();
|
||||
geometry.setAttribute("position", new BufferAttribute(new Float32Array(graph.mesh.positions), 3));
|
||||
geometry.setAttribute("normal", new BufferAttribute(new Float32Array(graph.mesh.normals), 3));
|
||||
geometry.setAttribute("uv", new BufferAttribute(new Float32Array(graph.mesh.uvs), 2));
|
||||
geometry.setAttribute("uv1", new BufferAttribute(new Float32Array(graph.mesh.uvs2), 2));
|
||||
geometry.setIndex([...graph.mesh.indices]);
|
||||
graph.mesh.layers.forEach((layer, index) => geometry.addGroup(layer.indexStart, layer.indexCount, index));
|
||||
return geometry;
|
||||
}, [graph]);
|
||||
const materials = useMemo(() => graph.mesh?.layers.map((layer) => {
|
||||
const source = graph.materials[layer.materialIndex];
|
||||
const maps = layer.textureIndices.map((index, slot) => {
|
||||
const texture = textures.get(index)!.clone();
|
||||
texture.wrapS = graph.textures[index].flags & 1 ? RepeatWrapping : ClampToEdgeWrapping;
|
||||
texture.wrapT = graph.textures[index].flags & 2 ? RepeatWrapping : ClampToEdgeWrapping;
|
||||
texture.channel = Math.max(0, layer.textureCoordinateIndices[slot] ?? 0);
|
||||
texture.needsUpdate = true;
|
||||
return texture;
|
||||
});
|
||||
const material = new MeshBasicMaterial({
|
||||
map: maps[0], blending: blend(source.blendMode), transparent: source.blendMode !== 0,
|
||||
alphaTest: source.blendMode === 1 ? 0.5 : 0, depthWrite: !(source.flags & 16) && source.blendMode < 2,
|
||||
depthTest: !(source.flags & 8), side: DoubleSide, toneMapped: false,
|
||||
});
|
||||
// The WotLK spell batches use one or two source textures. Preserve the
|
||||
// second color/alpha layer and its independent UV transform in the shader.
|
||||
if (maps[1]) {
|
||||
material.onBeforeCompile = (shader) => {
|
||||
shader.uniforms.wowSecondMap = { value: maps[1] };
|
||||
shader.uniforms.wowSecondMatrix = { value: maps[1].matrix };
|
||||
shader.vertexShader = shader.vertexShader.replace("#include <common>",
|
||||
"#include <common>\nuniform mat3 wowSecondMatrix;\nvarying vec2 vWowSecondUv;"
|
||||
+ (maps[1].channel === 1 ? "\n#ifndef USE_UV1\nattribute vec2 uv1;\n#endif" : ""));
|
||||
shader.vertexShader = shader.vertexShader.replace("#include <uv_vertex>",
|
||||
`#include <uv_vertex>\nvWowSecondUv = (wowSecondMatrix * vec3(${maps[1].channel === 1 ? "uv1" : "uv"}, 1.0)).xy;`);
|
||||
shader.fragmentShader = shader.fragmentShader.replace("#include <common>",
|
||||
"#include <common>\nuniform sampler2D wowSecondMap;\nvarying vec2 vWowSecondUv;");
|
||||
shader.fragmentShader = shader.fragmentShader.replace("#include <map_fragment>",
|
||||
"#include <map_fragment>\ndiffuseColor *= texture2D(wowSecondMap, vWowSecondUv);");
|
||||
};
|
||||
material.customProgramCacheKey = () => `wow-dual-${maps[1].channel}`;
|
||||
}
|
||||
return { material, maps };
|
||||
}) ?? [], [graph, textures]);
|
||||
useEffect(() => () => {
|
||||
geometry?.dispose();
|
||||
materials.forEach(({ material, maps }) => { material.dispose(); maps.forEach((texture) => texture.dispose()); });
|
||||
}, [geometry, materials]);
|
||||
useFrame((state) => {
|
||||
if (!active) return;
|
||||
const frame = wowModelFrame(graph, playbackRef.current.elapsedMs, loop, playbackRef.current.decaying);
|
||||
root.current?.getWorldQuaternion(scratch.world);
|
||||
state.camera.getWorldQuaternion(scratch.camera).premultiply(scratch.world.invert());
|
||||
// Authored billboard planes face source +X, whereas the camera faces +Z.
|
||||
scratch.camera.multiply(scratch.billboardAxis);
|
||||
updateWowBoneMatrices(graph, frame, scratch.bones, scratch.camera);
|
||||
if (!geometry || !graph.mesh) return;
|
||||
const mesh = graph.mesh;
|
||||
const positions = geometry.getAttribute("position") as BufferAttribute;
|
||||
const normals = geometry.getAttribute("normal") as BufferAttribute;
|
||||
for (let index = 0; index < mesh.positions.length / 3; index++) {
|
||||
scratch.position.set(0, 0, 0); scratch.normalSum.set(0, 0, 0);
|
||||
let total = 0;
|
||||
for (let slot = 0; slot < 4; slot++) {
|
||||
const weight = mesh.boneWeights[index * 4 + slot];
|
||||
if (!weight) continue;
|
||||
const matrix = scratch.bones[mesh.boneIndices[index * 4 + slot]];
|
||||
scratch.vertex.fromArray(mesh.positions, index * 3).applyMatrix4(matrix);
|
||||
scratch.normal.fromArray(mesh.normals, index * 3).transformDirection(matrix);
|
||||
scratch.position.addScaledVector(scratch.vertex, weight);
|
||||
scratch.normalSum.addScaledVector(scratch.normal, weight);
|
||||
total += weight;
|
||||
}
|
||||
if (!total) { scratch.position.fromArray(mesh.positions, index * 3); scratch.normalSum.fromArray(mesh.normals, index * 3); }
|
||||
positions.setXYZ(index, ...scratch.position.toArray());
|
||||
normals.setXYZ(index, ...scratch.normalSum.normalize().toArray());
|
||||
}
|
||||
positions.needsUpdate = true; normals.needsUpdate = true;
|
||||
mesh.layers.forEach((layer, index) => {
|
||||
const { material, maps } = materials[index];
|
||||
const color = mesh.colors[layer.colorIndex];
|
||||
const weight = mesh.textureWeights[layer.textureWeightIndex];
|
||||
const rgb = color ? sampleOriginalCombatMeshTrack(graph, color.color, frame) : undefined;
|
||||
material.color.setRGB(...(rgb ?? [1, 1, 1]));
|
||||
material.opacity = Math.max(0, Math.min(1, (color ? sampleOriginalCombatMeshTrack(graph, color.alpha, frame) ?? 32767 : 32767)
|
||||
* (weight ? sampleOriginalCombatMeshTrack(graph, weight, frame) ?? 32767 : 32767) / (32767 * 32767)));
|
||||
maps.forEach((texture, slot) => {
|
||||
const transform = graph.textureTransforms[layer.textureTransformIndices[slot]];
|
||||
if (!transform) return;
|
||||
const t = sampleOriginalCombatMeshTrack(graph, transform.translation, frame) ?? [0, 0, 0];
|
||||
const s = sampleOriginalCombatMeshTrack(graph, transform.scale, frame) ?? [1, 1, 1];
|
||||
const q = sampleOriginalCombatMeshTrack(graph, transform.rotation, frame) ?? [0, 0, 0, 1];
|
||||
texture.offset.set(t[0], t[1]); texture.repeat.set(s[0], s[1]); texture.center.set(0.5, 0.5);
|
||||
texture.rotation = 2 * Math.atan2(q[2], q[3]); texture.updateMatrix();
|
||||
});
|
||||
});
|
||||
});
|
||||
return <group ref={root} rotation={[-Math.PI / 2, 0, 0]}>
|
||||
{geometry && <mesh name="original-wow-animated-mesh" geometry={geometry}
|
||||
material={materials.map((entry) => entry.material)} frustumCulled={false} />}
|
||||
{graph.ribbons.map((ribbon, index) => <OriginalWowRibbon key={index} graph={graph} ribbon={ribbon}
|
||||
textures={textures} active={active} bones={scratch.bones} playbackRef={playbackRef} loop={loop} />)}
|
||||
</group>;
|
||||
});
|
||||
|
||||
const OriginalWowRibbon = memo(function OriginalWowRibbon({ graph, ribbon, textures, active, bones, playbackRef, loop }: {
|
||||
readonly graph: OriginalWowModelGraph; readonly ribbon: WowRibbon; readonly textures: ReadonlyMap<number, Texture>;
|
||||
readonly active: boolean; readonly bones: readonly Matrix4[];
|
||||
readonly playbackRef: MutableRefObject<{ elapsedMs: number; decaying: boolean }>; readonly loop: boolean;
|
||||
}) {
|
||||
const root = useRef<Group>(null);
|
||||
const capacity = Math.min(128, Math.max(4, Math.ceil(ribbon.edgesPerSecond * ribbon.edgeLifetime) + 2));
|
||||
const geometry = useMemo(() => {
|
||||
const g = new BufferGeometry();
|
||||
g.setAttribute("position", new BufferAttribute(new Float32Array(capacity * 6), 3));
|
||||
g.setAttribute("uv", new BufferAttribute(new Float32Array(capacity * 4), 2));
|
||||
g.setIndex(Array.from({ length: (capacity - 1) * 6 }, (_, i) => {
|
||||
const segment = Math.floor(i / 6) * 2;
|
||||
return segment + [0, 1, 2, 2, 1, 3][i % 6];
|
||||
}));
|
||||
g.setDrawRange(0, 0); return g;
|
||||
}, [capacity]);
|
||||
const material = useMemo(() => new MeshBasicMaterial({
|
||||
map: textures.get(ribbon.textureIndices[0]), transparent: true, depthWrite: false,
|
||||
side: DoubleSide, blending: blend(graph.materials[ribbon.materialIndices[0]]?.blendMode ?? 4), toneMapped: false,
|
||||
}), [graph, ribbon, textures]);
|
||||
const history = useMemo(() => ({
|
||||
edges: [] as { time: number; above: Vector3; below: Vector3 }[], inverse: new Matrix4(),
|
||||
top: new Vector3(), bottom: new Vector3(), local: new Vector3(), last: -Infinity,
|
||||
}), []);
|
||||
useEffect(() => () => { geometry.dispose(); material.dispose(); }, [geometry, material]);
|
||||
useFrame(() => {
|
||||
if (!active || !root.current) return;
|
||||
const elapsed = playbackRef.current.elapsedMs;
|
||||
const frame = wowModelFrame(graph, elapsed, loop, playbackRef.current.decaying);
|
||||
const sample = (key: "alpha" | "heightAbove" | "heightBelow" | "textureSlot" | "visibility") =>
|
||||
sampleOriginalCombatMeshTrack(graph, ribbon.tracks[key], frame);
|
||||
const lifetimeMs = Math.max(20, ribbon.edgeLifetime * 1000);
|
||||
if (elapsed < history.last) { history.edges.length = 0; history.last = -Infinity; }
|
||||
while (history.edges.length && elapsed - history.edges[0].time > lifetimeMs) history.edges.shift();
|
||||
root.current.updateWorldMatrix(true, false);
|
||||
if (elapsed - history.last >= 1000 / Math.max(1, ribbon.edgesPerSecond) && (sample("visibility") ?? 1) > 0) {
|
||||
const bone = bones[ribbon.parentBone];
|
||||
history.top.fromArray(ribbon.position); history.bottom.copy(history.top);
|
||||
history.top.z += sample("heightAbove") ?? 0.1;
|
||||
history.bottom.z -= sample("heightBelow") ?? 0.1;
|
||||
history.top.applyMatrix4(bone).applyMatrix4(root.current.matrixWorld);
|
||||
history.bottom.applyMatrix4(bone).applyMatrix4(root.current.matrixWorld);
|
||||
history.edges.push({ time: elapsed, above: history.top.clone(), below: history.bottom.clone() });
|
||||
if (history.edges.length > capacity) history.edges.shift();
|
||||
history.last = elapsed;
|
||||
}
|
||||
history.inverse.copy(root.current.matrixWorld).invert();
|
||||
const positions = geometry.getAttribute("position") as BufferAttribute;
|
||||
const uv = geometry.getAttribute("uv") as BufferAttribute;
|
||||
const columns = Math.max(1, ribbon.textureColumns), rows = Math.max(1, ribbon.textureRows);
|
||||
const slot = Math.max(0, Math.floor(sample("textureSlot") ?? 0)) % (columns * rows);
|
||||
history.edges.forEach((edge, index) => {
|
||||
for (const side of [0, 1]) {
|
||||
history.local.copy(side ? edge.below : edge.above);
|
||||
history.local.y -= ribbon.gravity * ((elapsed - edge.time) / 1000) ** 2 * 0.5;
|
||||
history.local.applyMatrix4(history.inverse);
|
||||
positions.setXYZ(index * 2 + side, ...history.local.toArray());
|
||||
uv.setXY(index * 2 + side, (slot % columns + index / Math.max(1, history.edges.length - 1)) / columns,
|
||||
(Math.floor(slot / columns) + side) / rows);
|
||||
}
|
||||
});
|
||||
material.color.setRGB(...(sampleOriginalCombatMeshTrack(graph, ribbon.tracks.color, frame) ?? [1, 1, 1]));
|
||||
material.opacity = Math.max(0, Math.min(1, (sample("alpha") ?? 32767) / 32767));
|
||||
positions.needsUpdate = true; uv.needsUpdate = true;
|
||||
geometry.setDrawRange(0, Math.max(0, history.edges.length - 1) * 6);
|
||||
});
|
||||
return <group ref={root}><mesh name="original-wow-ribbon" geometry={geometry} material={material} frustumCulled={false} /></group>;
|
||||
});
|
||||
Reference in new issue
Block a user