@multiplekex/shallot 0.2.5 → 0.3.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/package.json +1 -1
- package/src/core/component.ts +1 -1
- package/src/core/index.ts +1 -13
- package/src/core/math.ts +186 -0
- package/src/core/state.ts +1 -1
- package/src/core/xml.ts +56 -41
- package/src/extras/orbit/index.ts +1 -1
- package/src/extras/text/index.ts +10 -65
- package/src/extras/{water.ts → water/index.ts} +59 -4
- package/src/standard/raster/batch.ts +149 -0
- package/src/standard/raster/forward.ts +832 -0
- package/src/standard/raster/index.ts +146 -472
- package/src/standard/raster/shadow.ts +408 -0
- package/src/standard/raytracing/bvh/blas.ts +335 -87
- package/src/standard/raytracing/bvh/radix.ts +225 -228
- package/src/standard/raytracing/bvh/refit.ts +711 -0
- package/src/standard/raytracing/bvh/structs.ts +0 -55
- package/src/standard/raytracing/bvh/tlas.ts +153 -141
- package/src/standard/raytracing/bvh/traverse.ts +72 -64
- package/src/standard/raytracing/index.ts +233 -204
- package/src/standard/raytracing/instance.ts +30 -18
- package/src/standard/raytracing/ray.ts +1 -1
- package/src/standard/raytracing/shaders.ts +23 -40
- package/src/standard/render/camera.ts +10 -28
- package/src/standard/render/data.ts +1 -1
- package/src/standard/render/index.ts +68 -12
- package/src/standard/render/light.ts +2 -2
- package/src/standard/render/mesh.ts +404 -0
- package/src/standard/render/overlay.ts +5 -2
- package/src/standard/render/postprocess.ts +263 -267
- package/src/standard/render/surface/index.ts +81 -12
- package/src/standard/render/surface/shaders.ts +265 -11
- package/src/standard/render/surface/structs.ts +10 -0
- package/src/standard/tween/tween.ts +44 -115
- package/src/standard/render/mesh/box.ts +0 -20
- package/src/standard/render/mesh/index.ts +0 -315
- package/src/standard/render/mesh/plane.ts +0 -11
- package/src/standard/render/mesh/sphere.ts +0 -40
- package/src/standard/render/mesh/unified.ts +0 -96
- package/src/standard/render/surface/compile.ts +0 -65
- package/src/standard/render/surface/noise.ts +0 -58
|
@@ -6,7 +6,7 @@ export function compileVertexBody(vertex?: string): string {
|
|
|
6
6
|
: "return worldPos;";
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
const STARS_WGSL = /* wgsl */ `
|
|
10
10
|
fn hashStar(p: vec2<f32>) -> f32 {
|
|
11
11
|
var p3 = fract(vec3(p.x, p.y, p.x) * 0.1031);
|
|
12
12
|
p3 += dot(p3, p3.yzx + 33.33);
|
|
@@ -67,9 +67,66 @@ fn sampleStars(dir: vec3<f32>) -> vec3<f32> {
|
|
|
67
67
|
}
|
|
68
68
|
`;
|
|
69
69
|
|
|
70
|
-
export
|
|
70
|
+
export const NOISE_WGSL = /* wgsl */ `
|
|
71
|
+
fn hash2(p: vec2<f32>) -> f32 {
|
|
72
|
+
var p3 = fract(vec3(p.x, p.y, p.x) * 0.1031);
|
|
73
|
+
p3 += dot(p3, p3.yzx + 33.33);
|
|
74
|
+
return fract((p3.x + p3.y) * p3.z);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fn value2d(p: vec2f, seed: vec2f) -> f32 {
|
|
78
|
+
let i = floor(p);
|
|
79
|
+
let f = fract(p);
|
|
80
|
+
let u = f * f * (3.0 - 2.0 * f);
|
|
81
|
+
return mix(
|
|
82
|
+
mix(fract(sin(dot(i, seed)) * 43758.5) * 2.0 - 1.0,
|
|
83
|
+
fract(sin(dot(i + vec2(1.0, 0.0), seed)) * 43758.5) * 2.0 - 1.0, u.x),
|
|
84
|
+
mix(fract(sin(dot(i + vec2(0.0, 1.0), seed)) * 43758.5) * 2.0 - 1.0,
|
|
85
|
+
fract(sin(dot(i + vec2(1.0, 1.0), seed)) * 43758.5) * 2.0 - 1.0, u.x), u.y);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fn simplex2(p: vec2<f32>) -> f32 {
|
|
89
|
+
let K1 = 0.366025404;
|
|
90
|
+
let K2 = 0.211324865;
|
|
91
|
+
|
|
92
|
+
let i = floor(p + (p.x + p.y) * K1);
|
|
93
|
+
let a = p - i + (i.x + i.y) * K2;
|
|
94
|
+
|
|
95
|
+
let o = select(vec2(0.0, 1.0), vec2(1.0, 0.0), a.x > a.y);
|
|
96
|
+
let b = a - o + K2;
|
|
97
|
+
let c = a - 1.0 + 2.0 * K2;
|
|
98
|
+
|
|
99
|
+
let h = max(0.5 - vec3(dot(a, a), dot(b, b), dot(c, c)), vec3(0.0));
|
|
100
|
+
let h4 = h * h * h * h;
|
|
101
|
+
|
|
102
|
+
let n = vec3(
|
|
103
|
+
dot(a, vec2(hash2(i) * 2.0 - 1.0, hash2(i + vec2(0.0, 1.0)) * 2.0 - 1.0)),
|
|
104
|
+
dot(b, vec2(hash2(i + o) * 2.0 - 1.0, hash2(i + o + vec2(0.0, 1.0)) * 2.0 - 1.0)),
|
|
105
|
+
dot(c, vec2(hash2(i + 1.0) * 2.0 - 1.0, hash2(i + vec2(1.0, 2.0)) * 2.0 - 1.0))
|
|
106
|
+
);
|
|
71
107
|
|
|
72
|
-
|
|
108
|
+
return dot(h4, n) * 70.0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const FBM2_OCTAVES = 5;
|
|
112
|
+
|
|
113
|
+
fn fbm2(p: vec2<f32>) -> f32 {
|
|
114
|
+
var value = 0.0;
|
|
115
|
+
var amplitude = 0.5;
|
|
116
|
+
var frequency = 1.0;
|
|
117
|
+
var pos = p;
|
|
118
|
+
|
|
119
|
+
for (var i = 0; i < FBM2_OCTAVES; i++) {
|
|
120
|
+
value += amplitude * simplex2(pos * frequency);
|
|
121
|
+
amplitude *= 0.5;
|
|
122
|
+
frequency *= 2.0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
`;
|
|
128
|
+
|
|
129
|
+
const MOON_WGSL = /* wgsl */ `
|
|
73
130
|
fn sampleMoon(dir: vec3<f32>) -> vec3<f32> {
|
|
74
131
|
if (sky.moonParams.z <= 0.0) {
|
|
75
132
|
return vec3(0.0);
|
|
@@ -125,7 +182,7 @@ fn sampleMoon(dir: vec3<f32>) -> vec3<f32> {
|
|
|
125
182
|
}
|
|
126
183
|
`;
|
|
127
184
|
|
|
128
|
-
|
|
185
|
+
const CLOUDS_WGSL = /* wgsl */ `
|
|
129
186
|
fn sampleClouds(dir: vec3<f32>) -> vec4<f32> {
|
|
130
187
|
if (sky.cloudParams.w <= 0.0 || dir.y < 0.01) {
|
|
131
188
|
return vec4(0.0);
|
|
@@ -146,10 +203,8 @@ fn sampleClouds(dir: vec3<f32>) -> vec4<f32> {
|
|
|
146
203
|
}
|
|
147
204
|
`;
|
|
148
205
|
|
|
149
|
-
const DEG_TO_RAD = Math.PI / 180;
|
|
150
|
-
|
|
151
206
|
export const SKY_DIR_WGSL = /* wgsl */ `
|
|
152
|
-
const DEG_TO_RAD: f32 =
|
|
207
|
+
const DEG_TO_RAD: f32 = 0.017453292;
|
|
153
208
|
|
|
154
209
|
fn computeSkyDir(screenX: f32, screenY: f32) -> vec3<f32> {
|
|
155
210
|
let width = scene.viewport.x;
|
|
@@ -248,10 +303,209 @@ fn applyHaze(color: vec3<f32>, dist: f32) -> vec3<f32> {
|
|
|
248
303
|
}
|
|
249
304
|
`;
|
|
250
305
|
|
|
306
|
+
export const SHADOW_SAMPLE_WGSL = /* wgsl */ `
|
|
307
|
+
const GOLDEN_ANGLE: f32 = 2.399963229728653;
|
|
308
|
+
const CASCADE_BLEND_RANGE: f32 = 0.1;
|
|
309
|
+
|
|
310
|
+
fn selectCascade(viewZ: f32) -> u32 {
|
|
311
|
+
if (viewZ < shadow.cascadeSplits.x) { return 0u; }
|
|
312
|
+
if (viewZ < shadow.cascadeSplits.y) { return 1u; }
|
|
313
|
+
if (viewZ < shadow.cascadeSplits.z) { return 2u; }
|
|
314
|
+
return 3u;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
fn getCascadeViewProj(cascade: u32) -> mat4x4<f32> {
|
|
318
|
+
switch cascade {
|
|
319
|
+
case 0u: { return shadow.cascade0ViewProj; }
|
|
320
|
+
case 1u: { return shadow.cascade1ViewProj; }
|
|
321
|
+
case 2u: { return shadow.cascade2ViewProj; }
|
|
322
|
+
default: { return shadow.cascade3ViewProj; }
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
fn getCascadeTexelSize(cascade: u32) -> f32 {
|
|
327
|
+
switch cascade {
|
|
328
|
+
case 0u: { return shadow.cascadeTexelSizes.x; }
|
|
329
|
+
case 1u: { return shadow.cascadeTexelSizes.y; }
|
|
330
|
+
case 2u: { return shadow.cascadeTexelSizes.z; }
|
|
331
|
+
default: { return shadow.cascadeTexelSizes.w; }
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
fn getCascadeSplit(cascade: u32) -> f32 {
|
|
336
|
+
switch cascade {
|
|
337
|
+
case 0u: { return shadow.cascadeSplits.x; }
|
|
338
|
+
case 1u: { return shadow.cascadeSplits.y; }
|
|
339
|
+
case 2u: { return shadow.cascadeSplits.z; }
|
|
340
|
+
default: { return shadow.cascadeSplits.w; }
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
fn computeShadowBias(cascade: u32, NdotL: f32) -> f32 {
|
|
345
|
+
let baseBias = 0.0001 + f32(cascade) * 0.00005;
|
|
346
|
+
let clampedNdotL = max(NdotL, 0.01);
|
|
347
|
+
let slopeBias = baseBias * sqrt(1.0 - clampedNdotL * clampedNdotL) / clampedNdotL;
|
|
348
|
+
return baseBias + min(slopeBias, baseBias * 5.0);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
fn computeNormalOffset(normal: vec3<f32>, lightDir: vec3<f32>, cascade: u32) -> vec3<f32> {
|
|
352
|
+
let NdotL = abs(dot(normal, lightDir));
|
|
353
|
+
let texelSize = getCascadeTexelSize(cascade);
|
|
354
|
+
let slopeScale = saturate(1.0 - NdotL);
|
|
355
|
+
return normal * texelSize * (1.0 + slopeScale * 2.0);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
fn sampleShadowAtCascadeWithBias(worldPos: vec3<f32>, cascade: u32, bias: f32) -> f32 {
|
|
359
|
+
let lightPos = getCascadeViewProj(cascade) * vec4(worldPos, 1.0);
|
|
360
|
+
let ndc = lightPos.xyz / lightPos.w;
|
|
361
|
+
|
|
362
|
+
let inBounds = abs(ndc.x) <= 1.0 && abs(ndc.y) <= 1.0 && ndc.z >= 0.0 && ndc.z <= 1.0;
|
|
363
|
+
|
|
364
|
+
let safeNdc = clamp(ndc, vec3(-1.0, -1.0, 0.0), vec3(1.0, 1.0, 1.0));
|
|
365
|
+
var uv = safeNdc.xy * 0.5 + 0.5;
|
|
366
|
+
uv.y = 1.0 - uv.y;
|
|
367
|
+
|
|
368
|
+
let offset = vec2(f32(cascade % 2u) * 0.5, f32(cascade / 2u) * 0.5);
|
|
369
|
+
uv = uv * 0.5 + offset;
|
|
370
|
+
|
|
371
|
+
let sampled = textureSampleCompare(shadowMap, shadowSampler, uv, safeNdc.z - bias);
|
|
372
|
+
return select(1.0, sampled, inBounds);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
fn sampleShadowOffsetWithBias(worldPos: vec3<f32>, cascade: u32, uvOffset: vec2<f32>, bias: f32) -> f32 {
|
|
376
|
+
let lightPos = getCascadeViewProj(cascade) * vec4(worldPos, 1.0);
|
|
377
|
+
let ndc = lightPos.xyz / lightPos.w;
|
|
378
|
+
|
|
379
|
+
let inBounds = abs(ndc.x) <= 1.0 && abs(ndc.y) <= 1.0 && ndc.z >= 0.0 && ndc.z <= 1.0;
|
|
380
|
+
|
|
381
|
+
let safeNdc = clamp(ndc, vec3(-1.0, -1.0, 0.0), vec3(1.0, 1.0, 1.0));
|
|
382
|
+
var uv = safeNdc.xy * 0.5 + 0.5;
|
|
383
|
+
uv.y = 1.0 - uv.y;
|
|
384
|
+
|
|
385
|
+
let cascadeOffset = vec2(f32(cascade % 2u) * 0.5, f32(cascade / 2u) * 0.5);
|
|
386
|
+
uv = uv * 0.5 + cascadeOffset + uvOffset;
|
|
387
|
+
|
|
388
|
+
let sampled = textureSampleCompare(shadowMap, shadowSampler, uv, safeNdc.z - bias);
|
|
389
|
+
return select(1.0, sampled, inBounds);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
fn vogelDiskSample(sampleIndex: u32, samplesCount: u32, rotation: f32) -> vec2<f32> {
|
|
393
|
+
let angle = GOLDEN_ANGLE * f32(sampleIndex) + rotation;
|
|
394
|
+
let radius = sqrt((f32(sampleIndex) + 0.5) / f32(samplesCount));
|
|
395
|
+
return vec2(cos(angle), sin(angle)) * radius;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
fn sampleShadowPCFAtCascade(worldPos: vec3<f32>, cascade: u32, softness: f32, samples: u32, bias: f32) -> f32 {
|
|
399
|
+
let atlasTexelSize = 1.0 / 2048.0;
|
|
400
|
+
var total = 0.0;
|
|
401
|
+
|
|
402
|
+
for (var i = 0u; i < samples; i++) {
|
|
403
|
+
let disk = vogelDiskSample(i, samples, 0.0);
|
|
404
|
+
let uvOffset = disk * softness * atlasTexelSize * 4.0;
|
|
405
|
+
total += sampleShadowOffsetWithBias(worldPos, cascade, uvOffset, bias);
|
|
406
|
+
}
|
|
407
|
+
return total / f32(samples);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
fn computeCascadeBlend(viewZ: f32, cascade: u32) -> f32 {
|
|
411
|
+
if (cascade >= 3u) { return 0.0; }
|
|
412
|
+
|
|
413
|
+
let splitEnd = getCascadeSplit(cascade);
|
|
414
|
+
let blendStart = splitEnd * (1.0 - CASCADE_BLEND_RANGE);
|
|
415
|
+
|
|
416
|
+
if (viewZ < blendStart) { return 0.0; }
|
|
417
|
+
return saturate((viewZ - blendStart) / (splitEnd - blendStart));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
fn distanceFade(viewZ: f32, maxDist: f32) -> f32 {
|
|
421
|
+
let fadeStart = maxDist * 0.9;
|
|
422
|
+
let fade = saturate((maxDist - viewZ) / (maxDist - fadeStart));
|
|
423
|
+
return select(fade, 1.0, viewZ <= fadeStart);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
fn sampleShadow(worldPos: vec3<f32>, normal: vec3<f32>, viewZ: f32) -> f32 {
|
|
427
|
+
let lightDir = -scene.sunDirection.xyz;
|
|
428
|
+
let NdotL = max(dot(normal, lightDir), 0.0);
|
|
429
|
+
|
|
430
|
+
let cascade = selectCascade(viewZ);
|
|
431
|
+
let offset = computeNormalOffset(normal, lightDir, cascade);
|
|
432
|
+
let offsetPos = worldPos + offset;
|
|
433
|
+
let bias = computeShadowBias(cascade, NdotL);
|
|
434
|
+
let shadowCurrent = sampleShadowAtCascadeWithBias(offsetPos, cascade, bias);
|
|
435
|
+
|
|
436
|
+
let nextCascade = min(cascade + 1u, 3u);
|
|
437
|
+
let nextOffset = computeNormalOffset(normal, lightDir, nextCascade);
|
|
438
|
+
let nextOffsetPos = worldPos + nextOffset;
|
|
439
|
+
let nextBias = computeShadowBias(nextCascade, NdotL);
|
|
440
|
+
let shadowNext = sampleShadowAtCascadeWithBias(nextOffsetPos, nextCascade, nextBias);
|
|
441
|
+
|
|
442
|
+
let blendFactor = computeCascadeBlend(viewZ, cascade) * f32(cascade < 3u);
|
|
443
|
+
let cascadeShadow = mix(shadowCurrent, shadowNext, blendFactor);
|
|
444
|
+
|
|
445
|
+
let fade = distanceFade(viewZ, shadow.cascadeSplits.w);
|
|
446
|
+
return mix(1.0, cascadeShadow, fade);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
fn sampleShadowPCF(worldPos: vec3<f32>, normal: vec3<f32>, viewZ: f32, softness: f32, samples: u32) -> f32 {
|
|
450
|
+
if (softness <= 0.0) {
|
|
451
|
+
return sampleShadow(worldPos, normal, viewZ);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
let lightDir = -scene.sunDirection.xyz;
|
|
455
|
+
let NdotL = max(dot(normal, lightDir), 0.0);
|
|
456
|
+
|
|
457
|
+
let cascade = selectCascade(viewZ);
|
|
458
|
+
let offset = computeNormalOffset(normal, lightDir, cascade);
|
|
459
|
+
let offsetPos = worldPos + offset;
|
|
460
|
+
let bias = computeShadowBias(cascade, NdotL);
|
|
461
|
+
let shadowCurrent = sampleShadowPCFAtCascade(offsetPos, cascade, softness, samples, bias);
|
|
462
|
+
|
|
463
|
+
let nextCascade = min(cascade + 1u, 3u);
|
|
464
|
+
let nextOffset = computeNormalOffset(normal, lightDir, nextCascade);
|
|
465
|
+
let nextOffsetPos = worldPos + nextOffset;
|
|
466
|
+
let nextBias = computeShadowBias(nextCascade, NdotL);
|
|
467
|
+
let shadowNext = sampleShadowPCFAtCascade(nextOffsetPos, nextCascade, softness, samples, nextBias);
|
|
468
|
+
|
|
469
|
+
let blendFactor = computeCascadeBlend(viewZ, cascade) * f32(cascade < 3u);
|
|
470
|
+
let cascadeShadow = mix(shadowCurrent, shadowNext, blendFactor);
|
|
471
|
+
|
|
472
|
+
let fade = distanceFade(viewZ, shadow.cascadeSplits.w);
|
|
473
|
+
return mix(1.0, cascadeShadow, fade);
|
|
474
|
+
}
|
|
475
|
+
`;
|
|
476
|
+
|
|
477
|
+
export const SPECULAR_WGSL = /* wgsl */ `
|
|
478
|
+
const DIELECTRIC_F0: f32 = 0.04;
|
|
479
|
+
|
|
480
|
+
fn blinnPhongSpecular(N: vec3<f32>, L: vec3<f32>, V: vec3<f32>, roughness: f32) -> f32 {
|
|
481
|
+
let H = normalize(L + V);
|
|
482
|
+
let NdotH = max(dot(N, H), 0.0);
|
|
483
|
+
let shininess = pow(2.0, (1.0 - roughness) * 10.0);
|
|
484
|
+
let intensity = (1.0 - roughness) * (1.0 - roughness);
|
|
485
|
+
return pow(NdotH, shininess) * intensity;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
fn schlickFresnel(cosTheta: f32, F0: vec3<f32>) -> vec3<f32> {
|
|
489
|
+
return F0 + (vec3(1.0) - F0) * pow(1.0 - cosTheta, 5.0);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
fn computeF0Vec(baseColor: vec3<f32>, metallic: f32) -> vec3<f32> {
|
|
493
|
+
return mix(vec3(DIELECTRIC_F0), baseColor, metallic);
|
|
494
|
+
}
|
|
495
|
+
`;
|
|
496
|
+
|
|
251
497
|
export const WGSL_LIGHTING_CALC = /* wgsl */ `
|
|
252
|
-
let
|
|
253
|
-
let
|
|
254
|
-
let
|
|
498
|
+
let V = normalize(scene.cameraWorld[3].xyz - surface.worldPos);
|
|
499
|
+
let L = -scene.sunDirection.xyz;
|
|
500
|
+
let NdotL = max(dot(surface.normal, L), 0.0);
|
|
501
|
+
let NdotV = max(dot(surface.normal, V), 0.0);
|
|
502
|
+
let F0 = computeF0Vec(surface.baseColor, surface.metallic);
|
|
503
|
+
let F = schlickFresnel(NdotV, F0);
|
|
255
504
|
let diffuseWeight = 1.0 - surface.metallic;
|
|
256
|
-
let
|
|
505
|
+
let ambient = scene.ambientColor.rgb * scene.ambientColor.a;
|
|
506
|
+
let sunDiffuse = scene.sunColor.rgb * NdotL * shadowFactor;
|
|
507
|
+
let diffuseColor = surface.baseColor * (ambient + sunDiffuse) * diffuseWeight + surface.emission;
|
|
508
|
+
let specTerm = blinnPhongSpecular(surface.normal, L, V, surface.roughness);
|
|
509
|
+
let specular = scene.sunColor.rgb * specTerm * F * NdotL * shadowFactor;
|
|
510
|
+
let litColor = diffuseColor + specular;
|
|
257
511
|
`;
|
|
@@ -64,6 +64,16 @@ struct Data {
|
|
|
64
64
|
_pad2: u32,
|
|
65
65
|
}`;
|
|
66
66
|
|
|
67
|
+
export const SHADOW_STRUCT_WGSL = /* wgsl */ `
|
|
68
|
+
struct Shadow {
|
|
69
|
+
cascade0ViewProj: mat4x4<f32>,
|
|
70
|
+
cascade1ViewProj: mat4x4<f32>,
|
|
71
|
+
cascade2ViewProj: mat4x4<f32>,
|
|
72
|
+
cascade3ViewProj: mat4x4<f32>,
|
|
73
|
+
cascadeSplits: vec4<f32>,
|
|
74
|
+
cascadeTexelSizes: vec4<f32>,
|
|
75
|
+
}`;
|
|
76
|
+
|
|
67
77
|
export const WGSL_STRUCTS = /* wgsl */ `
|
|
68
78
|
struct VertexInput {
|
|
69
79
|
@location(0) position: vec3<f32>,
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
defineRelation,
|
|
3
|
-
registerPostLoadHook,
|
|
4
|
-
toCamelCase,
|
|
5
|
-
type State,
|
|
6
|
-
type System,
|
|
7
|
-
type Plugin,
|
|
8
|
-
type PostLoadContext,
|
|
9
|
-
} from "../../core";
|
|
1
|
+
import { defineRelation, toCamelCase, type State, type System, type Plugin } from "../../core";
|
|
10
2
|
import {
|
|
11
3
|
setTraits,
|
|
12
4
|
getRegisteredComponent,
|
|
@@ -63,23 +55,17 @@ function bindFieldAccessor(
|
|
|
63
55
|
return accessor;
|
|
64
56
|
}
|
|
65
57
|
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
|
|
58
|
+
function getOrBindAccessor(tweenEid: number): FieldAccessor | undefined {
|
|
59
|
+
const existing = fieldAccessors.get(tweenEid);
|
|
60
|
+
if (existing) return existing;
|
|
69
61
|
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const value = part.slice(colonIdx + 1).trim();
|
|
78
|
-
if (key && value) parsed[key] = value;
|
|
79
|
-
}
|
|
80
|
-
return parsed;
|
|
81
|
-
}
|
|
82
|
-
return attrs;
|
|
62
|
+
const path = Tween.field[tweenEid];
|
|
63
|
+
if (!path) return undefined;
|
|
64
|
+
|
|
65
|
+
const parsed = resolveFieldPath(path);
|
|
66
|
+
if (!parsed) return undefined;
|
|
67
|
+
|
|
68
|
+
return bindFieldAccessor(tweenEid, parsed.component, parsed.field) ?? undefined;
|
|
83
69
|
}
|
|
84
70
|
|
|
85
71
|
export const TweenState = {
|
|
@@ -88,6 +74,29 @@ export const TweenState = {
|
|
|
88
74
|
COMPLETE: 2,
|
|
89
75
|
} as const;
|
|
90
76
|
|
|
77
|
+
const fieldPaths = new Map<number, string>();
|
|
78
|
+
|
|
79
|
+
function fieldProxy(): Record<number, string | undefined> {
|
|
80
|
+
return new Proxy({} as Record<number, string | undefined>, {
|
|
81
|
+
get(_, prop) {
|
|
82
|
+
const eid = Number(prop);
|
|
83
|
+
if (Number.isNaN(eid)) return undefined;
|
|
84
|
+
return fieldPaths.get(eid);
|
|
85
|
+
},
|
|
86
|
+
set(_, prop, value) {
|
|
87
|
+
const eid = Number(prop);
|
|
88
|
+
if (Number.isNaN(eid)) return false;
|
|
89
|
+
if (value === undefined || value === null) {
|
|
90
|
+
fieldPaths.delete(eid);
|
|
91
|
+
} else {
|
|
92
|
+
fieldPaths.set(eid, value as string);
|
|
93
|
+
}
|
|
94
|
+
fieldAccessors.delete(eid);
|
|
95
|
+
return true;
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
91
100
|
export const Tween = {
|
|
92
101
|
state: [] as number[],
|
|
93
102
|
from: [] as number[],
|
|
@@ -95,7 +104,8 @@ export const Tween = {
|
|
|
95
104
|
duration: [] as number[],
|
|
96
105
|
elapsed: [] as number[],
|
|
97
106
|
delay: [] as number[],
|
|
98
|
-
|
|
107
|
+
easing: [] as number[],
|
|
108
|
+
field: fieldProxy(),
|
|
99
109
|
};
|
|
100
110
|
|
|
101
111
|
setTraits(Tween, {
|
|
@@ -106,96 +116,18 @@ setTraits(Tween, {
|
|
|
106
116
|
duration: 1,
|
|
107
117
|
elapsed: 0,
|
|
108
118
|
delay: 0,
|
|
109
|
-
|
|
119
|
+
easing: 0,
|
|
110
120
|
}),
|
|
111
|
-
|
|
112
|
-
const parsed = parseTweenAttrs(attrs);
|
|
113
|
-
const result: Record<string, number> = {};
|
|
114
|
-
|
|
115
|
-
if (parsed.duration) result.duration = parseFloat(parsed.duration);
|
|
116
|
-
if (parsed.delay) result.delay = parseFloat(parsed.delay);
|
|
117
|
-
if (parsed.easing) result.easingIndex = getEasingIndex(parsed.easing);
|
|
118
|
-
|
|
119
|
-
if (parsed.target) {
|
|
120
|
-
setupTweenFromXml(parsed, eid);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return result;
|
|
124
|
-
},
|
|
121
|
+
parse: { easing: getEasingIndex },
|
|
125
122
|
});
|
|
126
123
|
|
|
127
|
-
export const TweenTarget = defineRelation("
|
|
124
|
+
export const TweenTarget = defineRelation("target", {
|
|
128
125
|
exclusive: true,
|
|
129
126
|
});
|
|
130
127
|
|
|
131
|
-
interface ParsedTargetPath {
|
|
132
|
-
readonly entity: string;
|
|
133
|
-
readonly component: string;
|
|
134
|
-
readonly field: string;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function parseTargetPath(path: string): ParsedTargetPath | null {
|
|
138
|
-
if (!path.startsWith("@")) return null;
|
|
139
|
-
|
|
140
|
-
const rest = path.slice(1);
|
|
141
|
-
const firstDot = rest.indexOf(".");
|
|
142
|
-
if (firstDot === -1) return null;
|
|
143
|
-
|
|
144
|
-
const entity = rest.slice(0, firstDot);
|
|
145
|
-
const fieldPath = rest.slice(firstDot + 1);
|
|
146
|
-
const dotIndex = fieldPath.lastIndexOf(".");
|
|
147
|
-
if (dotIndex === -1) return null;
|
|
148
|
-
|
|
149
|
-
return {
|
|
150
|
-
entity,
|
|
151
|
-
component: fieldPath.slice(0, dotIndex),
|
|
152
|
-
field: fieldPath.slice(dotIndex + 1),
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
interface PendingTween {
|
|
157
|
-
readonly tweenEid: number;
|
|
158
|
-
readonly target: string;
|
|
159
|
-
readonly to: string;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
let pendingXmlTweens: PendingTween[] = [];
|
|
163
|
-
|
|
164
|
-
function setupTweenFromXml(attrs: Record<string, string>, tweenEid: number): void {
|
|
165
|
-
pendingXmlTweens.push({
|
|
166
|
-
tweenEid,
|
|
167
|
-
target: attrs.target,
|
|
168
|
-
to: attrs.to,
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export function finalizePendingTweens(state: State, context: PostLoadContext): void {
|
|
173
|
-
for (const pending of pendingXmlTweens) {
|
|
174
|
-
const parsed = parseTargetPath(pending.target);
|
|
175
|
-
if (!parsed) continue;
|
|
176
|
-
|
|
177
|
-
const targetEid = context.getEntityByName(parsed.entity);
|
|
178
|
-
if (targetEid === null) continue;
|
|
179
|
-
|
|
180
|
-
const binding = bindFieldAccessor(pending.tweenEid, parsed.component, parsed.field);
|
|
181
|
-
if (!binding) continue;
|
|
182
|
-
|
|
183
|
-
state.addRelation(pending.tweenEid, TweenTarget, targetEid);
|
|
184
|
-
const toValue =
|
|
185
|
-
pending.to.startsWith("0x") || pending.to.startsWith("0X")
|
|
186
|
-
? parseInt(pending.to, 16)
|
|
187
|
-
: parseFloat(pending.to);
|
|
188
|
-
if (!Number.isFinite(toValue)) {
|
|
189
|
-
throw new Error(`Tween has invalid 'to' value: "${pending.to}" (parsed as ${toValue})`);
|
|
190
|
-
}
|
|
191
|
-
Tween.to[pending.tweenEid] = toValue;
|
|
192
|
-
}
|
|
193
|
-
pendingXmlTweens = [];
|
|
194
|
-
}
|
|
195
|
-
|
|
196
128
|
export function captureFromValue(state: State, tweenEid: number): void {
|
|
197
129
|
const targetEid = state.getFirstRelationTarget(tweenEid, TweenTarget);
|
|
198
|
-
const binding =
|
|
130
|
+
const binding = getOrBindAccessor(tweenEid);
|
|
199
131
|
|
|
200
132
|
if (binding && targetEid >= 0) {
|
|
201
133
|
Tween.from[tweenEid] = binding.get(targetEid) ?? 0;
|
|
@@ -209,7 +141,7 @@ export function ensureResolved(state: State, tweenEid: number): void {
|
|
|
209
141
|
if (duration > 0 && elapsed >= duration) return;
|
|
210
142
|
|
|
211
143
|
const targetEid = state.getFirstRelationTarget(tweenEid, TweenTarget);
|
|
212
|
-
const binding =
|
|
144
|
+
const binding = getOrBindAccessor(tweenEid);
|
|
213
145
|
|
|
214
146
|
if (binding && targetEid >= 0) {
|
|
215
147
|
const toValue = Tween.to[tweenEid];
|
|
@@ -235,7 +167,7 @@ function updateTweens(state: State, dt: number): void {
|
|
|
235
167
|
if (tweenState !== TweenState.PLAYING) continue;
|
|
236
168
|
|
|
237
169
|
const targetEid = state.getFirstRelationTarget(tweenEid, TweenTarget);
|
|
238
|
-
const binding =
|
|
170
|
+
const binding = getOrBindAccessor(tweenEid);
|
|
239
171
|
|
|
240
172
|
if (Tween.elapsed[tweenEid] === 0 && binding && targetEid >= 0) {
|
|
241
173
|
Tween.from[tweenEid] = binding.get(targetEid) ?? 0;
|
|
@@ -253,7 +185,7 @@ function updateTweens(state: State, dt: number): void {
|
|
|
253
185
|
);
|
|
254
186
|
}
|
|
255
187
|
|
|
256
|
-
const easingFn = getEasing(Tween.
|
|
188
|
+
const easingFn = getEasing(Tween.easing[tweenEid]);
|
|
257
189
|
const easedProgress = easingFn(rawProgress);
|
|
258
190
|
|
|
259
191
|
const from = Tween.from[tweenEid];
|
|
@@ -305,7 +237,7 @@ export function createTween(
|
|
|
305
237
|
Tween.to[tweenEid] = options.to;
|
|
306
238
|
Tween.duration[tweenEid] = options.duration ?? 1;
|
|
307
239
|
Tween.elapsed[tweenEid] = 0;
|
|
308
|
-
Tween.
|
|
240
|
+
Tween.easing[tweenEid] = getEasingIndex(options.easing ?? "linear");
|
|
309
241
|
|
|
310
242
|
return tweenEid;
|
|
311
243
|
}
|
|
@@ -327,7 +259,4 @@ export const TweenPlugin: Plugin = {
|
|
|
327
259
|
systems: [TweenSystem],
|
|
328
260
|
components: { Tween, Sequence, Pause },
|
|
329
261
|
relations: [TweenTarget],
|
|
330
|
-
initialize() {
|
|
331
|
-
registerPostLoadHook(finalizePendingTweens);
|
|
332
|
-
},
|
|
333
262
|
};
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { MeshData } from "./index";
|
|
2
|
-
|
|
3
|
-
export function createBox(): MeshData {
|
|
4
|
-
const vertices = new Float32Array([
|
|
5
|
-
-0.5, -0.5, 0.5, 0, 0, 1, 0.5, -0.5, 0.5, 0, 0, 1, 0.5, 0.5, 0.5, 0, 0, 1, -0.5, 0.5, 0.5,
|
|
6
|
-
0, 0, 1, 0.5, -0.5, -0.5, 0, 0, -1, -0.5, -0.5, -0.5, 0, 0, -1, -0.5, 0.5, -0.5, 0, 0, -1,
|
|
7
|
-
0.5, 0.5, -0.5, 0, 0, -1, -0.5, 0.5, 0.5, 0, 1, 0, 0.5, 0.5, 0.5, 0, 1, 0, 0.5, 0.5, -0.5,
|
|
8
|
-
0, 1, 0, -0.5, 0.5, -0.5, 0, 1, 0, -0.5, -0.5, -0.5, 0, -1, 0, 0.5, -0.5, -0.5, 0, -1, 0,
|
|
9
|
-
0.5, -0.5, 0.5, 0, -1, 0, -0.5, -0.5, 0.5, 0, -1, 0, 0.5, -0.5, 0.5, 1, 0, 0, 0.5, -0.5,
|
|
10
|
-
-0.5, 1, 0, 0, 0.5, 0.5, -0.5, 1, 0, 0, 0.5, 0.5, 0.5, 1, 0, 0, -0.5, -0.5, -0.5, -1, 0, 0,
|
|
11
|
-
-0.5, -0.5, 0.5, -1, 0, 0, -0.5, 0.5, 0.5, -1, 0, 0, -0.5, 0.5, -0.5, -1, 0, 0,
|
|
12
|
-
]);
|
|
13
|
-
|
|
14
|
-
const indices = new Uint16Array([
|
|
15
|
-
0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18,
|
|
16
|
-
16, 18, 19, 20, 21, 22, 20, 22, 23,
|
|
17
|
-
]);
|
|
18
|
-
|
|
19
|
-
return { vertices, indices, vertexCount: 24, indexCount: 36 };
|
|
20
|
-
}
|