Skip to content

Commit

Permalink
feat(model): add spherical billboard support for bones
Browse files Browse the repository at this point in the history
  • Loading branch information
fallenoak committed Feb 7, 2024
1 parent 9f07452 commit 9e4d1df
Show file tree
Hide file tree
Showing 12 changed files with 257 additions and 66 deletions.
4 changes: 2 additions & 2 deletions src/lib/map/DoodadManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ class DoodadManager {
this.#doodadDefs.delete(areaId);
}

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

#refDoodad(refId: number) {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/map/MapManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ class MapManager extends EventTarget {
this.#cullGroups();

this.#doodadManager.cull(this.#cullingFrustum, camera.position);
this.#doodadManager.update(deltaTime);
this.#doodadManager.update(deltaTime, camera);
}

dispose() {
Expand Down
10 changes: 7 additions & 3 deletions src/lib/model/Model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ModelMaterial from './ModelMaterial.js';
import ModelAnimator from './ModelAnimator.js';
import ModelAnimation from './ModelAnimation.js';
import { getSizeCategory } from '../world.js';
import { SkinnedMesh } from 'three';

class Model extends THREE.Object3D {
animation: ModelAnimation;
Expand Down Expand Up @@ -32,6 +33,7 @@ class Model extends THREE.Object3D {
// Avoid skinning overhead when model does not make use of bone animations
if (skinned) {
this.#mesh = new THREE.SkinnedMesh(geometry, materials);
(this.#mesh as SkinnedMesh).boundingSphere = this.boundingSphere;
} else {
this.#mesh = new THREE.Mesh(geometry, materials);
}
Expand All @@ -43,10 +45,8 @@ class Model extends THREE.Object3D {
// Every model instance gets a unique animation state managed by a single animator
this.animation = animator.createAnimation(this);

// Every skinned model instance gets a unique skeleton
if (skinned) {
this.#mesh.add(...this.animation.rootBones);
(this.#mesh as THREE.SkinnedMesh).bind(this.animation.skeleton);
(this.#mesh as SkinnedMesh).skeleton = this.animation.skeleton as any;
}

this.diffuseColor = new THREE.Color(1.0, 1.0, 1.0);
Expand All @@ -66,6 +66,10 @@ class Model extends THREE.Object3D {
return this.#boundingSphereWorld;
}

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

get size() {
return this.#size;
}
Expand Down
25 changes: 10 additions & 15 deletions src/lib/model/ModelAnimation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { ModelMaterialColor, ModelTextureTransform } from './types.js';
import Model from './Model.js';
import ModelAnimator from './ModelAnimator.js';
import { BoneSpec } from './loader/types.js';
import ModelSkeleton from './ModelSkeleton.js';
import ModelBone from './ModelBone.js';

class ModelAnimation extends THREE.Object3D {
// States
Expand All @@ -11,8 +13,7 @@ class ModelAnimation extends THREE.Object3D {
materialColors: ModelMaterialColor[] = [];

// Skeleton
skeleton: THREE.Skeleton;
rootBones: THREE.Bone[];
skeleton: ModelSkeleton;

#model: Model;
#animator: ModelAnimator;
Expand Down Expand Up @@ -96,24 +97,18 @@ class ModelAnimation extends THREE.Object3D {
return;
}

const bones: THREE.Bone[] = [];
const rootBones: THREE.Bone[] = [];
const bones: ModelBone[] = [];

for (const boneSpec of boneSpecs) {
const bone = new THREE.Bone();
bone.visible = false;
bone.position.set(boneSpec.position[0], boneSpec.position[1], boneSpec.position[2]);
bones.push(bone);
const flags = boneSpec.flags;
const parent = bones[boneSpec.parentIndex] ?? null;
const pivot = new THREE.Vector3(boneSpec.pivot[0], boneSpec.pivot[1], boneSpec.pivot[2]);
const bone = new ModelBone(flags, parent, pivot);

if (boneSpec.parentIndex === -1) {
rootBones.push(bone);
} else {
bones[boneSpec.parentIndex].add(bone);
}
bones.push(bone);
}

this.skeleton = new THREE.Skeleton(bones);
this.rootBones = rootBones;
this.skeleton = new ModelSkeleton(this.#model.mesh, bones);
}

#autoplay() {
Expand Down
4 changes: 2 additions & 2 deletions src/lib/model/ModelAnimator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class ModelAnimator {
this.#modelsByAnimation.delete(animation);
}

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

for (const model of this.#modelsByAnimation.values()) {
Expand All @@ -80,7 +80,7 @@ class ModelAnimator {

// Ensure bone matrices are updated (matrix world auto-updates are disabled)
if (model.skinned) {
model.updateMatrixWorld();
model.animation.skeleton.updateBones(camera);
}
}
}
Expand Down
20 changes: 20 additions & 0 deletions src/lib/model/ModelBone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as THREE from 'three';

class ModelBone {
flags: number;
parent: ModelBone | null;
pivot: THREE.Vector3;

translation = new THREE.Vector3();
rotation = new THREE.Quaternion();
scale = new THREE.Vector3(1.0, 1.0, 1.0);
matrix = new THREE.Matrix4();

constructor(flags: number, parent: ModelBone | null, pivot: THREE.Vector3) {
this.flags = flags;
this.parent = parent;
this.pivot = pivot;
}
}

export default ModelBone;
10 changes: 5 additions & 5 deletions src/lib/model/ModelManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ class ModelManager {
return this.#createModel(resources);
}

update(deltaTime: number) {
update(deltaTime: number, camera: THREE.Camera) {
for (const resources of this.#loaded.values()) {
if (resources.animator) {
resources.animator.update(deltaTime);
resources.animator.update(deltaTime, camera);
}
}
}
Expand Down Expand Up @@ -274,13 +274,13 @@ class ModelManager {

for (const [index, bone] of spec.bones.entries()) {
animator.registerTrack(
{ state: 'bones', index, property: 'position' },
bone.positionTrack,
{ state: 'bones', index, property: 'translation' },
bone.translationTrack,
THREE.VectorKeyframeTrack,
);

animator.registerTrack(
{ state: 'bones', index, property: 'quaternion' },
{ state: 'bones', index, property: 'rotation' },
bone.rotationTrack,
THREE.QuaternionKeyframeTrack,
(value: number) => (value > 0 ? value - 0x7fff : value + 0x7fff) / 0x7fff,
Expand Down
74 changes: 74 additions & 0 deletions src/lib/model/ModelSkeleton.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import * as THREE from 'three';
import ModelBone from './ModelBone.js';
import { updateBone } from './skeleton.js';

class ModelSkeleton {
#root: THREE.Object3D;
#bones: ModelBone[];
#boneTexture: THREE.DataTexture;
#boneMatrices: Float32Array;

constructor(root: THREE.Object3D, bones: ModelBone[] = []) {
this.#root = root;
this.#bones = bones;

this.#createBoneData();
}

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

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

dispose() {
if (this.#boneTexture) {
this.#boneTexture.dispose();
}
}

update() {
// noop
}

updateBones(camera: THREE.Camera) {
// Ensure model view matrix is synchronized
this.#root.modelViewMatrix.multiplyMatrices(camera.matrixWorldInverse, this.#root.matrixWorld);

for (const [index, bone] of this.#bones.entries()) {
updateBone(this.#root, bone);
bone.matrix.toArray(this.#boneMatrices, index * 16);
}

this.#boneTexture.needsUpdate = true;
}

/**
* From Skeleton#computeBoneTexture in Three.js. Creates bone matrices array and bone texture.
*
* @private
*/
#createBoneData() {
let size = Math.sqrt(this.#bones.length * 4);
size = Math.ceil(size / 4) * 4;
size = Math.max(size, 4);

const boneMatrices = new Float32Array(size * size * 4);

const boneTexture = new THREE.DataTexture(
boneMatrices,
size,
size,
THREE.RGBAFormat,
THREE.FloatType,
);
boneTexture.needsUpdate = true;

this.#boneMatrices = boneMatrices;
this.#boneTexture = boneTexture;
}
}

export default ModelSkeleton;
27 changes: 2 additions & 25 deletions src/lib/model/loader/ModelLoaderWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,29 +162,6 @@ class ModelLoaderWorker extends SceneWorker {
let skinned = false;

for (const bone of model.bones) {
let position = bone.pivot;

// Convert pivot to absolute position
let parentBone = boneSpecs[bone.parentIndex];
while (parentBone) {
position[0] -= parentBone.position[0];
position[1] -= parentBone.position[1];
position[2] -= parentBone.position[2];

parentBone = boneSpecs[parentBone.parentIndex];
}

// Convert translation track to position track
const positionTrack = bone.translationTrack;
for (let s = 0; s < positionTrack.sequenceKeys.length; s++) {
const values = positionTrack.sequenceKeys[s];
for (let i = 0; i < values.length / 3; i++) {
values[i * 3] += position[0];
values[i * 3 + 1] += position[1];
values[i * 3 + 2] += position[2];
}
}

// If bone animations are present, the model needs skinning
const hasTranslationAnim = bone.translationTrack.sequenceTimes.length > 0;
const hasRotationAnim = bone.rotationTrack.sequenceTimes.length > 0;
Expand All @@ -199,10 +176,10 @@ class ModelLoaderWorker extends SceneWorker {
}

boneSpecs.push({
position,
pivot: bone.pivot,
parentIndex: bone.parentIndex,
flags: bone.flags,
positionTrack: positionTrack,
translationTrack: bone.translationTrack,
rotationTrack: bone.rotationTrack,
scaleTrack: bone.scaleTrack,
});
Expand Down
6 changes: 3 additions & 3 deletions src/lib/model/loader/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ type SequenceSpec = {
};

type BoneSpec = {
position: Float32Array;
parentIndex: number;
flags: number;
positionTrack: M2Track<Float32Array>;
parentIndex: number;
pivot: Float32Array;
translationTrack: M2Track<Float32Array>;
rotationTrack: M2Track<Int16Array>;
scaleTrack: M2Track<Float32Array>;
};
Expand Down
16 changes: 6 additions & 10 deletions src/lib/model/shader/vertex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import { composeShader } from '../../shader/util.js';
const VERTEX_SHADER_PRECISIONS = ['highp float'];

const VERTEX_SHADER_UNIFORMS = [
{ name: 'bindMatrix', type: 'mat4', if: 'USE_SKINNING' },
{ name: 'bindMatrixInverse', type: 'mat4', if: 'USE_SKINNING' },
{ name: 'boneTexture', type: 'highp sampler2D', if: 'USE_SKINNING' },
{ name: 'modelMatrix', type: 'mat4' },
{ name: 'modelViewMatrix', type: 'mat4' },
Expand Down Expand Up @@ -79,12 +77,12 @@ vec3 objectNormal = normal;
skinMatrix += skinWeight.y * boneMatY;
skinMatrix += skinWeight.z * boneMatZ;
skinMatrix += skinWeight.w * boneMatW;
skinMatrix = bindMatrixInverse * skinMatrix * bindMatrix;
objectNormal = vec4(skinMatrix * vec4(objectNormal, 0.0)).xyz;
vec3 skinNormal = vec4(skinMatrix * vec4(objectNormal, 0.0)).xyz;
vViewNormal = normalize(skinNormal);
#else
vViewNormal = normalize(normalMatrix * objectNormal);
#endif
vViewNormal = normalize(normalMatrix * objectNormal);
`;

const VERTEX_SHADER_MAIN_FOG = `
Expand All @@ -96,17 +94,15 @@ ${VARIABLE_FOG_FACTOR.name} = calculateFogFactor(${UNIFORM_FOG_PARAMS.name}, cam

const VERTEX_SHADER_MAIN_POSITION = `
#ifdef USE_SKINNING
vec4 skinVertex = bindMatrix * vec4(position, 1.0);
vec4 skinVertex = vec4(position, 1.0);
vec4 skinned = vec4(0.0);
skinned += boneMatX * skinVertex * skinWeight.x;
skinned += boneMatY * skinVertex * skinWeight.y;
skinned += boneMatZ * skinVertex * skinWeight.z;
skinned += boneMatW * skinVertex * skinWeight.w;
vec3 skinnedPosition = (bindMatrixInverse * skinned).xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(skinnedPosition, 1.0);
gl_Position = projectionMatrix * skinned;
#else
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
#endif
Expand Down
Loading

0 comments on commit 9e4d1df

Please sign in to comment.