Skip to content

Commit 0c3933d

Browse files
dakerfloryst
authored andcommitted
feat(GLTFImporter): add vtkGLTFImporter
1 parent 1043314 commit 0c3933d

File tree

15 files changed

+2853
-0
lines changed

15 files changed

+2853
-0
lines changed
Loading

Documentation/content/examples/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ This will allow you to see the some live code running in your browser. Just pick
173173
[![PLYWriter Example][PLYWriterWithIcon]](./PLYWriter.html "PLY writer(ply)")
174174
[![STLReader Example][STLReaderWithIcon]](./STLReader.html "STL reader(stl)")
175175
[![STLWriter Example][STLWriterWithIcon]](./STLWriter.html "STL writer(stl)")
176+
[![GLTFImporter Example][GLTFImporter]](./GLTFImporter.html "GLTF importer(gltf, glb)")
176177
[![PolyDataReader Example][PolyDataReaderWithIcon]](./PolyDataReader.html "VTK legacy reader(VTK)")
177178
[![ElevationReader Example][ElevationReaderWithIcon]](./ElevationReader.html "Elevation reader(CSV, JPG)")
178179
[![OBJReader Example][OBJReaderWithIcon]](./OBJReader.html "OBJ reader(OBJ, MTL, JPG)")
@@ -195,6 +196,7 @@ This will allow you to see the some live code running in your browser. Just pick
195196
[PLYWriterWithIcon]: ../docs/gallery/PLYWriterWithIcon.jpg
196197
[STLReaderWithIcon]: ../docs/gallery/STLReaderWithIcon.jpg
197198
[STLWriterWithIcon]: ../docs/gallery/STLWriterWithIcon.jpg
199+
[GLTFImporter]: ../docs/gallery/GLTFImporterWithIcon.jpg
198200
[PolyDataReaderWithIcon]: ../docs/gallery/VTKReaderWithIcon.jpg
199201
[ElevationReaderWithIcon]: ../docs/gallery/ElevationReaderWithIcon.jpg
200202
[OBJReaderWithIcon]: ../docs/gallery/OBJReaderWithIcon.jpg
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
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} glTFChannel
10+
* @param {glTFChannel[]} glTFSamplers
11+
* @returns
12+
*/
13+
function createAnimationChannel(glTFChannel, glTFSamplers) {
14+
const path = glTFChannel.target.path;
15+
const node = glTFChannel.target.node;
16+
17+
function applyAnimation(value) {
18+
let axisAngle;
19+
let w;
20+
let nq;
21+
switch (path) {
22+
case 'translation':
23+
node.setPosition(value[0], value[1], value[2]);
24+
break;
25+
case 'rotation':
26+
// Convert quaternion to axis-angle representation
27+
nq = quat.normalize(quat.create(), value);
28+
axisAngle = new Float64Array(3);
29+
w = quat.getAxisAngle(axisAngle, nq);
30+
// Apply rotation using rotateWXYZ
31+
node.rotateWXYZ(
32+
vtkMath.degreesFromRadians(w),
33+
axisAngle[0],
34+
axisAngle[1],
35+
axisAngle[2]
36+
);
37+
break;
38+
case 'scale':
39+
node.setScale(value[0], value[1], value[2]);
40+
break;
41+
default:
42+
vtkWarningMacro(`Unsupported animation path: ${path}`);
43+
}
44+
}
45+
46+
function animate(currentTime) {
47+
const sampler = glTFSamplers[glTFChannel.sampler];
48+
const value = sampler.evaluate(currentTime, path);
49+
applyAnimation(value);
50+
}
51+
52+
return { ...glTFChannel, animate };
53+
}
54+
55+
/**
56+
* Create an animation sampler
57+
* @param {glTFSampler} glTFSampler
58+
* @returns
59+
*/
60+
function createAnimationSampler(glTFSampler) {
61+
let lastKeyframeIndex = 0;
62+
63+
function findKeyframes(time) {
64+
let i1 = lastKeyframeIndex;
65+
while (i1 < glTFSampler.input.length - 1 && glTFSampler.input[i1] <= time) {
66+
i1++;
67+
}
68+
const i0 = Math.max(0, i1 - 1);
69+
lastKeyframeIndex = i0;
70+
return [glTFSampler.input[i0], glTFSampler.input[i1], i0, i1];
71+
}
72+
73+
function stepInterpolate(path, i0) {
74+
const startIndex = i0 * 3;
75+
const v0 = new Array(3);
76+
for (let i = 0; i < 3; ++i) {
77+
v0[i] = glTFSampler.output[startIndex + i];
78+
}
79+
80+
return v0;
81+
}
82+
83+
function linearInterpolate(path, t0, t1, i0, i1, t) {
84+
const ratio = (t - t0) / (t1 - t0);
85+
const startIndex = i0 * 4;
86+
const endIndex = i1 * 4;
87+
88+
const v0 = new Array(4);
89+
const v1 = new Array(4);
90+
for (let i = 0; i < 4; ++i) {
91+
v0[i] = glTFSampler.output[startIndex + i];
92+
v1[i] = glTFSampler.output[endIndex + i];
93+
}
94+
95+
switch (path) {
96+
case 'translation':
97+
case 'scale':
98+
return vec3.lerp(vec3.create(), v0, v1, ratio);
99+
case 'rotation':
100+
return quat.slerp(quat.create(), v0, v1, ratio);
101+
default:
102+
vtkWarningMacro(`Unsupported animation path: ${path}`);
103+
return null;
104+
}
105+
}
106+
107+
function cubicSplineInterpolate(path, t0, t1, i0, i1, time) {
108+
const dt = t1 - t0;
109+
const t = (time - t0) / dt;
110+
const t2 = t * t;
111+
const t3 = t2 * t;
112+
113+
const p0 = glTFSampler.output[i0 * 3 + 1];
114+
const m0 = dt * glTFSampler.output[i0 * 3 + 2];
115+
const p1 = glTFSampler.output[i1 * 3 + 1];
116+
const m1 = dt * glTFSampler.output[i1 * 3];
117+
118+
if (Array.isArray(p0)) {
119+
return p0.map((v, j) => {
120+
const a = 2 * t3 - 3 * t2 + 1;
121+
const b = t3 - 2 * t2 + t;
122+
const c = -2 * t3 + 3 * t2;
123+
const d = t3 - t2;
124+
return a * v + b * m0[j] + c * p1[j] + d * m1[j];
125+
});
126+
}
127+
128+
const a = 2 * t3 - 3 * t2 + 1;
129+
const b = t3 - 2 * t2 + t;
130+
const c = -2 * t3 + 3 * t2;
131+
const d = t3 - t2;
132+
return a * p0 + b * m0 + c * p1 + d * m1;
133+
}
134+
135+
function evaluate(time, path) {
136+
const [t0, t1, i0, i1] = findKeyframes(time);
137+
138+
let result;
139+
140+
switch (glTFSampler.interpolation) {
141+
case 'STEP':
142+
result = stepInterpolate(path, i0);
143+
break;
144+
case 'LINEAR':
145+
result = linearInterpolate(path, t0, t1, i0, i1, time);
146+
break;
147+
case 'CUBICSPLINE':
148+
result = cubicSplineInterpolate(path, t0, t1, i0, i1, time);
149+
break;
150+
default:
151+
throw new Error(
152+
`Unknown interpolation method: ${glTFSampler.interpolation}`
153+
);
154+
}
155+
return result;
156+
}
157+
158+
return { ...glTFSampler, evaluate };
159+
}
160+
161+
/**
162+
* Create an animation
163+
* @param {glTFAnimation} glTFAnimation
164+
* @param {Map} nodes
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 {Map} nodes
187+
* @param {*} accessors
188+
* @returns
189+
*/
190+
function createAnimationMixer(nodes, accessors) {
191+
const animations = new Map();
192+
const activeAnimations = new Map();
193+
194+
function addAnimation(glTFAnimation) {
195+
const annimation = createAnimation(glTFAnimation, nodes, accessors);
196+
animations.set(glTFAnimation.id, annimation);
197+
vtkDebugMacro(`Animation "${glTFAnimation.id}" added to mixer`);
198+
}
199+
200+
function play(name, weight = 1) {
201+
if (!animations.has(name)) {
202+
vtkWarningMacro(`Animation "${name}" not found in mixer`);
203+
return;
204+
}
205+
activeAnimations.set(name, {
206+
animation: animations.get(name),
207+
weight,
208+
time: 0,
209+
});
210+
vtkDebugMacro(`Playing animation "${name}" with weight ${weight}`);
211+
}
212+
213+
function stop(name) {
214+
if (activeAnimations.delete(name)) {
215+
vtkWarningMacro(`Stopped animation "${name}"`);
216+
} else {
217+
vtkWarningMacro(`Animation "${name}" was not playing`);
218+
}
219+
}
220+
221+
function stopAll() {
222+
activeAnimations.clear();
223+
vtkWarningMacro('Stopped all animations');
224+
}
225+
226+
function update(deltaTime) {
227+
// Normalize weights
228+
const totalWeight = Array.from(activeAnimations.values()).reduce(
229+
(sum, { weight }) => sum + weight,
230+
0
231+
);
232+
233+
activeAnimations.forEach(({ animation, weight, time }, name) => {
234+
const normalizedWeight = totalWeight > 0 ? weight / totalWeight : 0;
235+
const newTime = time + deltaTime;
236+
activeAnimations.set(name, { animation, weight, time: newTime });
237+
238+
vtkDebugMacro(
239+
`Updating animation "${name}" at time ${newTime.toFixed(
240+
3
241+
)} with normalized weight ${normalizedWeight.toFixed(3)}`
242+
);
243+
244+
animation.update(newTime, normalizedWeight);
245+
});
246+
}
247+
248+
return { addAnimation, play, stop, stopAll, update };
249+
}
250+
251+
export {
252+
createAnimation,
253+
createAnimationChannel,
254+
createAnimationMixer,
255+
createAnimationSampler,
256+
};
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)