ugly-game 0.7.9 → 0.7.10
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/dist/assets/gpuProps.d.ts +38 -1
- package/dist/assets/gpuProps.js +193 -44
- package/package.json +1 -1
|
@@ -26,9 +26,37 @@ export interface PropMesh {
|
|
|
26
26
|
colors: Float32Array;
|
|
27
27
|
indices: Uint32Array;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Per-instance deformation. Every field is neutral at its default, so an instance that supplies none
|
|
31
|
+
* renders byte-identically to one drawn before this existed.
|
|
32
|
+
*
|
|
33
|
+
* This is the cheap half of "variety": one generated mesh becomes an unlimited stand of visibly
|
|
34
|
+
* different plants without a second asset, a second draw call, or a provider call. The expensive
|
|
35
|
+
* half — new silhouettes — still needs a new mesh.
|
|
36
|
+
*/
|
|
37
|
+
export interface PropDeform {
|
|
38
|
+
/** Vertical scale. 0.6 squat, 1.6 lanky. */
|
|
39
|
+
height?: number;
|
|
40
|
+
/** Horizontal scale, applied before taper. */
|
|
41
|
+
girth?: number;
|
|
42
|
+
/** >0 narrows toward the top, <0 flares. */
|
|
43
|
+
taper?: number;
|
|
44
|
+
/** Lateral tip displacement in units of the plant's own height; grows as u². */
|
|
45
|
+
bend?: number;
|
|
46
|
+
bendDir?: number;
|
|
47
|
+
/** Radians of twist from base to tip. */
|
|
48
|
+
twist?: number;
|
|
49
|
+
/** Downward pull on geometry that reaches out — a trunk is unaffected, a frond sags. */
|
|
50
|
+
droop?: number;
|
|
51
|
+
yaw?: number;
|
|
52
|
+
/** Multiplied into vertex colour; gives a within-species value spread. */
|
|
53
|
+
valueJitter?: number;
|
|
54
|
+
}
|
|
29
55
|
export interface PropInstance {
|
|
30
56
|
model: number;
|
|
31
57
|
mat: Float32Array;
|
|
58
|
+
/** Optional per-instance deform. Omitted ⇒ the mesh is drawn exactly as authored. */
|
|
59
|
+
deform?: PropDeform;
|
|
32
60
|
}
|
|
33
61
|
export declare class GpuPropsRenderer {
|
|
34
62
|
private device;
|
|
@@ -38,6 +66,8 @@ export declare class GpuPropsRenderer {
|
|
|
38
66
|
private lightBuf;
|
|
39
67
|
private models;
|
|
40
68
|
private instBufs;
|
|
69
|
+
/** Allocated capacity in instances, so a per-frame LOD re-bucket reuses the buffer. */
|
|
70
|
+
private caps;
|
|
41
71
|
private bgs;
|
|
42
72
|
private occBGs;
|
|
43
73
|
private counts;
|
|
@@ -59,7 +89,14 @@ export declare class GpuPropsRenderer {
|
|
|
59
89
|
tick(dt: number): void;
|
|
60
90
|
constructor(device: GPUDevice, format: GPUTextureFormat);
|
|
61
91
|
setModels(meshes: PropMesh[]): void;
|
|
62
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* Group instances by model and upload the per-model instance buffers.
|
|
94
|
+
*
|
|
95
|
+
* Safe to call every frame: buffers are REUSED whenever the new count fits the allocated capacity,
|
|
96
|
+
* and capacity grows with headroom. That matters because distance-bucketed LOD means an instance
|
|
97
|
+
* changes which model it belongs to as the camera moves, so the grouping is not a one-time setup —
|
|
98
|
+
* re-allocating a megabyte of storage buffers per frame would cost more than the LOD saves.
|
|
99
|
+
*/
|
|
63
100
|
setInstances(instances: PropInstance[]): void;
|
|
64
101
|
/** Occluder pass: draw all instanced props' depth into the shared shadow map from the sun's ortho view. */
|
|
65
102
|
shadowPass(pass: GPURenderPassEncoder, lightVP: Float32Array): void;
|
package/dist/assets/gpuProps.js
CHANGED
|
@@ -9,7 +9,15 @@ const SHADER = /* wgsl */ `
|
|
|
9
9
|
// grade = (hueShift, saturation, valueLift, unused)
|
|
10
10
|
struct U { mvp: mat4x4<f32>, lightVP: mat4x4<f32>, sun: vec4<f32>, sunCol: vec4<f32>, amb: vec4<f32>, cam: vec4<f32>, fog: vec4<f32>, up: vec4<f32>, wind: vec4<f32>, surf: vec4<f32>, grade: vec4<f32> };
|
|
11
11
|
@group(0) @binding(0) var<uniform> u: U;
|
|
12
|
-
|
|
12
|
+
// One instance. 'm' is the placement; d0/d1/d2 are the per-instance DEFORM, which is what lets
|
|
13
|
+
// thousands of visibly different plants share a single mesh and a single draw call. Baking each
|
|
14
|
+
// variant on the CPU instead costs one model, one vertex buffer and one draw call per plant —
|
|
15
|
+
// measured, that is the difference between a forest and a slideshow.
|
|
16
|
+
// d0 = (heightScale, girth, taper, 1/sourceHeight)
|
|
17
|
+
// d1 = (bend, bendDir, twist, droop)
|
|
18
|
+
// d2 = (yaw, valueJitter, 1/halfWidth, unused)
|
|
19
|
+
struct Inst { m: mat4x4<f32>, d0: vec4<f32>, d1: vec4<f32>, d2: vec4<f32> };
|
|
20
|
+
@group(0) @binding(1) var<storage,read> models: array<Inst>;
|
|
13
21
|
@group(1) @binding(0) var shadowMap: texture_depth_2d; // the SAME sun shadow map the terrain cubes sample
|
|
14
22
|
@group(1) @binding(1) var shadowSamp: sampler_comparison;
|
|
15
23
|
struct VO { @builtin(position) pos: vec4<f32>, @location(0) world: vec3<f32>, @location(1) n: vec3<f32>, @location(2) c: vec3<f32>, @location(3) l: vec3<f32> };
|
|
@@ -28,13 +36,63 @@ fn sway(local: vec3<f32>, world: vec3<f32>, up: vec3<f32>, scale: f32) -> vec3<f
|
|
|
28
36
|
wd = normalize(wd - up * dot(wd, up)); // project into the local tangent plane
|
|
29
37
|
return world + wd * d;
|
|
30
38
|
}
|
|
39
|
+
// PER-INSTANCE DEFORM, in local space, before placement. Mirrors shared/lifeVariation.ts deform()
|
|
40
|
+
// exactly: the CPU version is the reference and the two must agree, or the same seed gives a
|
|
41
|
+
// different plant depending on which path drew it.
|
|
42
|
+
//
|
|
43
|
+
// Assumes the model's base sits at y = 0, which the cook guarantees (it normalises every plant to
|
|
44
|
+
// base-at-origin). u then runs 0 at the base to 1 at the tip, and everything is anchored: a deformed
|
|
45
|
+
// plant still meets the ground exactly where the original did, so no instance needs its contact
|
|
46
|
+
// re-solved.
|
|
47
|
+
fn deformLocal(p: vec3<f32>, d0: vec4<f32>, d1: vec4<f32>, d2: vec4<f32>) -> vec3<f32> {
|
|
48
|
+
let H = 1.0 / max(d0.w, 1e-6);
|
|
49
|
+
let u = clamp(p.y * d0.w, 0.0, 1.0);
|
|
50
|
+
var x = p.x; var y = p.y; var z = p.z;
|
|
51
|
+
let rs = d0.y * (1.0 + d0.z * (1.0 - 2.0 * u)); // taper about the vertical axis
|
|
52
|
+
x = x * rs; z = z * rs;
|
|
53
|
+
let a = d1.z * u; // twist accumulates with height
|
|
54
|
+
let ca = cos(a); let sa = sin(a);
|
|
55
|
+
let tx = x * ca - z * sa; let tz = x * sa + z * ca;
|
|
56
|
+
x = tx; z = tz;
|
|
57
|
+
y = y * d0.x;
|
|
58
|
+
if (d1.w != 0.0) { // droop: weighted by radial reach
|
|
59
|
+
let r = sqrt(x * x + z * z) * d2.z;
|
|
60
|
+
y = y - d1.w * H * d0.x * r * r * u;
|
|
61
|
+
}
|
|
62
|
+
let dd = d1.x * H * d0.x * u * u; // bend as u²: leaves the ground vertical
|
|
63
|
+
x = x + cos(d1.y) * dd;
|
|
64
|
+
z = z + sin(d1.y) * dd;
|
|
65
|
+
let cy = cos(d2.x); let sy = sin(d2.x);
|
|
66
|
+
return vec3(x * cy - z * sy, y, x * sy + z * cy);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Normal under the same deform, approximated.
|
|
70
|
+
*
|
|
71
|
+
* The exact answer is the inverse-transpose of the deform's Jacobian, which for a bend and a droop
|
|
72
|
+
* is not cheap. Taper and non-uniform height are the terms that actually matter to shading, so the
|
|
73
|
+
* normal gets the inverse non-uniform scale plus the rigid rotations (twist + yaw) and nothing else.
|
|
74
|
+
* The CPU path recomputes normals exactly; the perf lab renders both so the difference can be looked
|
|
75
|
+
* at rather than assumed.
|
|
76
|
+
*/
|
|
77
|
+
fn deformNormal(n: vec3<f32>, py: f32, d0: vec4<f32>, d1: vec4<f32>, d2: vec4<f32>) -> vec3<f32> {
|
|
78
|
+
let u = clamp(py * d0.w, 0.0, 1.0);
|
|
79
|
+
let rs = max(d0.y * (1.0 + d0.z * (1.0 - 2.0 * u)), 1e-4);
|
|
80
|
+
var v = vec3(n.x / rs, n.y / max(d0.x, 1e-4), n.z / rs);
|
|
81
|
+
let a = d1.z * u + d2.x;
|
|
82
|
+
let ca = cos(a); let sa = sin(a);
|
|
83
|
+
return normalize(vec3(v.x * ca - v.z * sa, v.y, v.x * sa + v.z * ca));
|
|
84
|
+
}
|
|
31
85
|
@vertex fn vs(@location(0) p: vec3<f32>, @location(1) nrm: vec3<f32>, @location(2) col: vec3<f32>, @builtin(instance_index) ii: u32) -> VO {
|
|
32
|
-
let
|
|
33
|
-
|
|
86
|
+
let inst = models[ii];
|
|
87
|
+
let m = inst.m;
|
|
88
|
+
let lp = deformLocal(p, inst.d0, inst.d1, inst.d2);
|
|
89
|
+
let ln = deformNormal(nrm, p.y, inst.d0, inst.d1, inst.d2);
|
|
90
|
+
var world = (m * vec4(lp, 1.0)).xyz;
|
|
34
91
|
let upv = normalize((m * vec4(0.0, 1.0, 0.0, 0.0)).xyz);
|
|
35
92
|
let sc = length(m[0].xyz);
|
|
36
|
-
world = sway(
|
|
37
|
-
var o: VO; o.pos = u.mvp * vec4(world, 1.0); o.world = world; o.n = (m * vec4(
|
|
93
|
+
world = sway(lp, world, upv, sc);
|
|
94
|
+
var o: VO; o.pos = u.mvp * vec4(world, 1.0); o.world = world; o.n = (m * vec4(ln, 0.0)).xyz;
|
|
95
|
+
o.c = col * inst.d2.y; o.l = lp; return o;
|
|
38
96
|
}
|
|
39
97
|
// same fixed directional FILL as the terrain cubes (gpuShadow.fixedFill) so plants/rocks read the same 3D shape,
|
|
40
98
|
// day or night, under one shared light. up.w > 0.5 signals a valid local-up was supplied (else fall back to flat).
|
|
@@ -124,13 +182,40 @@ fn hsvShift(c: vec3<f32>, hue: f32, sat: f32, val: f32) -> vec3<f32> {
|
|
|
124
182
|
const OCCLUDER = /* wgsl */ `
|
|
125
183
|
struct OU { lightVP: mat4x4<f32>, wind: vec4<f32> };
|
|
126
184
|
@group(0) @binding(0) var<uniform> o: OU;
|
|
127
|
-
|
|
185
|
+
struct Inst { m: mat4x4<f32>, d0: vec4<f32>, d1: vec4<f32>, d2: vec4<f32> };
|
|
186
|
+
@group(0) @binding(1) var<storage,read> models: array<Inst>;
|
|
187
|
+
// The occluder must apply the identical DEFORM as well as the identical sway. A deformed plant whose
|
|
188
|
+
// shadow is cast from its undeformed silhouette is the same detachment bug as swaying leaves over a
|
|
189
|
+
// still shadow, and just as visible.
|
|
190
|
+
fn deformLocal(p: vec3<f32>, d0: vec4<f32>, d1: vec4<f32>, d2: vec4<f32>) -> vec3<f32> {
|
|
191
|
+
let H = 1.0 / max(d0.w, 1e-6);
|
|
192
|
+
let u = clamp(p.y * d0.w, 0.0, 1.0);
|
|
193
|
+
var x = p.x; var y = p.y; var z = p.z;
|
|
194
|
+
let rs = d0.y * (1.0 + d0.z * (1.0 - 2.0 * u));
|
|
195
|
+
x = x * rs; z = z * rs;
|
|
196
|
+
let a = d1.z * u;
|
|
197
|
+
let ca = cos(a); let sa = sin(a);
|
|
198
|
+
let tx = x * ca - z * sa; let tz = x * sa + z * ca;
|
|
199
|
+
x = tx; z = tz;
|
|
200
|
+
y = y * d0.x;
|
|
201
|
+
if (d1.w != 0.0) {
|
|
202
|
+
let r = sqrt(x * x + z * z) * d2.z;
|
|
203
|
+
y = y - d1.w * H * d0.x * r * r * u;
|
|
204
|
+
}
|
|
205
|
+
let dd = d1.x * H * d0.x * u * u;
|
|
206
|
+
x = x + cos(d1.y) * dd;
|
|
207
|
+
z = z + sin(d1.y) * dd;
|
|
208
|
+
let cy = cos(d2.x); let sy = sin(d2.x);
|
|
209
|
+
return vec3(x * cy - z * sy, y, x * sy + z * cy);
|
|
210
|
+
}
|
|
128
211
|
@vertex fn vs(@location(0) p: vec3<f32>, @builtin(instance_index) ii: u32) -> @builtin(position) vec4<f32> {
|
|
129
|
-
let
|
|
130
|
-
|
|
212
|
+
let inst = models[ii];
|
|
213
|
+
let m = inst.m;
|
|
214
|
+
let dp = deformLocal(p, inst.d0, inst.d1, inst.d2);
|
|
215
|
+
var world = (m * vec4(dp, 1.0)).xyz;
|
|
131
216
|
if (o.wind.z > 0.0001) {
|
|
132
217
|
let upv = normalize((m * vec4(0.0, 1.0, 0.0, 0.0)).xyz);
|
|
133
|
-
let h = max(
|
|
218
|
+
let h = max(dp.y, 0.0);
|
|
134
219
|
let stiff = pow(min(h * 0.22, 1.0), 1.6);
|
|
135
220
|
let ph = o.wind.w + dot(world.xz, vec2(0.13, 0.17));
|
|
136
221
|
let a = o.wind.z * stiff * length(m[0].xyz);
|
|
@@ -140,6 +225,8 @@ struct OU { lightVP: mat4x4<f32>, wind: vec4<f32> };
|
|
|
140
225
|
}
|
|
141
226
|
return o.lightVP * vec4(world, 1.0);
|
|
142
227
|
}`;
|
|
228
|
+
/** Floats per instance in the storage buffer: mat4 + d0 + d1 + d2. */
|
|
229
|
+
const INST_FLOATS = 28;
|
|
143
230
|
export class GpuPropsRenderer {
|
|
144
231
|
device;
|
|
145
232
|
pipe;
|
|
@@ -148,6 +235,8 @@ export class GpuPropsRenderer {
|
|
|
148
235
|
lightBuf;
|
|
149
236
|
models = [];
|
|
150
237
|
instBufs = [];
|
|
238
|
+
/** Allocated capacity in instances, so a per-frame LOD re-bucket reuses the buffer. */
|
|
239
|
+
caps = [];
|
|
151
240
|
bgs = [];
|
|
152
241
|
occBGs = [];
|
|
153
242
|
counts = [];
|
|
@@ -270,54 +359,76 @@ export class GpuPropsRenderer {
|
|
|
270
359
|
colB: mk(m.colors, U.VERTEX | U.COPY_DST),
|
|
271
360
|
idxB: mk(m.indices, U.INDEX | U.COPY_DST),
|
|
272
361
|
idxCount: m.indices.length,
|
|
362
|
+
extent: measureExtent(m.positions),
|
|
273
363
|
}));
|
|
364
|
+
this.caps = meshes.map(() => 0);
|
|
274
365
|
this.instBufs = meshes.map(() => null);
|
|
275
366
|
this.bgs = meshes.map(() => null);
|
|
276
367
|
this.occBGs = meshes.map(() => null);
|
|
277
368
|
this.counts = meshes.map(() => 0);
|
|
278
369
|
}
|
|
279
|
-
/**
|
|
370
|
+
/**
|
|
371
|
+
* Group instances by model and upload the per-model instance buffers.
|
|
372
|
+
*
|
|
373
|
+
* Safe to call every frame: buffers are REUSED whenever the new count fits the allocated capacity,
|
|
374
|
+
* and capacity grows with headroom. That matters because distance-bucketed LOD means an instance
|
|
375
|
+
* changes which model it belongs to as the camera moves, so the grouping is not a one-time setup —
|
|
376
|
+
* re-allocating a megabyte of storage buffers per frame would cost more than the LOD saves.
|
|
377
|
+
*/
|
|
280
378
|
setInstances(instances) {
|
|
281
379
|
const dev = this.device;
|
|
282
|
-
const
|
|
283
|
-
const
|
|
380
|
+
const n = this.models.length;
|
|
381
|
+
const counts = new Uint32Array(n);
|
|
382
|
+
for (const it of instances)
|
|
383
|
+
if (it.model >= 0 && it.model < n)
|
|
384
|
+
counts[it.model]++;
|
|
385
|
+
const cursors = new Uint32Array(n);
|
|
386
|
+
const arrays = new Array(n);
|
|
387
|
+
for (let m = 0; m < n; m++)
|
|
388
|
+
arrays[m] = counts[m] ? new Float32Array(counts[m] * INST_FLOATS) : null;
|
|
284
389
|
for (const it of instances) {
|
|
285
|
-
if (it.model < 0 || it.model >=
|
|
390
|
+
if (it.model < 0 || it.model >= n)
|
|
286
391
|
continue;
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
392
|
+
const arr = arrays[it.model];
|
|
393
|
+
const o = cursors[it.model]++ * INST_FLOATS;
|
|
394
|
+
arr.set(it.mat.subarray(0, 16), o);
|
|
395
|
+
writeDeform(arr, o + 16, it.deform, this.models[it.model].extent);
|
|
290
396
|
}
|
|
291
|
-
for (let m = 0; m <
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
this.instBufs[m]
|
|
297
|
-
|
|
298
|
-
|
|
397
|
+
for (let m = 0; m < n; m++) {
|
|
398
|
+
const arr = arrays[m];
|
|
399
|
+
this.counts[m] = counts[m];
|
|
400
|
+
if (!arr) {
|
|
401
|
+
// keep the buffer allocated — an empty LOD bucket this frame is usually full the next one
|
|
402
|
+
if (!this.instBufs[m]) {
|
|
403
|
+
this.bgs[m] = null;
|
|
404
|
+
this.occBGs[m] = null;
|
|
405
|
+
}
|
|
299
406
|
continue;
|
|
300
407
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
408
|
+
if (!this.instBufs[m] || counts[m] > this.caps[m]) {
|
|
409
|
+
this.instBufs[m]?.destroy();
|
|
410
|
+
const cap = Math.max(16, Math.ceil(counts[m] * 1.5));
|
|
411
|
+
this.instBufs[m] = dev.createBuffer({
|
|
412
|
+
size: cap * INST_FLOATS * 4,
|
|
413
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
414
|
+
});
|
|
415
|
+
this.caps[m] = cap;
|
|
416
|
+
this.bgs[m] = dev.createBindGroup({
|
|
417
|
+
layout: this.pipe.getBindGroupLayout(0),
|
|
418
|
+
entries: [
|
|
419
|
+
{ binding: 0, resource: { buffer: this.uBuf } },
|
|
420
|
+
{ binding: 1, resource: { buffer: this.instBufs[m] } },
|
|
421
|
+
],
|
|
422
|
+
});
|
|
423
|
+
this.occBGs[m] = dev.createBindGroup({
|
|
424
|
+
layout: this.occPipe.getBindGroupLayout(0),
|
|
425
|
+
entries: [
|
|
426
|
+
{ binding: 0, resource: { buffer: this.lightBuf } },
|
|
427
|
+
{ binding: 1, resource: { buffer: this.instBufs[m] } },
|
|
428
|
+
],
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
dev.queue.writeBuffer(this.instBufs[m], 0, arr);
|
|
321
432
|
}
|
|
322
433
|
}
|
|
323
434
|
/** Occluder pass: draw all instanced props' depth into the shared shadow map from the sun's ortho view. */
|
|
@@ -401,6 +512,44 @@ export class GpuPropsRenderer {
|
|
|
401
512
|
rp.end();
|
|
402
513
|
}
|
|
403
514
|
}
|
|
515
|
+
/** Source height and half-width of a model, reciprocated so the shader multiplies rather than divides. */
|
|
516
|
+
function measureExtent(pos) {
|
|
517
|
+
let maxY = -Infinity;
|
|
518
|
+
let minX = Infinity, maxX = -Infinity, minZ = Infinity, maxZ = -Infinity;
|
|
519
|
+
for (let v = 0; v + 2 < pos.length; v += 3) {
|
|
520
|
+
if (pos[v + 1] > maxY)
|
|
521
|
+
maxY = pos[v + 1];
|
|
522
|
+
if (pos[v] < minX)
|
|
523
|
+
minX = pos[v];
|
|
524
|
+
if (pos[v] > maxX)
|
|
525
|
+
maxX = pos[v];
|
|
526
|
+
if (pos[v + 2] < minZ)
|
|
527
|
+
minZ = pos[v + 2];
|
|
528
|
+
if (pos[v + 2] > maxZ)
|
|
529
|
+
maxZ = pos[v + 2];
|
|
530
|
+
}
|
|
531
|
+
// The cook normalises every prop to base-at-y=0, so maxY IS the height. A model that violates that
|
|
532
|
+
// still renders — it just gets a `u` ramp that does not reach 1, i.e. a milder deform, never a
|
|
533
|
+
// broken one.
|
|
534
|
+
const h = Math.max(1e-6, maxY);
|
|
535
|
+
const halfW = Math.max(1e-6, Math.max(maxX - minX, maxZ - minZ) * 0.5);
|
|
536
|
+
return { invH: 1 / h, invHalfW: 1 / halfW };
|
|
537
|
+
}
|
|
538
|
+
/** Pack a PropDeform into d0/d1/d2. An absent deform writes the exact identity. */
|
|
539
|
+
function writeDeform(out, o, d, e) {
|
|
540
|
+
out[o] = d?.height ?? 1;
|
|
541
|
+
out[o + 1] = d?.girth ?? 1;
|
|
542
|
+
out[o + 2] = d?.taper ?? 0;
|
|
543
|
+
out[o + 3] = e.invH;
|
|
544
|
+
out[o + 4] = d?.bend ?? 0;
|
|
545
|
+
out[o + 5] = d?.bendDir ?? 0;
|
|
546
|
+
out[o + 6] = d?.twist ?? 0;
|
|
547
|
+
out[o + 7] = d?.droop ?? 0;
|
|
548
|
+
out[o + 8] = d?.yaw ?? 0;
|
|
549
|
+
out[o + 9] = d?.valueJitter ?? 1;
|
|
550
|
+
out[o + 10] = e.invHalfW;
|
|
551
|
+
out[o + 11] = 0;
|
|
552
|
+
}
|
|
404
553
|
// column-major identity mat4, used for lightVP when shadow-receive is off
|
|
405
554
|
const IDENTITY4 = new Float32Array([
|
|
406
555
|
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1,
|