Skip to content

Commit

Permalink
feat(model): add support for texture weight and transform animations
Browse files Browse the repository at this point in the history
  • Loading branch information
fallenoak committed Jan 24, 2024
1 parent 18f8d40 commit a68e2ce
Show file tree
Hide file tree
Showing 12 changed files with 349 additions and 13 deletions.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
],
"dependencies": {
"@tweenjs/tween.js": "^23.1.1",
"@wowserhq/format": "^0.19.0"
"@wowserhq/format": "^0.20.0"
},
"peerDependencies": {
"three": "^0.160.0"
Expand Down
4 changes: 4 additions & 0 deletions src/lib/map/DoodadManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ class DoodadManager {

return group;
}

update(deltaTime: number) {
this.#modelManager.update(deltaTime);
}
}

export default DoodadManager;
2 changes: 2 additions & 0 deletions src/lib/map/MapManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ class MapManager extends EventTarget {
}

update(deltaTime: number, camera: THREE.Camera) {
this.#doodadManager.update(deltaTime);

this.#mapLight.update(camera);

// If fog end is closer than the configured view distance, use the fog end plus extension to
Expand Down
130 changes: 130 additions & 0 deletions src/lib/model/ModelAnimator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import * as THREE from 'three';
import { M2Track } from '@wowserhq/format';
import { SequenceSpec } from './loader/types.js';

interface Constructor<T> {
new (...args: any[]): T;
}

class ModelAnimator {
#mixer: THREE.AnimationMixer;

#loops: number[] = [];
#loopClips: THREE.AnimationClip[] = [];

#sequences: SequenceSpec[] = [];
#sequenceClips: THREE.AnimationClip[] = [];

constructor(root: THREE.Object3D, loops: Uint32Array, sequences: SequenceSpec[]) {
this.#mixer = new THREE.AnimationMixer(root);
this.#mixer.timeScale = 1000;

for (const loop of loops) {
this.#registerLoop(loop);
}

for (const sequence of sequences) {
this.#registerSequence(sequence);
}
}

get loops() {
return this.#loops;
}

get sequences() {
return this.#sequences;
}

update(deltaTime: number) {
this.#mixer.update(deltaTime);
}

getLoop(root: THREE.Object3D, index: number) {
const clip = this.#loopClips[index];
return this.#mixer.clipAction(clip, root);
}

getSequence(root: THREE.Object3D, index: number) {
const clip = this.#sequenceClips[index];
return this.#mixer.clipAction(clip, root);
}

registerTrack<T extends THREE.TypedArray>(
name: string,
track: M2Track<T>,
TrackType: Constructor<THREE.KeyframeTrack>,
transform?: (value: any) => any,
) {
// Empty track
if (track.sequenceTimes.length === 0 || track.sequenceKeys.length === 0) {
return;
}

if (track.loopIndex === 0xffff) {
this.#registerSequenceTrack(name, track, TrackType, transform);
} else {
this.#registerLoopTrack(name, track, TrackType, transform);
}
}

#registerLoopTrack<T extends THREE.TypedArray>(
name: string,
track: M2Track<T>,
TrackType: Constructor<THREE.KeyframeTrack>,
transform?: (value: any) => any,
) {
const clip = this.#loopClips[track.loopIndex];

for (let s = 0; s < track.sequenceTimes.length; s++) {
const times = track.sequenceTimes[s];
const values = transform
? Array.from(track.sequenceKeys[s]).map((value) => transform(value))
: track.sequenceKeys[s];

// Empty loop
if (times.length === 0 || values.length === 0) {
continue;
}

clip.tracks.push(new TrackType(name, times, values));
}
}

#registerSequenceTrack<T extends THREE.TypedArray>(
name: string,
track: M2Track<T>,
TrackType: Constructor<THREE.KeyframeTrack>,
transform?: (value: any) => any,
) {
for (let s = 0; s < track.sequenceTimes.length; s++) {
const clip = this.#sequenceClips[s];

const times = track.sequenceTimes[s];
const values = transform
? Array.from(track.sequenceKeys[s]).map((value) => transform(value))
: track.sequenceKeys[s];

// Empty sequence
if (times.length === 0 || values.length === 0) {
continue;
}

clip.tracks.push(new TrackType(name, times, values));
}
}

#registerLoop(duration: number) {
const index = this.#loops.length;
this.#loops[index] = duration;
this.#loopClips[index] = new THREE.AnimationClip(`loop-${index}`, duration, []);
}

#registerSequence(spec: SequenceSpec) {
const index = this.#sequences.length;
this.#sequences[index] = spec;
this.#sequenceClips[index] = new THREE.AnimationClip(`sequence-${index}`, spec.duration, []);
}
}

export default ModelAnimator;
69 changes: 68 additions & 1 deletion src/lib/model/ModelManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ import { getFragmentShader } from './shader/fragment.js';
import ModelLoader from './loader/ModelLoader.js';
import { MaterialSpec, ModelSpec, TextureSpec } from './loader/types.js';
import SceneLight from '../light/SceneLight.js';
import ModelAnimator from './ModelAnimator.js';

type ModelResources = {
name: string;
geometry: THREE.BufferGeometry;
materials: THREE.Material[];
animator: ModelAnimator;
textureWeightCount: number;
textureTransformCount: number;
};

type ModelManagerOptions = {
Expand Down Expand Up @@ -45,6 +49,14 @@ class ModelManager {
return this.#createMesh(resources);
}

update(deltaTime: number) {
for (const resources of this.#loaded.values()) {
if (resources.animator) {
resources.animator.update(deltaTime);
}
}
}

#getResources(path: string) {
const refId = normalizePath(path);

Expand All @@ -67,13 +79,17 @@ class ModelManager {
async #loadResources(refId: string, path: string) {
const spec = await this.#loader.loadSpec(path);

const animator = this.#createAnimator(spec);
const geometry = this.#createGeometry(spec);
const materials = await this.#createMaterials(spec);

const resources: ModelResources = {
name: spec.name,
geometry,
materials,
animator,
textureWeightCount: spec.textureWeights.length,
textureTransformCount: spec.textureTransforms.length,
};

this.#loaded.set(refId, resources);
Expand Down Expand Up @@ -149,12 +165,16 @@ class ModelManager {
const textures = await Promise.all(
spec.textures.map((textureSpec) => this.#createTexture(textureSpec)),
);
const textureWeightIndex = spec.textureWeightIndex;
const textureTransformIndices = spec.textureTransformIndices;
const uniforms = { ...this.#sceneLight.uniforms };

return new ModelMaterial(
vertexShader,
fragmentShader,
textures,
textureWeightIndex,
textureTransformIndices,
uniforms,
spec.blend,
spec.flags,
Expand All @@ -177,11 +197,58 @@ class ModelManager {
}

#createMesh(resources: ModelResources) {
const mesh = new ModelMesh(resources.geometry, resources.materials);
const mesh = new ModelMesh(
resources.geometry,
resources.materials,
resources.animator,
resources.textureWeightCount,
resources.textureTransformCount,
);

mesh.name = resources.name;

return mesh;
}

#createAnimator(spec: ModelSpec) {
if (spec.loops.length === 0 && spec.sequences.length === 0) {
return null;
}

const root = new THREE.Object3D();
const animator = new ModelAnimator(root, spec.loops, spec.sequences);

for (const [index, textureWeight] of spec.textureWeights.entries()) {
animator.registerTrack(
`.textureWeights[${index}]`,
textureWeight.weightTrack,
THREE.NumberKeyframeTrack,
(value: number) => value / 0x7fff,
);
}

for (const [index, textureTransform] of spec.textureTransforms.entries()) {
animator.registerTrack(
`.textureTransforms[${index}].translation`,
textureTransform.translationTrack,
THREE.VectorKeyframeTrack,
);

animator.registerTrack(
`.textureTransforms[${index}].rotation`,
textureTransform.rotationTrack,
THREE.QuaternionKeyframeTrack,
);

animator.registerTrack(
`.textureTransforms[${index}].scaling`,
textureTransform.scalingTrack,
THREE.VectorKeyframeTrack,
);
}

return animator;
}
}

export default ModelManager;
Expand Down
29 changes: 29 additions & 0 deletions src/lib/model/ModelMaterial.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ const DEFAULT_FLAGS: number = 0x0;
const DEFAULT_ALPHA: number = 1.0;

class ModelMaterial extends THREE.RawShaderMaterial {
#textureWeightIndex: number;
#textureTransformIndices: number[];
#textureTransforms: THREE.Matrix4[];

#blend: M2_MATERIAL_BLEND;

#materialParams: THREE.Vector4;
Expand All @@ -22,12 +26,18 @@ class ModelMaterial extends THREE.RawShaderMaterial {
vertexShader: string,
fragmentShader: string,
textures: THREE.Texture[],
textureWeightIndex: number,
textureTransformIndices: number[],
uniforms: Record<string, THREE.IUniform> = {},
blend = DEFAULT_BLEND,
flags = DEFAULT_FLAGS,
) {
super();

this.#textureWeightIndex = textureWeightIndex;
this.#textureTransformIndices = textureTransformIndices;
this.#textureTransforms = [new THREE.Matrix4(), new THREE.Matrix4()];

this.#blend = blend;

this.#materialParams = new THREE.Vector4(0.0, 0.0, 0.0, 0.0);
Expand Down Expand Up @@ -59,6 +69,7 @@ class ModelMaterial extends THREE.RawShaderMaterial {
this.uniforms = {
...uniforms,
textures: { value: textures },
textureTransforms: { value: this.#textureTransforms },
materialParams: { value: this.#materialParams },
diffuseColor: { value: this.#diffuseColor },
emissiveColor: { value: this.#emissiveColor },
Expand Down Expand Up @@ -112,6 +123,14 @@ class ModelMaterial extends THREE.RawShaderMaterial {
this.#materialParams.setZ(lit);
}

get textureWeightIndex() {
return this.#textureWeightIndex;
}

get textureTransformIndices() {
return this.#textureTransformIndices;
}

setDiffuseColor(color: THREE.Color) {
// Materials using BLEND_MOD and BLEND_MOD2X use hardcoded colors
if (
Expand Down Expand Up @@ -146,6 +165,16 @@ class ModelMaterial extends THREE.RawShaderMaterial {
this.uniformsNeedUpdate = true;
}

setTextureTransform(
index: number,
translation: THREE.Vector3,
rotation: THREE.Quaternion,
scaling: THREE.Vector3,
) {
this.#textureTransforms[index].compose(translation, rotation, scaling);
this.uniformsNeedUpdate = true;
}

#updateBlending() {
// Adjust OPAQUE and ALPHA_KEY blends if the material's alpha value is below 1.0
const isOpaque = this.#blend <= M2_MATERIAL_BLEND.BLEND_ALPHA_KEY && this.alpha >= 0.99998999;
Expand Down
Loading

0 comments on commit a68e2ce

Please sign in to comment.