Skip to content

Commit 5f189dc

Browse files
dakerAdnane Belmadiaf
authored andcommitted
feat(GLTFImporter): add GLTFImporter
1 parent 8057859 commit 5f189dc

File tree

13 files changed

+2735
-0
lines changed

13 files changed

+2735
-0
lines changed
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
import macro from 'vtk.js/Sources/macros';
2+
import * as vtkMath from 'vtk.js/Sources/Common/Core/Math';
3+
import { quat, vec3 } from 'gl-matrix';
4+
5+
const { vtkDebugMacro, vtkWarningMacro } = macro;
6+
7+
/**
8+
* Create an animation channel
9+
* @param {*} glTFChannel
10+
* @returns
11+
*/
12+
function createAnimationChannel(glTFChannel, glTFSamplers) {
13+
const path = glTFChannel.target.path;
14+
const node = glTFChannel.target.node;
15+
16+
function applyAnimation(value) {
17+
let axisAngle;
18+
let w;
19+
let nq;
20+
switch (path) {
21+
case 'translation':
22+
node.setPosition(value[0], value[1], value[2]);
23+
break;
24+
case 'rotation':
25+
// Convert quaternion to axis-angle representation
26+
nq = quat.normalize(quat.create(), value);
27+
axisAngle = new Float64Array(3);
28+
w = quat.getAxisAngle(axisAngle, nq);
29+
// Apply rotation using rotateWXYZ
30+
node.rotateWXYZ(
31+
vtkMath.degreesFromRadians(w),
32+
axisAngle[0],
33+
axisAngle[1],
34+
axisAngle[2]
35+
);
36+
break;
37+
case 'scale':
38+
node.setScale(value[0], value[1], value[2]);
39+
break;
40+
default:
41+
vtkWarningMacro(`Unsupported animation path: ${path}`);
42+
}
43+
}
44+
45+
function animate(currentTime) {
46+
const sampler = glTFSamplers[glTFChannel.sampler];
47+
const value = sampler.evaluate(currentTime, path);
48+
applyAnimation(value);
49+
}
50+
51+
return { ...glTFChannel, animate };
52+
}
53+
54+
/**
55+
* Create an animation sampler
56+
* @param {glTFSampler} glTFSampler
57+
* @returns
58+
*/
59+
function createAnimationSampler(glTFSampler) {
60+
let lastKeyframeIndex = 0;
61+
62+
function findKeyframes(time) {
63+
let i1 = lastKeyframeIndex;
64+
while (i1 < glTFSampler.input.length - 1 && glTFSampler.input[i1] <= time) {
65+
i1++;
66+
}
67+
const i0 = Math.max(0, i1 - 1);
68+
lastKeyframeIndex = i0;
69+
return [glTFSampler.input[i0], glTFSampler.input[i1], i0, i1];
70+
}
71+
72+
function stepInterpolate(path, i0) {
73+
const startIndex = i0 * 3;
74+
const v0 = new Array(3);
75+
for (let i = 0; i < 3; ++i) {
76+
v0[i] = glTFSampler.output[startIndex + i];
77+
}
78+
79+
return v0;
80+
}
81+
82+
function linearInterpolate(path, t0, t1, i0, i1, t) {
83+
const ratio = (t - t0) / (t1 - t0);
84+
const startIndex = i0 * 4;
85+
const endIndex = i1 * 4;
86+
87+
const v0 = new Array(4);
88+
const v1 = new Array(4);
89+
for (let i = 0; i < 4; ++i) {
90+
v0[i] = glTFSampler.output[startIndex + i];
91+
}
92+
for (let i = 0; i < 4; ++i) {
93+
v1[i] = glTFSampler.output[endIndex + i];
94+
}
95+
96+
switch (path) {
97+
case 'translation':
98+
case 'scale':
99+
return vec3.lerp(vec3.create(), v0, v1, ratio);
100+
case 'rotation':
101+
return quat.slerp(quat.create(), v0, v1, ratio);
102+
default:
103+
vtkWarningMacro(`Unsupported animation path: ${path}`);
104+
return null;
105+
}
106+
}
107+
108+
function cubicSplineInterpolate(path, t0, t1, i0, i1, time) {
109+
const dt = t1 - t0;
110+
const t = (time - t0) / dt;
111+
const t2 = t * t;
112+
const t3 = t2 * t;
113+
114+
const p0 = glTFSampler.output[i0 * 3 + 1];
115+
const m0 = dt * glTFSampler.output[i0 * 3 + 2];
116+
const p1 = glTFSampler.output[i1 * 3 + 1];
117+
const m1 = dt * glTFSampler.output[i1 * 3];
118+
119+
if (Array.isArray(p0)) {
120+
return p0.map((v, j) => {
121+
const a = 2 * t3 - 3 * t2 + 1;
122+
const b = t3 - 2 * t2 + t;
123+
const c = -2 * t3 + 3 * t2;
124+
const d = t3 - t2;
125+
return a * v + b * m0[j] + c * p1[j] + d * m1[j];
126+
});
127+
}
128+
129+
const a = 2 * t3 - 3 * t2 + 1;
130+
const b = t3 - 2 * t2 + t;
131+
const c = -2 * t3 + 3 * t2;
132+
const d = t3 - t2;
133+
return a * p0 + b * m0 + c * p1 + d * m1;
134+
}
135+
136+
function evaluate(time, path) {
137+
const [t0, t1, i0, i1] = findKeyframes(time);
138+
139+
let result;
140+
141+
switch (glTFSampler.interpolation) {
142+
case 'STEP':
143+
result = stepInterpolate(path, i0);
144+
break;
145+
case 'LINEAR':
146+
result = linearInterpolate(path, t0, t1, i0, i1, time);
147+
break;
148+
case 'CUBICSPLINE':
149+
result = cubicSplineInterpolate(path, t0, t1, i0, i1, time);
150+
break;
151+
default:
152+
throw new Error(
153+
`Unknown interpolation method: ${glTFSampler.interpolation}`
154+
);
155+
}
156+
return result;
157+
}
158+
159+
return { ...glTFSampler, evaluate };
160+
}
161+
162+
/**
163+
* Create an animation
164+
* @param {glTFAnimation} glTFAnimation
165+
* @returns
166+
*/
167+
function createAnimation(glTFAnimation, nodes) {
168+
glTFAnimation.samplers = glTFAnimation.samplers.map((sampler) =>
169+
createAnimationSampler(sampler)
170+
);
171+
172+
glTFAnimation.channels = glTFAnimation.channels.map((channel) => {
173+
channel.target.node = nodes.get(`node-${channel.target.node}`);
174+
return createAnimationChannel(channel, glTFAnimation.samplers);
175+
});
176+
177+
function update(currentTime) {
178+
glTFAnimation.channels.forEach((channel) => channel.animate(currentTime));
179+
}
180+
181+
return { ...glTFAnimation, update };
182+
}
183+
184+
/**
185+
* Create an animation mixer
186+
* @param {*} nodes
187+
* @returns
188+
*/
189+
function createAnimationMixer(nodes, accessors) {
190+
const animations = new Map();
191+
const activeAnimations = new Map();
192+
193+
function addAnimation(glTFAnimation) {
194+
const annimation = createAnimation(glTFAnimation, nodes, accessors);
195+
animations.set(glTFAnimation.id, annimation);
196+
vtkDebugMacro(`Animation "${glTFAnimation.id}" added to mixer`);
197+
}
198+
199+
function play(name, weight = 1) {
200+
if (!animations.has(name)) {
201+
vtkWarningMacro(`Animation "${name}" not found in mixer`);
202+
return;
203+
}
204+
activeAnimations.set(name, {
205+
animation: animations.get(name),
206+
weight,
207+
time: 0,
208+
});
209+
vtkDebugMacro(`Playing animation "${name}" with weight ${weight}`);
210+
}
211+
212+
function stop(name) {
213+
if (activeAnimations.delete(name)) {
214+
vtkWarningMacro(`Stopped animation "${name}"`);
215+
} else {
216+
vtkWarningMacro(`Animation "${name}" was not playing`);
217+
}
218+
}
219+
220+
function stopAll() {
221+
activeAnimations.clear();
222+
vtkWarningMacro('Stopped all animations');
223+
}
224+
225+
function update(deltaTime) {
226+
// Normalize weights
227+
const totalWeight = Array.from(activeAnimations.values()).reduce(
228+
(sum, { weight }) => sum + weight,
229+
0
230+
);
231+
232+
activeAnimations.forEach(({ animation, weight, time }, name) => {
233+
const normalizedWeight = totalWeight > 0 ? weight / totalWeight : 0;
234+
const newTime = time + deltaTime;
235+
activeAnimations.set(name, { animation, weight, time: newTime });
236+
237+
vtkDebugMacro(
238+
`Updating animation "${name}" at time ${newTime.toFixed(
239+
3
240+
)} with normalized weight ${normalizedWeight.toFixed(3)}`
241+
);
242+
243+
animation.update(newTime, normalizedWeight);
244+
});
245+
}
246+
247+
return { addAnimation, play, stop, stopAll, update };
248+
}
249+
250+
export {
251+
createAnimation,
252+
createAnimationChannel,
253+
createAnimationMixer,
254+
createAnimationSampler,
255+
};
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
export const BINARY_HEADER_MAGIC = 'glTF';
2+
export const BINARY_HEADER_LENGTH = 12;
3+
export const BINARY_CHUNK_TYPES = { JSON: 0x4e4f534a, BIN: 0x004e4942 };
4+
export const BINARY_HEADER_INTS = 3;
5+
export const BINARY_CHUNK_HEADER_INTS = 2;
6+
7+
export const MIN_LIGHT_ATTENUATION = 0.01;
8+
9+
export const COMPONENTS = {
10+
SCALAR: 1,
11+
VEC2: 2,
12+
VEC3: 3,
13+
VEC4: 4,
14+
MAT2: 4,
15+
MAT3: 9,
16+
MAT4: 16,
17+
};
18+
19+
export const BYTES = {
20+
5120: 1, // BYTE
21+
5121: 1, // UNSIGNED_BYTE
22+
5122: 2, // SHORT
23+
5123: 2, // UNSIGNED_SHORT
24+
5125: 4, // UNSIGNED_INT
25+
5126: 4, // FLOAT
26+
};
27+
28+
export const MODES = {
29+
GL_POINTS: 0,
30+
GL_LINES: 1,
31+
GL_LINE_LOOP: 2,
32+
GL_LINE_STRIP: 3,
33+
GL_TRIANGLES: 4,
34+
GL_TRIANGLE_STRIP: 5,
35+
GL_TRIANGLE_FAN: 6,
36+
};
37+
38+
export const ARRAY_TYPES = {
39+
5120: Int8Array,
40+
5121: Uint8Array,
41+
5122: Int16Array,
42+
5123: Uint16Array,
43+
5125: Uint32Array,
44+
5126: Float32Array,
45+
};
46+
47+
export const GL_SAMPLER = {
48+
NEAREST: 9728,
49+
LINEAR: 9729,
50+
NEAREST_MIPMAP_NEAREST: 9984,
51+
LINEAR_MIPMAP_NEAREST: 9985,
52+
NEAREST_MIPMAP_LINEAR: 9986,
53+
LINEAR_MIPMAP_LINEAR: 9987,
54+
REPEAT: 10497,
55+
CLAMP_TO_EDGE: 33071,
56+
MIRRORED_REPEAT: 33648,
57+
TEXTURE_MAG_FILTER: 10240,
58+
TEXTURE_MIN_FILTER: 10241,
59+
TEXTURE_WRAP_S: 10242,
60+
TEXTURE_WRAP_T: 10243,
61+
};
62+
63+
export const DEFAULT_SAMPLER = {
64+
magFilter: GL_SAMPLER.NEAREST,
65+
minFilter: GL_SAMPLER.LINEAR_MIPMAP_LINEAR,
66+
wrapS: GL_SAMPLER.REPEAT,
67+
wrapT: GL_SAMPLER.REPEAT,
68+
};
69+
70+
export const SEMANTIC_ATTRIBUTE_MAP = {
71+
NORMAL: 'normal',
72+
POSITION: 'position',
73+
TEXCOORD_0: 'texcoord0',
74+
TEXCOORD_1: 'texcoord1',
75+
WEIGHTS_0: 'weight',
76+
JOINTS_0: 'joint',
77+
COLOR_0: 'color',
78+
TANGENT: 'tangent',
79+
};
80+
81+
export const ALPHA_MODE = {
82+
OPAQUE: 'OPAQUE',
83+
MASK: 'MASK',
84+
BLEND: 'BLEND',
85+
};

0 commit comments

Comments
 (0)