Skip to content

Commit 2e8152b

Browse files
dakerAdnane Belmadiaf
authored andcommitted
feat(GLTFImporter): add GLTFImporter
1 parent 4691eea commit 2e8152b

File tree

15 files changed

+3163
-388
lines changed

15 files changed

+3163
-388
lines changed
29.8 KB
Loading

Documentation/content/examples/index.md

Lines changed: 390 additions & 388 deletions
Large diffs are not rendered by default.
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
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+
v1[i] = glTFSampler.output[endIndex + i];
92+
}
93+
94+
switch (path) {
95+
case 'translation':
96+
case 'scale':
97+
return vec3.lerp(vec3.create(), v0, v1, ratio);
98+
case 'rotation':
99+
return quat.slerp(quat.create(), v0, v1, ratio);
100+
default:
101+
vtkWarningMacro(`Unsupported animation path: ${path}`);
102+
return null;
103+
}
104+
}
105+
106+
function cubicSplineInterpolate(path, t0, t1, i0, i1, time) {
107+
const dt = t1 - t0;
108+
const t = (time - t0) / dt;
109+
const t2 = t * t;
110+
const t3 = t2 * t;
111+
112+
const p0 = glTFSampler.output[i0 * 3 + 1];
113+
const m0 = dt * glTFSampler.output[i0 * 3 + 2];
114+
const p1 = glTFSampler.output[i1 * 3 + 1];
115+
const m1 = dt * glTFSampler.output[i1 * 3];
116+
117+
if (Array.isArray(p0)) {
118+
return p0.map((v, j) => {
119+
const a = 2 * t3 - 3 * t2 + 1;
120+
const b = t3 - 2 * t2 + t;
121+
const c = -2 * t3 + 3 * t2;
122+
const d = t3 - t2;
123+
return a * v + b * m0[j] + c * p1[j] + d * m1[j];
124+
});
125+
}
126+
127+
const a = 2 * t3 - 3 * t2 + 1;
128+
const b = t3 - 2 * t2 + t;
129+
const c = -2 * t3 + 3 * t2;
130+
const d = t3 - t2;
131+
return a * p0 + b * m0 + c * p1 + d * m1;
132+
}
133+
134+
function evaluate(time, path) {
135+
const [t0, t1, i0, i1] = findKeyframes(time);
136+
137+
let result;
138+
139+
switch (glTFSampler.interpolation) {
140+
case 'STEP':
141+
result = stepInterpolate(path, i0);
142+
break;
143+
case 'LINEAR':
144+
result = linearInterpolate(path, t0, t1, i0, i1, time);
145+
break;
146+
case 'CUBICSPLINE':
147+
result = cubicSplineInterpolate(path, t0, t1, i0, i1, time);
148+
break;
149+
default:
150+
throw new Error(
151+
`Unknown interpolation method: ${glTFSampler.interpolation}`
152+
);
153+
}
154+
return result;
155+
}
156+
157+
return { ...glTFSampler, evaluate };
158+
}
159+
160+
/**
161+
* Create an animation
162+
* @param {glTFAnimation} glTFAnimation
163+
* @returns
164+
*/
165+
function createAnimation(glTFAnimation, nodes) {
166+
glTFAnimation.samplers = glTFAnimation.samplers.map((sampler) =>
167+
createAnimationSampler(sampler)
168+
);
169+
170+
glTFAnimation.channels = glTFAnimation.channels.map((channel) => {
171+
channel.target.node = nodes.get(`node-${channel.target.node}`);
172+
return createAnimationChannel(channel, glTFAnimation.samplers);
173+
});
174+
175+
function update(currentTime) {
176+
glTFAnimation.channels.forEach((channel) => channel.animate(currentTime));
177+
}
178+
179+
return { ...glTFAnimation, update };
180+
}
181+
182+
/**
183+
* Create an animation mixer
184+
* @param {*} nodes
185+
* @returns
186+
*/
187+
function createAnimationMixer(nodes, accessors) {
188+
const animations = new Map();
189+
const activeAnimations = new Map();
190+
191+
function addAnimation(glTFAnimation) {
192+
const annimation = createAnimation(glTFAnimation, nodes, accessors);
193+
animations.set(glTFAnimation.id, annimation);
194+
vtkDebugMacro(`Animation "${glTFAnimation.id}" added to mixer`);
195+
}
196+
197+
function play(name, weight = 1) {
198+
if (!animations.has(name)) {
199+
vtkWarningMacro(`Animation "${name}" not found in mixer`);
200+
return;
201+
}
202+
activeAnimations.set(name, {
203+
animation: animations.get(name),
204+
weight,
205+
time: 0,
206+
});
207+
vtkDebugMacro(`Playing animation "${name}" with weight ${weight}`);
208+
}
209+
210+
function stop(name) {
211+
if (activeAnimations.delete(name)) {
212+
vtkWarningMacro(`Stopped animation "${name}"`);
213+
} else {
214+
vtkWarningMacro(`Animation "${name}" was not playing`);
215+
}
216+
}
217+
218+
function stopAll() {
219+
activeAnimations.clear();
220+
vtkWarningMacro('Stopped all animations');
221+
}
222+
223+
function update(deltaTime) {
224+
// Normalize weights
225+
const totalWeight = Array.from(activeAnimations.values()).reduce(
226+
(sum, { weight }) => sum + weight,
227+
0
228+
);
229+
230+
activeAnimations.forEach(({ animation, weight, time }, name) => {
231+
const normalizedWeight = totalWeight > 0 ? weight / totalWeight : 0;
232+
const newTime = time + deltaTime;
233+
activeAnimations.set(name, { animation, weight, time: newTime });
234+
235+
vtkDebugMacro(
236+
`Updating animation "${name}" at time ${newTime.toFixed(
237+
3
238+
)} with normalized weight ${normalizedWeight.toFixed(3)}`
239+
);
240+
241+
animation.update(newTime, normalizedWeight);
242+
});
243+
}
244+
245+
return { addAnimation, play, stop, stopAll, update };
246+
}
247+
248+
export {
249+
createAnimation,
250+
createAnimationChannel,
251+
createAnimationMixer,
252+
createAnimationSampler,
253+
};
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)