three-vat 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +164 -308
- package/dist/carrier-BFCPmcQK.d.ts +472 -0
- package/dist/chunk-2PGZ44TP.js +202 -0
- package/dist/chunk-W2ZAFMPB.js +53 -0
- package/dist/index.d.ts +78 -14
- package/dist/index.js +137 -47
- package/dist/tsl.d.ts +43 -31
- package/dist/tsl.js +94 -38
- package/dist/webgl.d.ts +31 -26
- package/dist/webgl.js +135 -33
- package/package.json +24 -21
- package/dist/chunk-SXTVASKG.js +0 -40
- package/dist/instance-playback-BrGBIKLe.d.ts +0 -109
package/dist/tsl.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isBatchedCarrier, assertVATCarrier, assertBakedNormal } from './chunk-W2ZAFMPB.js';
|
|
2
|
+
import { INFINITE_REPETITIONS, LoopMode, EndMode, createVATPlaybackTexture, PACK_TEXELS } from './chunk-2PGZ44TP.js';
|
|
2
3
|
import { InstancedMesh } from 'three';
|
|
3
|
-
import { uniform, Fn, positionLocal, positionGeometry, normalLocal, instancedMesh, int, vertexIndex, textureLoad, ivec2, mix,
|
|
4
|
+
import { uniform, Fn, positionLocal, positionGeometry, normalLocal, batch, instancedMesh, int, vertexIndex, float, bool, textureLoad, ivec2, mix, batchIndirectIndex, instanceIndex, hash } from 'three/tsl';
|
|
4
5
|
|
|
5
6
|
function getMaxTextureSize(renderer) {
|
|
6
7
|
const backend = renderer.backend;
|
|
@@ -10,17 +11,30 @@ function getMaxTextureSize(renderer) {
|
|
|
10
11
|
if (gl) return gl.getParameter(gl.MAX_TEXTURE_SIZE);
|
|
11
12
|
throw new Error("three-vat: renderer has no initialized backend \u2014 call `await renderer.init()` first");
|
|
12
13
|
}
|
|
13
|
-
var
|
|
14
|
-
function
|
|
15
|
-
const
|
|
14
|
+
var packTexel = (texture, field, instance) => textureLoad(texture, ivec2(int(field), instance));
|
|
15
|
+
function texturePlayback(texture, instance) {
|
|
16
|
+
const clip = packTexel(texture, PACK_TEXELS.clip, instance);
|
|
17
|
+
const playback = packTexel(texture, PACK_TEXELS.playback, instance);
|
|
18
|
+
const fade = packTexel(texture, PACK_TEXELS.fade, instance);
|
|
19
|
+
const frames = clip.y;
|
|
16
20
|
return {
|
|
17
|
-
startFrame: int(
|
|
21
|
+
startFrame: int(clip.x),
|
|
18
22
|
frames,
|
|
19
|
-
duration: frames.div(
|
|
20
|
-
|
|
21
|
-
speed:
|
|
23
|
+
duration: frames.div(clip.z),
|
|
24
|
+
startTime: playback.x,
|
|
25
|
+
speed: clip.w,
|
|
26
|
+
loopMode: playback.y,
|
|
27
|
+
repetitions: playback.z,
|
|
28
|
+
endMode: playback.w,
|
|
29
|
+
fade: {
|
|
30
|
+
startFrame: fade.x,
|
|
31
|
+
frames: fade.y,
|
|
32
|
+
phase: fade.z,
|
|
33
|
+
duration: fade.w
|
|
34
|
+
}
|
|
22
35
|
};
|
|
23
36
|
}
|
|
37
|
+
var instanceIdOf = (carrier) => isBatchedCarrier(carrier) ? int(batchIndirectIndex) : int(instanceIndex);
|
|
24
38
|
function clipAt(vat, clipIndex) {
|
|
25
39
|
const clip = vat.clips[clipIndex];
|
|
26
40
|
if (!clip) {
|
|
@@ -28,60 +42,102 @@ function clipAt(vat, clipIndex) {
|
|
|
28
42
|
}
|
|
29
43
|
return clip;
|
|
30
44
|
}
|
|
31
|
-
function hashedPlayback(clip, desync) {
|
|
45
|
+
function hashedPlayback(clip, desync, instance) {
|
|
32
46
|
return {
|
|
33
47
|
startFrame: int(clip.startFrame),
|
|
34
48
|
frames: float(clip.frames),
|
|
35
49
|
duration: float(clip.frames / clip.fps),
|
|
36
|
-
|
|
37
|
-
|
|
50
|
+
// Negated, because desync is now a start time in the *past*: an instance
|
|
51
|
+
// that began `desync` seconds ago is that far into its clip already.
|
|
52
|
+
startTime: hash(instance).mul(-desync),
|
|
53
|
+
// Everything but the phase comes from the clip's own baked defaults, so a
|
|
54
|
+
// clip baked "once, clamped, at 2x" plays that way here too. A second set
|
|
55
|
+
// of defaults living in this path would be a crowd that animates
|
|
56
|
+
// differently depending on whether anyone wrote the attributes.
|
|
57
|
+
speed: float(clip.speed),
|
|
58
|
+
loopMode: float(clip.loopMode),
|
|
59
|
+
repetitions: float(clip.repetitions),
|
|
60
|
+
endMode: float(clip.endMode),
|
|
61
|
+
// Nothing to fade out of: this path is the zero-config default, where an
|
|
62
|
+
// instance has never been written and so has no animation it left behind.
|
|
63
|
+
fade: { startFrame: float(0), frames: float(0), phase: float(0), duration: float(0) }
|
|
38
64
|
};
|
|
39
65
|
}
|
|
40
|
-
function checkPlaybackAttributes(geometry) {
|
|
41
|
-
const names = Object.values(PLAYBACK_ATTRIBUTES);
|
|
42
|
-
const missing = names.filter((name) => geometry.getAttribute(name) === void 0);
|
|
43
|
-
if (missing.length === 0) return true;
|
|
44
|
-
if (missing.length === names.length) return false;
|
|
45
|
-
throw new Error(
|
|
46
|
-
`three-vat: geometry carries only part of the instance-playback contract (missing ${missing.join(", ")}) \u2014 write all of it with \`addVATInstanceAttributes\` from \`three-vat\`, or pass no geometry for the hashed default`
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
66
|
function vatNodes(vat, options = {}) {
|
|
50
|
-
const { time = uniform(0),
|
|
67
|
+
const { time = uniform(0), carrier } = options;
|
|
51
68
|
const { position, normal } = vatDecode(vat, options);
|
|
52
69
|
const decode = Fn(() => {
|
|
53
|
-
positionLocal.assign((
|
|
54
|
-
normalLocal.assign(normal);
|
|
55
|
-
if (
|
|
70
|
+
positionLocal.assign((carrier ? positionGeometry : positionLocal).add(position));
|
|
71
|
+
if (normal) normalLocal.assign(normal);
|
|
72
|
+
if (carrier) {
|
|
73
|
+
if (isBatchedCarrier(carrier)) batch(carrier);
|
|
74
|
+
else instancedMesh(carrier);
|
|
75
|
+
}
|
|
56
76
|
return positionLocal;
|
|
57
77
|
}, "vec3");
|
|
58
78
|
return { positionNode: decode(), time };
|
|
59
79
|
}
|
|
60
80
|
function vatDecode(vat, options = {}) {
|
|
61
|
-
const { time = uniform(0),
|
|
62
|
-
|
|
81
|
+
const { time = uniform(0), playback: playbackTexture, carrier, clipIndex = 0, desync = 0 } = options;
|
|
82
|
+
if (carrier) assertVATCarrier(carrier, vat);
|
|
83
|
+
const instance = instanceIdOf(carrier);
|
|
84
|
+
const playback = playbackTexture ? texturePlayback(playbackTexture.texture, instance) : hashedPlayback(clipAt(vat, clipIndex), desync, instance);
|
|
63
85
|
const vertexRow = int(vertexIndex);
|
|
86
|
+
const frames = playback.frames;
|
|
87
|
+
const last = frames.sub(1);
|
|
88
|
+
const elapsed = time.sub(playback.startTime);
|
|
89
|
+
const local = elapsed.mul(playback.speed);
|
|
90
|
+
const loops = local.div(playback.duration);
|
|
91
|
+
const started = local.greaterThanEqual(0);
|
|
92
|
+
const finished = started.and(playback.repetitions.notEqual(INFINITE_REPETITIONS)).and(loops.greaterThanEqual(playback.repetitions));
|
|
93
|
+
const isPingPong = playback.loopMode.equal(LoopMode.PingPong);
|
|
94
|
+
const bounce = loops.mod(2);
|
|
95
|
+
const pingPongPhase = bounce.lessThan(1).select(bounce, float(2).sub(bounce));
|
|
96
|
+
const endPhase = playback.endMode.equal(EndMode.Clamp).select(float(1), float(0));
|
|
97
|
+
const phase = started.select(
|
|
98
|
+
finished.select(endPhase, isPingPong.select(pingPongPhase, loops.fract())),
|
|
99
|
+
float(0)
|
|
100
|
+
);
|
|
101
|
+
const wraps = started.select(
|
|
102
|
+
finished.select(bool(false), isPingPong.select(bool(false), bool(true))),
|
|
103
|
+
bool(false)
|
|
104
|
+
);
|
|
105
|
+
const f = phase.mul(wraps.select(frames, last));
|
|
106
|
+
const f0 = f.floor().min(last);
|
|
107
|
+
const f1 = wraps.select(f0.add(1).mod(frames), f0.add(1).min(last));
|
|
108
|
+
const frameMix = f.sub(f0);
|
|
109
|
+
const bandRow = (offset) => int(offset).add(playback.startFrame);
|
|
110
|
+
const row0 = bandRow(f0);
|
|
111
|
+
const row1 = bandRow(f1);
|
|
112
|
+
const fade = playback.fade;
|
|
113
|
+
const fadeWeight = fade.duration.greaterThan(0).select(
|
|
114
|
+
float(1).sub(elapsed.div(fade.duration).clamp(0, 1)),
|
|
115
|
+
float(0)
|
|
116
|
+
);
|
|
117
|
+
const frozenRow = int(fade.phase.mul(fade.frames).floor().min(fade.frames.sub(1)).max(0)).add(
|
|
118
|
+
int(fade.startFrame)
|
|
119
|
+
);
|
|
64
120
|
const sample = (tex) => {
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
const s1 = textureLoad(tex, ivec2(vertexRow, f1.add(playback.startFrame))).xyz;
|
|
70
|
-
return mix(s0, s1, t.fract());
|
|
121
|
+
const s0 = textureLoad(tex, ivec2(vertexRow, row0)).xyz;
|
|
122
|
+
const s1 = textureLoad(tex, ivec2(vertexRow, row1)).xyz;
|
|
123
|
+
const frozen = textureLoad(tex, ivec2(vertexRow, frozenRow)).xyz;
|
|
124
|
+
return mix(mix(s0, s1, frameMix), frozen, fadeWeight);
|
|
71
125
|
};
|
|
126
|
+
const normalTexture = vat.normalTexture;
|
|
72
127
|
return {
|
|
73
128
|
position: sample(vat.positionTexture),
|
|
74
|
-
normal: sample(
|
|
129
|
+
normal: normalTexture ? sample(normalTexture).normalize() : null
|
|
75
130
|
};
|
|
76
131
|
}
|
|
77
132
|
function createVATMesh(vat, instances, options = {}) {
|
|
78
133
|
const time = options.time ?? uniform(0);
|
|
79
|
-
const
|
|
134
|
+
const playback = createVATPlaybackTexture(instances);
|
|
135
|
+
for (const source of vat.materials) assertBakedNormal(vat, source);
|
|
80
136
|
const materials = vat.materials.map((source) => source.clone());
|
|
81
|
-
const mesh = new InstancedMesh(geometry, materials, instances.length);
|
|
82
|
-
const { positionNode } = vatNodes(vat, { time,
|
|
137
|
+
const mesh = new InstancedMesh(vat.geometry, materials, instances.length);
|
|
138
|
+
const { positionNode } = vatNodes(vat, { time, playback, carrier: mesh });
|
|
83
139
|
for (const material of materials) material.positionNode = positionNode;
|
|
84
|
-
return { mesh, time };
|
|
140
|
+
return { mesh, time, playback };
|
|
85
141
|
}
|
|
86
142
|
|
|
87
143
|
export { createVATMesh, getMaxTextureSize, vatDecode, vatNodes };
|
package/dist/webgl.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { IUniform, MeshDepthMaterial, WebGLRenderer, Material } from 'three';
|
|
2
|
-
import {
|
|
2
|
+
import { V as VAT, i as VATPlaybackTexture, a as VATCarrier, h as VATInstance, e as VATCrowd } from './carrier-BFCPmcQK.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* The real maximum texture dimension this GPU accepts, for
|
|
@@ -16,34 +16,36 @@ interface VATUniforms {
|
|
|
16
16
|
}
|
|
17
17
|
/** Create the shared time uniform. Update `uVatTime.value` once per frame. */
|
|
18
18
|
declare function createVATUniforms(time?: number): VATUniforms;
|
|
19
|
-
/**
|
|
20
|
-
* The instance-playback contract now lives in the core entry point, so both
|
|
21
|
-
* decode paths can read it (ADR-0009).
|
|
22
|
-
*
|
|
23
|
-
* @deprecated Renamed to `addVATInstanceAttributes` and moved to `three-vat`.
|
|
24
|
-
* Removed from `three-vat/webgl` in the next minor version — import it from
|
|
25
|
-
* `three-vat` instead.
|
|
26
|
-
*/
|
|
27
|
-
declare const addInstancedVATAttributes: typeof addVATInstanceAttributes;
|
|
28
|
-
/**
|
|
29
|
-
* @deprecated Moved to `three-vat`. Removed from `three-vat/webgl` in the next
|
|
30
|
-
* minor version — import `VATInstance` from `three-vat` instead.
|
|
31
|
-
*/
|
|
32
|
-
type VATInstance = VATInstance$1;
|
|
33
19
|
/**
|
|
34
20
|
* Patch any built-in material so its vertex stage samples the VAT instead of
|
|
35
21
|
* skinning. Works on the render material and on `MeshDepthMaterial` (needed for
|
|
36
22
|
* instanced shadows — see {@link createVATDepthMaterial}). Mutates and returns
|
|
37
23
|
* the material.
|
|
24
|
+
*
|
|
25
|
+
* `playback` is the crowd's playback texture — the decode reads the pack out of
|
|
26
|
+
* it by this instance's logical index, so a material patched for one crowd
|
|
27
|
+
* renders that crowd's playback and no other's.
|
|
28
|
+
*
|
|
29
|
+
* `carrier` is the mesh this material will draw on, and it is needed for one
|
|
30
|
+
* reason: how the shader names that logical index. Omit it for an
|
|
31
|
+
* `InstancedMesh`, where the index is `gl_InstanceID`. Pass a `BatchedMesh` and
|
|
32
|
+
* the decode resolves the index through `getIndirectIndex( gl_DrawID )`
|
|
33
|
+
* instead, because that carrier culls and sorts per instance and its drawn slot
|
|
34
|
+
* is a permutation that changes every frame (ADR-0016). A batch a VAT cannot be
|
|
35
|
+
* decoded on is refused here rather than rendered wrong.
|
|
38
36
|
*/
|
|
39
|
-
declare function patchVATMaterial<T extends Material>(material: T, vat: VAT, uniforms: VATUniforms): T;
|
|
37
|
+
declare function patchVATMaterial<T extends Material>(material: T, vat: VAT, uniforms: VATUniforms, playback: VATPlaybackTexture, carrier?: VATCarrier): T;
|
|
40
38
|
/**
|
|
41
|
-
* Build the `customDepthMaterial`
|
|
39
|
+
* Build the `customDepthMaterial` a VAT crowd needs so it casts
|
|
42
40
|
* correctly-deformed shadows instead of bind-pose shadows. Assign the result to
|
|
43
41
|
* `mesh.customDepthMaterial` (and, for point lights, mirror with a patched
|
|
44
42
|
* `MeshDistanceMaterial`).
|
|
43
|
+
*
|
|
44
|
+
* `carrier` means what it means in {@link patchVATMaterial}: omit it for an
|
|
45
|
+
* `InstancedMesh`, pass the `BatchedMesh` for a batched crowd, so the shadow
|
|
46
|
+
* pass resolves the same instance index the render pass does.
|
|
45
47
|
*/
|
|
46
|
-
declare function createVATDepthMaterial(vat: VAT, uniforms: VATUniforms): MeshDepthMaterial;
|
|
48
|
+
declare function createVATDepthMaterial(vat: VAT, uniforms: VATUniforms, playback: VATPlaybackTexture, carrier?: VATCarrier): MeshDepthMaterial;
|
|
47
49
|
/** Options for {@link createVATMesh}. */
|
|
48
50
|
interface CreateVATMeshOptions {
|
|
49
51
|
/**
|
|
@@ -56,12 +58,12 @@ interface CreateVATMeshOptions {
|
|
|
56
58
|
}
|
|
57
59
|
/**
|
|
58
60
|
* Turn a baked VAT and a list of instances into a crowd ready to render: an
|
|
59
|
-
* `InstancedMesh`
|
|
60
|
-
* materials decode the VAT, and
|
|
61
|
-
* in the bind pose.
|
|
61
|
+
* `InstancedMesh` rendering the bake's geometry, a playback texture carrying
|
|
62
|
+
* the instance-playback contract, materials that decode the VAT, and shadows
|
|
63
|
+
* that are deformed rather than frozen in the bind pose.
|
|
62
64
|
*
|
|
63
65
|
* ```ts
|
|
64
|
-
* const { mesh, time } = createVATMesh(vat, instances)
|
|
66
|
+
* const { mesh, time, playback } = createVATMesh(vat, instances)
|
|
65
67
|
* mesh.castShadow = mesh.receiveShadow = true
|
|
66
68
|
* scene.add(mesh)
|
|
67
69
|
* // per frame:
|
|
@@ -78,11 +80,14 @@ interface CreateVATMeshOptions {
|
|
|
78
80
|
* - **`castShadow` / `receiveShadow`**, which are scene decisions. The depth and
|
|
79
81
|
* distance materials the shadow passes need are already attached either way.
|
|
80
82
|
*
|
|
81
|
-
* Everything here is the exported primitives —
|
|
83
|
+
* Everything here is the exported primitives — `createVATPlaybackTexture`,
|
|
82
84
|
* {@link patchVATMaterial}, {@link createVATDepthMaterial} — composed in the one
|
|
83
85
|
* order that is correct. Reach for them directly only when rendering onto
|
|
84
|
-
* something other than a plain `InstancedMesh
|
|
86
|
+
* something other than a plain `InstancedMesh` — a `BatchedMesh`, for three's
|
|
87
|
+
* own per-instance culling and sorting, being the other carrier this library
|
|
88
|
+
* supports (docs/usage.md); the returned `playback` is what
|
|
89
|
+
* {@link setVATInstance} writes into either way.
|
|
85
90
|
*/
|
|
86
|
-
declare function createVATMesh(vat: VAT, instances: VATInstance
|
|
91
|
+
declare function createVATMesh(vat: VAT, instances: VATInstance[], options?: CreateVATMeshOptions): VATCrowd;
|
|
87
92
|
|
|
88
|
-
export { type CreateVATMeshOptions, type
|
|
93
|
+
export { type CreateVATMeshOptions, type VATUniforms, createVATDepthMaterial, createVATMesh, createVATUniforms, getMaxTextureSize, patchVATMaterial };
|
package/dist/webgl.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { assertBakedNormal, assertVATCarrier, isBatchedCarrier } from './chunk-W2ZAFMPB.js';
|
|
2
|
+
import { PACK_TEXELS, EndMode, LoopMode, INFINITE_REPETITIONS, createVATPlaybackTexture } from './chunk-2PGZ44TP.js';
|
|
2
3
|
import { MeshDepthMaterial, RGBADepthPacking, InstancedMesh, MeshDistanceMaterial } from 'three';
|
|
3
4
|
|
|
4
5
|
function getMaxTextureSize(renderer) {
|
|
@@ -7,67 +8,168 @@ function getMaxTextureSize(renderer) {
|
|
|
7
8
|
function createVATUniforms(time = 0) {
|
|
8
9
|
return { uVatTime: { value: time } };
|
|
9
10
|
}
|
|
10
|
-
var
|
|
11
|
+
var glslFloat = (n) => n.toFixed(1);
|
|
11
12
|
var DECODE_PRELUDE = (
|
|
12
13
|
/* glsl */
|
|
13
14
|
`
|
|
14
15
|
uniform highp sampler2D uVatPosTex;
|
|
15
|
-
uniform highp sampler2D
|
|
16
|
+
uniform highp sampler2D uVatPlaybackTex;
|
|
16
17
|
uniform float uVatTime;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
18
|
+
vec3 vatSample( const in sampler2D tex, const in int vatInstance ) {
|
|
19
|
+
// The pack, fetched by this instance's *logical* index rather than read
|
|
20
|
+
// off an attribute indexed by the drawn slot (ADR-0016). Three texels of
|
|
21
|
+
// one row, in the order src/instance-playback.ts lays them out; the
|
|
22
|
+
// arithmetic below is untouched by where they came from, because the pack
|
|
23
|
+
// was already three vec4s.
|
|
24
|
+
//
|
|
25
|
+
// The index arrives as a parameter rather than being read here, because
|
|
26
|
+
// where it comes from is the carrier's business and not the decode's:
|
|
27
|
+
// gl_InstanceID on an InstancedMesh, getIndirectIndex( gl_DrawID ) on a
|
|
28
|
+
// BatchedMesh \u2014 see INSTANCE_ID in src/webgl.ts. It also has to be a
|
|
29
|
+
// parameter: getIndirectIndex is declared by batching_pars_vertex, which
|
|
30
|
+
// three expands *after* this prelude, so naming it up here would not
|
|
31
|
+
// compile.
|
|
32
|
+
//
|
|
33
|
+
// Fetched inside the function, so each injection point stays
|
|
34
|
+
// self-contained (ADR-0006) \u2014 which costs a second set of fetches in the
|
|
35
|
+
// normal decode. Every vertex of an instance reads the same three texels,
|
|
36
|
+
// so the texture cache absorbs them; the 5% demo bench is what says so.
|
|
37
|
+
vec4 vatClip = texelFetch( uVatPlaybackTex, ivec2( ${PACK_TEXELS.clip}, vatInstance ), 0 );
|
|
38
|
+
vec4 vatPlayback = texelFetch( uVatPlaybackTex, ivec2( ${PACK_TEXELS.playback}, vatInstance ), 0 );
|
|
39
|
+
vec4 vatFade = texelFetch( uVatPlaybackTex, ivec2( ${PACK_TEXELS.fade}, vatInstance ), 0 );
|
|
40
|
+
|
|
41
|
+
float frames = vatClip.y;
|
|
42
|
+
float last = frames - 1.0;
|
|
43
|
+
float duration = frames / vatClip.z;
|
|
44
|
+
// Local time: how far into its own animation this instance is. A start time
|
|
45
|
+
// in the past is what desyncs a crowd; a start time in the future has not
|
|
46
|
+
// begun, which is not the same thing as having finished.
|
|
47
|
+
float local = ( uVatTime - vatPlayback.x ) * vatClip.w;
|
|
48
|
+
float loops = local / duration;
|
|
49
|
+
float repetitions = vatPlayback.z;
|
|
50
|
+
|
|
51
|
+
bool started = local >= 0.0;
|
|
52
|
+
bool finished = started && repetitions != ${glslFloat(INFINITE_REPETITIONS)} && loops >= repetitions;
|
|
53
|
+
|
|
54
|
+
float phase;
|
|
55
|
+
bool wraps;
|
|
56
|
+
if ( !started ) {
|
|
57
|
+
phase = 0.0;
|
|
58
|
+
wraps = false;
|
|
59
|
+
} else if ( finished ) {
|
|
60
|
+
// Held at an end pose, and in neither case sampling past it.
|
|
61
|
+
phase = vatPlayback.w == ${glslFloat(EndMode.Clamp)} ? 1.0 : 0.0;
|
|
62
|
+
wraps = false;
|
|
63
|
+
} else if ( vatPlayback.y == ${glslFloat(LoopMode.PingPong)} ) {
|
|
64
|
+
float m = mod( loops, 2.0 );
|
|
65
|
+
phase = m < 1.0 ? m : 2.0 - m;
|
|
66
|
+
wraps = false; // a ping-pong bounces; it does not wrap
|
|
67
|
+
} else {
|
|
68
|
+
phase = fract( loops );
|
|
69
|
+
wraps = true; // and here the interpolation crossing back is correct
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
float f = phase * ( wraps ? frames : last );
|
|
73
|
+
float f0 = min( floor( f ), last );
|
|
74
|
+
float f1 = wraps ? mod( f0 + 1.0, frames ) : min( f0 + 1.0, last );
|
|
75
|
+
vec3 s0 = texelFetch( tex, ivec2( gl_VertexID, int( vatClip.x + f0 ) ), 0 ).xyz;
|
|
76
|
+
vec3 s1 = texelFetch( tex, ivec2( gl_VertexID, int( vatClip.x + f1 ) ), 0 ).xyz;
|
|
77
|
+
vec3 sampled = mix( s0, s1, f - f0 );
|
|
78
|
+
|
|
79
|
+
// The pose-freeze fade, transcribed from the same resolver: one frozen row
|
|
80
|
+
// of the clip this instance was playing when it changed, blended away over
|
|
81
|
+
// vatFade.w. Wall clock, not clip time \u2014 the incoming clip's speed does
|
|
82
|
+
// not stretch a fade. A duration of zero is what "not fading" is, and the
|
|
83
|
+
// pack never writes one without a band to go with it.
|
|
84
|
+
if ( vatFade.w > 0.0 ) {
|
|
85
|
+
float weight = 1.0 - clamp( ( uVatTime - vatPlayback.x ) / vatFade.w, 0.0, 1.0 );
|
|
86
|
+
float fromRow = max( min( floor( vatFade.z * vatFade.y ), vatFade.y - 1.0 ), 0.0 );
|
|
87
|
+
vec3 frozen = texelFetch( tex, ivec2( gl_VertexID, int( vatFade.x + fromRow ) ), 0 ).xyz;
|
|
88
|
+
sampled = mix( sampled, frozen, weight );
|
|
89
|
+
}
|
|
90
|
+
return sampled;
|
|
30
91
|
}
|
|
31
92
|
`
|
|
32
93
|
);
|
|
33
|
-
var
|
|
94
|
+
var NORMAL_PRELUDE = (
|
|
34
95
|
/* glsl */
|
|
35
96
|
`
|
|
36
|
-
|
|
97
|
+
uniform highp sampler2D uVatNrmTex;
|
|
37
98
|
`
|
|
38
99
|
);
|
|
39
|
-
var
|
|
100
|
+
var INSTANCE_ID = {
|
|
101
|
+
instance: "gl_InstanceID",
|
|
102
|
+
batch: "int( getIndirectIndex( gl_DrawID ) )"
|
|
103
|
+
};
|
|
104
|
+
var decodePosition = (id) => (
|
|
40
105
|
/* glsl */
|
|
41
106
|
`
|
|
42
|
-
vec3
|
|
107
|
+
vec3 transformed = position + vatSample( uVatPosTex, ${INSTANCE_ID[id]} );
|
|
108
|
+
`
|
|
109
|
+
);
|
|
110
|
+
var decodeNormal = (id) => (
|
|
111
|
+
/* glsl */
|
|
112
|
+
`
|
|
113
|
+
vec3 objectNormal = normalize( vatSample( uVatNrmTex, ${INSTANCE_ID[id]} ) );
|
|
43
114
|
#ifdef USE_TANGENT
|
|
44
115
|
vec3 objectTangent = vec3( tangent.xyz );
|
|
45
116
|
#endif
|
|
46
117
|
`
|
|
47
118
|
);
|
|
48
|
-
function patchVATMaterial(material, vat, uniforms) {
|
|
119
|
+
function patchVATMaterial(material, vat, uniforms, playback, carrier) {
|
|
120
|
+
assertBakedNormal(vat, material);
|
|
121
|
+
if (carrier) assertVATCarrier(carrier, vat);
|
|
122
|
+
const normalTexture = vat.normalTexture;
|
|
123
|
+
const id = isBatchedCarrier(carrier) ? "batch" : "instance";
|
|
49
124
|
material.onBeforeCompile = (shader) => {
|
|
50
125
|
shader.uniforms.uVatPosTex = { value: vat.positionTexture };
|
|
51
|
-
shader.uniforms.
|
|
126
|
+
shader.uniforms.uVatPlaybackTex = { value: playback.texture };
|
|
52
127
|
shader.uniforms.uVatTime = uniforms.uVatTime;
|
|
53
|
-
|
|
128
|
+
let vertexShader = shader.vertexShader.replace("#include <begin_vertex>", decodePosition(id));
|
|
129
|
+
if (normalTexture) {
|
|
130
|
+
shader.uniforms.uVatNrmTex = { value: normalTexture };
|
|
131
|
+
vertexShader = vertexShader.replace("#include <beginnormal_vertex>", decodeNormal(id));
|
|
132
|
+
}
|
|
133
|
+
shader.vertexShader = (normalTexture ? NORMAL_PRELUDE : "") + DECODE_PRELUDE + vertexShader;
|
|
54
134
|
};
|
|
55
|
-
|
|
135
|
+
const key = `three-vat:${id}${normalTexture ? "" : ":no-normal"}`;
|
|
136
|
+
material.customProgramCacheKey = () => key;
|
|
137
|
+
guardCarrierMismatch(material, id);
|
|
56
138
|
return material;
|
|
57
139
|
}
|
|
58
|
-
function
|
|
140
|
+
function guardCarrierMismatch(material, id) {
|
|
141
|
+
const previous = material.onBeforeRender.bind(material);
|
|
142
|
+
let checked = false;
|
|
143
|
+
material.onBeforeRender = function(renderer, scene, camera, geometry, object, group) {
|
|
144
|
+
if (!checked) {
|
|
145
|
+
checked = true;
|
|
146
|
+
const drawnOn = isBatchedCarrier(object) ? "batch" : "instance";
|
|
147
|
+
if (drawnOn !== id) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`three-vat: this material was patched for ${CARRIER_NAME[id]} and is being drawn on ${CARRIER_NAME[drawnOn]}. Pass the carrier as \`patchVATMaterial\`'s fifth argument \u2014 the two spell the instance index differently, and a batch drawn with the instanced spelling plays instance 0\u2019s clip on every instance.`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
previous(renderer, scene, camera, geometry, object, group);
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
var CARRIER_NAME = {
|
|
157
|
+
instance: "an InstancedMesh",
|
|
158
|
+
batch: "a BatchedMesh"
|
|
159
|
+
};
|
|
160
|
+
function createVATDepthMaterial(vat, uniforms, playback, carrier) {
|
|
59
161
|
const depth = new MeshDepthMaterial({ depthPacking: RGBADepthPacking });
|
|
60
|
-
patchVATMaterial(depth, vat, uniforms);
|
|
162
|
+
patchVATMaterial(depth, vat, uniforms, playback, carrier);
|
|
61
163
|
return depth;
|
|
62
164
|
}
|
|
63
165
|
function createVATMesh(vat, instances, options = {}) {
|
|
64
166
|
const uniforms = options.time ? { uVatTime: options.time } : createVATUniforms();
|
|
65
|
-
const
|
|
66
|
-
const materials = vat.materials.map((source) => patchVATMaterial(source.clone(), vat, uniforms));
|
|
67
|
-
const mesh = new InstancedMesh(geometry, materials, instances.length);
|
|
68
|
-
mesh.customDepthMaterial = createVATDepthMaterial(vat, uniforms);
|
|
69
|
-
mesh.customDistanceMaterial = patchVATMaterial(new MeshDistanceMaterial(), vat, uniforms);
|
|
70
|
-
return { mesh, time: uniforms.uVatTime };
|
|
167
|
+
const playback = createVATPlaybackTexture(instances);
|
|
168
|
+
const materials = vat.materials.map((source) => patchVATMaterial(source.clone(), vat, uniforms, playback));
|
|
169
|
+
const mesh = new InstancedMesh(vat.geometry, materials, instances.length);
|
|
170
|
+
mesh.customDepthMaterial = createVATDepthMaterial(vat, uniforms, playback);
|
|
171
|
+
mesh.customDistanceMaterial = patchVATMaterial(new MeshDistanceMaterial(), vat, uniforms, playback);
|
|
172
|
+
return { mesh, time: uniforms.uVatTime, playback };
|
|
71
173
|
}
|
|
72
174
|
|
|
73
|
-
export {
|
|
175
|
+
export { createVATDepthMaterial, createVATMesh, createVATUniforms, getMaxTextureSize, patchVATMaterial };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "three-vat",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Bake glTF animation clips into vertex animation textures for zero-CPU instanced crowds in three.js.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"publishConfig": {
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
|
+
"packageManager": "pnpm@10.19.0",
|
|
19
20
|
"sideEffects": false,
|
|
20
21
|
"files": [
|
|
21
22
|
"dist"
|
|
@@ -34,6 +35,15 @@
|
|
|
34
35
|
"import": "./dist/tsl.js"
|
|
35
36
|
}
|
|
36
37
|
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"dev": "pnpm --filter three-vat-example dev",
|
|
40
|
+
"test": "vitest run && pnpm --filter three-vat-example test",
|
|
41
|
+
"build": "tsup",
|
|
42
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p release/tsconfig.json && pnpm --filter three-vat-example typecheck",
|
|
43
|
+
"build:watch": "tsup --watch",
|
|
44
|
+
"test:watch": "vitest",
|
|
45
|
+
"prepublishOnly": "pnpm typecheck && pnpm test && pnpm build"
|
|
46
|
+
},
|
|
37
47
|
"keywords": [
|
|
38
48
|
"three",
|
|
39
49
|
"threejs",
|
|
@@ -48,31 +58,24 @@
|
|
|
48
58
|
"tsl"
|
|
49
59
|
],
|
|
50
60
|
"peerDependencies": {
|
|
51
|
-
"three": ">=0.
|
|
61
|
+
"three": ">=0.186.0"
|
|
52
62
|
},
|
|
53
63
|
"devDependencies": {
|
|
54
|
-
"@types/three": "^0.
|
|
55
|
-
"
|
|
64
|
+
"@types/three": "^0.186.0",
|
|
65
|
+
"gifenc": "1.0.3",
|
|
66
|
+
"playwright-core": "1.63.0",
|
|
67
|
+
"pngjs": "7.0.0",
|
|
68
|
+
"three": "^0.186.0",
|
|
56
69
|
"tsup": "^8.3.0",
|
|
57
70
|
"typescript": "^5.6.0",
|
|
71
|
+
"vite": "^7.1.0",
|
|
58
72
|
"vitest": "^2.1.0"
|
|
59
73
|
},
|
|
60
74
|
"engines": {
|
|
61
|
-
"node": ">=
|
|
75
|
+
"node": ">=20"
|
|
62
76
|
},
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"fetch:test-assets": "node scripts/fetch-test-assets.mjs",
|
|
69
|
-
"typecheck": "tsc --noEmit",
|
|
70
|
-
"release": "pnpm publish ${NPM_OTP:+--otp=\"$NPM_OTP\"} && pnpm verify:published",
|
|
71
|
-
"verify:published": "node scripts/verify-published.mjs",
|
|
72
|
-
"example": "pnpm --filter three-vat-example dev",
|
|
73
|
-
"build:examples": "pnpm --filter three-vat-example build",
|
|
74
|
-
"test:examples": "pnpm --filter three-vat-example test",
|
|
75
|
-
"parity": "pnpm --filter three-vat-example parity",
|
|
76
|
-
"typecheck:examples": "pnpm --filter three-vat-example typecheck"
|
|
77
|
-
}
|
|
78
|
-
}
|
|
77
|
+
"workspaces": [
|
|
78
|
+
".",
|
|
79
|
+
"examples"
|
|
80
|
+
]
|
|
81
|
+
}
|
package/dist/chunk-SXTVASKG.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { InstancedBufferAttribute } from 'three';
|
|
2
|
-
|
|
3
|
-
// src/instance-playback.ts
|
|
4
|
-
var PLAYBACK_ATTRIBUTES = {
|
|
5
|
-
clipStart: "aClipStart",
|
|
6
|
-
clipFrames: "aClipFrames",
|
|
7
|
-
clipFps: "aClipFps",
|
|
8
|
-
timeOffset: "aTimeOffset",
|
|
9
|
-
speed: "aSpeed"
|
|
10
|
-
};
|
|
11
|
-
function addVATInstanceAttributes(geometry, instances) {
|
|
12
|
-
geometry.morphAttributes = {};
|
|
13
|
-
geometry.morphTargetsRelative = false;
|
|
14
|
-
const n = instances.length;
|
|
15
|
-
const clipStart = new Float32Array(n);
|
|
16
|
-
const clipFrames = new Float32Array(n);
|
|
17
|
-
const clipFps = new Float32Array(n);
|
|
18
|
-
const timeOffset = new Float32Array(n);
|
|
19
|
-
const speed = new Float32Array(n);
|
|
20
|
-
for (let i = 0; i < n; i++) {
|
|
21
|
-
const inst = instances[i];
|
|
22
|
-
clipStart[i] = inst.clip.startFrame;
|
|
23
|
-
clipFrames[i] = inst.clip.frames;
|
|
24
|
-
clipFps[i] = inst.clip.fps;
|
|
25
|
-
timeOffset[i] = inst.timeOffset;
|
|
26
|
-
speed[i] = inst.speed;
|
|
27
|
-
}
|
|
28
|
-
geometry.setAttribute(PLAYBACK_ATTRIBUTES.clipStart, new InstancedBufferAttribute(clipStart, 1));
|
|
29
|
-
geometry.setAttribute(PLAYBACK_ATTRIBUTES.clipFrames, new InstancedBufferAttribute(clipFrames, 1));
|
|
30
|
-
geometry.setAttribute(PLAYBACK_ATTRIBUTES.clipFps, new InstancedBufferAttribute(clipFps, 1));
|
|
31
|
-
geometry.setAttribute(PLAYBACK_ATTRIBUTES.timeOffset, new InstancedBufferAttribute(timeOffset, 1));
|
|
32
|
-
geometry.setAttribute(PLAYBACK_ATTRIBUTES.speed, new InstancedBufferAttribute(speed, 1));
|
|
33
|
-
}
|
|
34
|
-
function createCrowdGeometry(vat, instances) {
|
|
35
|
-
const geometry = vat.geometry.clone();
|
|
36
|
-
addVATInstanceAttributes(geometry, instances);
|
|
37
|
-
return geometry;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export { PLAYBACK_ATTRIBUTES, addVATInstanceAttributes, createCrowdGeometry };
|