Skip to content

Commit 01dbdb7

Browse files
dakerAdnane Belmadiaf
authored andcommitted
feat(GLTFReader): add GLTFReader
1 parent 8057859 commit 01dbdb7

File tree

15 files changed

+2058
-0
lines changed

15 files changed

+2058
-0
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
class AnimationClip {
2+
constructor(name) {
3+
this.name = name;
4+
this.tracks = [];
5+
this.duration = -1;
6+
}
7+
8+
addTrack(track) {
9+
this.tracks.push(track);
10+
this.duration = Math.max(this.duration, track.duration);
11+
}
12+
}
13+
14+
export default AnimationClip;
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/* eslint-disable no-case-declarations */
2+
/* eslint-disable class-methods-use-this */
3+
import { quat } from 'gl-matrix';
4+
5+
class AnimationMixer {
6+
constructor() {
7+
this.animations = new Map();
8+
this.activeAnimations = new Map();
9+
}
10+
11+
addAnimation(animation) {
12+
this.animations.set(animation.name, animation);
13+
}
14+
15+
play(name, weight = 1.0) {
16+
const animation = this.animations.get(name);
17+
if (animation) {
18+
this.activeAnimations.set(name, { animation, time: 0, weight });
19+
}
20+
}
21+
22+
stop(name) {
23+
this.activeAnimations.delete(name);
24+
}
25+
26+
stopAll() {
27+
this.activeAnimations.clear();
28+
}
29+
30+
update(deltaTime) {
31+
this.activeAnimations.forEach(({ animation, time, weight }, name) => {
32+
const newTime = (time + deltaTime) % animation.duration;
33+
this.activeAnimations.set(name, { animation, time: newTime, weight });
34+
35+
animation.tracks.forEach((track) => {
36+
const value = track.getValue(newTime);
37+
this.applyTrackValueToTarget(track.target, track.path, value, weight);
38+
});
39+
});
40+
}
41+
42+
applyTrackValueToTarget(target, path, value, weight) {
43+
switch (path) {
44+
case 'translation':
45+
target.setPosition(value[0], value[1], value[2]);
46+
break;
47+
case 'rotation':
48+
// Convert quaternion to axis-angle representation
49+
const axisAngle = this.quaternionToAxisAngle(value);
50+
// Apply rotation using rotateWXYZ
51+
target.rotateWXYZ(
52+
axisAngle[3],
53+
axisAngle[0],
54+
axisAngle[1],
55+
axisAngle[2]
56+
);
57+
break;
58+
case 'scale':
59+
target.setScale(value[0], value[1], value[2]);
60+
break;
61+
default:
62+
console.warn(`Unsupported animation path: ${path}`);
63+
}
64+
}
65+
66+
quaternionToAxisAngle(q) {
67+
// Ensure the quaternion is normalized
68+
const nq = quat.normalize(quat.create(), q);
69+
70+
// Calculate the angle
71+
const angle = 2 * Math.acos(nq[3]);
72+
73+
// Calculate the axis
74+
const s = Math.sqrt(1 - nq[3] * nq[3]);
75+
let x;
76+
let y;
77+
let z;
78+
if (s < 0.001) {
79+
// If s is close to zero, the angle is close to 0 or 180 degrees
80+
// In this case, any axis perpendicular to [0, 0, 1] will do
81+
x = 1;
82+
y = 0;
83+
z = 0;
84+
} else {
85+
x = nq[0] / s;
86+
y = nq[1] / s;
87+
z = nq[2] / s;
88+
}
89+
90+
// Convert angle to degrees
91+
const angleDegrees = angle * (180 / Math.PI);
92+
93+
return [x, y, z, angleDegrees];
94+
}
95+
}
96+
97+
export default AnimationMixer;
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/* eslint-disable operator-assignment */
2+
/* eslint-disable no-param-reassign */
3+
import { quat, vec3 } from 'gl-matrix';
4+
5+
class AnimationTrack {
6+
constructor(target, path, times, values, interpolation) {
7+
this.target = target;
8+
this.path = path;
9+
this.times = times;
10+
this.values = values;
11+
this.interpolation = interpolation;
12+
this.duration = times[times.length - 1];
13+
this.componentsPerValue = this.getComponentsPerValue();
14+
}
15+
16+
getComponentsPerValue() {
17+
switch (this.path) {
18+
case 'translation':
19+
case 'scale':
20+
return 3;
21+
case 'rotation':
22+
return 4;
23+
default:
24+
console.warn(`Unsupported animation path: ${this.path}`);
25+
return 0;
26+
}
27+
}
28+
29+
getValue(time) {
30+
time = time % this.duration; // Loop the animation
31+
32+
let i = 1;
33+
for (; i < this.times.length; i++) {
34+
if (this.times[i] > time) break;
35+
}
36+
i--;
37+
38+
const t = (time - this.times[i]) / (this.times[i + 1] - this.times[i]);
39+
40+
const startIndex = i * this.componentsPerValue;
41+
const endIndex = (i + 1) * this.componentsPerValue;
42+
43+
const value1 = this.values.subarray(
44+
startIndex,
45+
startIndex + this.componentsPerValue
46+
);
47+
const value2 = this.values.subarray(
48+
endIndex,
49+
endIndex + this.componentsPerValue
50+
);
51+
52+
switch (this.interpolation) {
53+
case 'STEP':
54+
return value1;
55+
case 'CUBICSPLINE':
56+
console.warn('Cubic spline interpolation not implemented');
57+
return this.linearInterpolate(value1, value2, t);
58+
case 'LINEAR':
59+
default:
60+
return this.linearInterpolate(value1, value2, t);
61+
}
62+
}
63+
64+
linearInterpolate(a, b, t) {
65+
switch (this.path) {
66+
case 'translation':
67+
case 'scale':
68+
return vec3.lerp(vec3.create(), a, b, t);
69+
case 'rotation':
70+
return quat.slerp(quat.create(), a, b, t);
71+
default:
72+
console.warn(`Unsupported animation path: ${this.path}`);
73+
return null;
74+
}
75+
}
76+
}
77+
78+
export default AnimationTrack;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import AnimationClip from './AnimationClip';
2+
import AnimationMixer from './AnimationMixer';
3+
import AnimationTrack from './AnimationTrack';
4+
5+
export default {
6+
AnimationClip,
7+
AnimationMixer,
8+
AnimationTrack,
9+
};
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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+
5+
export const COMPONENTS = {
6+
SCALAR: 1,
7+
VEC2: 2,
8+
VEC3: 3,
9+
VEC4: 4,
10+
MAT2: 4,
11+
MAT3: 9,
12+
MAT4: 16,
13+
};
14+
15+
export const BYTES = {
16+
5120: 1, // BYTE
17+
5121: 1, // UNSIGNED_BYTE
18+
5122: 2, // SHORT
19+
5123: 2, // UNSIGNED_SHORT
20+
5125: 4, // UNSIGNED_INT
21+
5126: 4, // FLOAT
22+
};
23+
24+
export const MODES = {
25+
GL_POINTS: 0,
26+
GL_LINES: 1,
27+
GL_LINE_LOOP: 2,
28+
GL_LINE_STRIP: 3,
29+
GL_TRIANGLES: 4,
30+
GL_TRIANGLE_STRIP: 5,
31+
GL_TRIANGLE_FAN: 6,
32+
};
33+
34+
export const ARRAY_TYPES = {
35+
5120: Int8Array,
36+
5121: Uint8Array,
37+
5122: Int16Array,
38+
5123: Uint16Array,
39+
5125: Uint32Array,
40+
5126: Float32Array,
41+
};
42+
43+
export const GL_SAMPLER = {
44+
NEAREST: 9728,
45+
LINEAR: 9729,
46+
NEAREST_MIPMAP_NEAREST: 9984,
47+
LINEAR_MIPMAP_NEAREST: 9985,
48+
NEAREST_MIPMAP_LINEAR: 9986,
49+
LINEAR_MIPMAP_LINEAR: 9987,
50+
REPEAT: 10497,
51+
CLAMP_TO_EDGE: 33071,
52+
MIRRORED_REPEAT: 33648,
53+
TEXTURE_MAG_FILTER: 10240,
54+
TEXTURE_MIN_FILTER: 10241,
55+
TEXTURE_WRAP_S: 10242,
56+
TEXTURE_WRAP_T: 10243,
57+
};
58+
59+
export const DEFAULT_SAMPLER = {
60+
magFilter: GL_SAMPLER.NEAREST,
61+
minFilter: GL_SAMPLER.LINEAR_MIPMAP_LINEAR,
62+
wrapS: GL_SAMPLER.REPEAT,
63+
wrapT: GL_SAMPLER.REPEAT,
64+
};
65+
66+
export const semanticAttributeMap = {
67+
NORMAL: 'normal',
68+
POSITION: 'position',
69+
TEXCOORD_0: 'texcoord0',
70+
TEXCOORD_1: 'texcoord1',
71+
WEIGHTS_0: 'weight',
72+
JOINTS_0: 'joint',
73+
COLOR_0: 'color',
74+
};
75+
76+
export const alphaMode = {
77+
OPAQUE: 'OPAQUE',
78+
MASK: 'MASK',
79+
BLEND: 'BLEND',
80+
};
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import BinaryHelper from 'vtk.js/Sources/IO/Core/BinaryHelper';
2+
import {
3+
BINARY_CHUNK_TYPES,
4+
BINARY_HEADER_LENGTH,
5+
BINARY_HEADER_MAGIC,
6+
} from './Contants';
7+
8+
function parseGLB(data) {
9+
let json;
10+
let buffer;
11+
12+
const headerView = new DataView(data, 0, BINARY_HEADER_LENGTH);
13+
14+
const header = {
15+
magic: BinaryHelper.arrayBufferToString(new Uint8Array(data, 0, 4)),
16+
version: headerView.getUint32(4, true),
17+
length: headerView.getUint32(8, true),
18+
};
19+
20+
if (header.magic !== BINARY_HEADER_MAGIC) {
21+
throw new Error('Unsupported glTF-Binary header.');
22+
} else if (header.version < 2.0) {
23+
throw new Error('Unsupported legacy binary file detected.');
24+
}
25+
26+
const chunkView = new DataView(data, BINARY_HEADER_LENGTH);
27+
let chunkIndex = 0;
28+
29+
while (chunkIndex < chunkView.byteLength) {
30+
const chunkLength = chunkView.getUint32(chunkIndex, true);
31+
chunkIndex += 4;
32+
33+
const chunkType = chunkView.getUint32(chunkIndex, true);
34+
chunkIndex += 4;
35+
36+
if (chunkType === BINARY_CHUNK_TYPES.JSON) {
37+
const contentArray = new Uint8Array(
38+
data,
39+
BINARY_HEADER_LENGTH + chunkIndex,
40+
chunkLength
41+
);
42+
json = BinaryHelper.arrayBufferToString(contentArray);
43+
} else if (chunkType === BINARY_CHUNK_TYPES.BIN) {
44+
const byteOffset = BINARY_HEADER_LENGTH + chunkIndex;
45+
const binaryChunk = new Uint8Array(data, byteOffset, chunkLength);
46+
buffer = binaryChunk.buffer;
47+
}
48+
49+
chunkIndex += chunkLength;
50+
}
51+
52+
if (!json) {
53+
throw new Error('glTF-Binary: JSON content not found.');
54+
}
55+
if (!buffer) {
56+
throw new Error('glTF-Binary: Binary chunk not found.');
57+
}
58+
return { json, buffer };
59+
}
60+
61+
export default parseGLB;

0 commit comments

Comments
 (0)