@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.
Files changed (41) hide show
  1. package/package.json +1 -1
  2. package/src/core/component.ts +1 -1
  3. package/src/core/index.ts +1 -13
  4. package/src/core/math.ts +186 -0
  5. package/src/core/state.ts +1 -1
  6. package/src/core/xml.ts +56 -41
  7. package/src/extras/orbit/index.ts +1 -1
  8. package/src/extras/text/index.ts +10 -65
  9. package/src/extras/{water.ts → water/index.ts} +59 -4
  10. package/src/standard/raster/batch.ts +149 -0
  11. package/src/standard/raster/forward.ts +832 -0
  12. package/src/standard/raster/index.ts +146 -472
  13. package/src/standard/raster/shadow.ts +408 -0
  14. package/src/standard/raytracing/bvh/blas.ts +335 -87
  15. package/src/standard/raytracing/bvh/radix.ts +225 -228
  16. package/src/standard/raytracing/bvh/refit.ts +711 -0
  17. package/src/standard/raytracing/bvh/structs.ts +0 -55
  18. package/src/standard/raytracing/bvh/tlas.ts +153 -141
  19. package/src/standard/raytracing/bvh/traverse.ts +72 -64
  20. package/src/standard/raytracing/index.ts +233 -204
  21. package/src/standard/raytracing/instance.ts +30 -18
  22. package/src/standard/raytracing/ray.ts +1 -1
  23. package/src/standard/raytracing/shaders.ts +23 -40
  24. package/src/standard/render/camera.ts +10 -28
  25. package/src/standard/render/data.ts +1 -1
  26. package/src/standard/render/index.ts +68 -12
  27. package/src/standard/render/light.ts +2 -2
  28. package/src/standard/render/mesh.ts +404 -0
  29. package/src/standard/render/overlay.ts +5 -2
  30. package/src/standard/render/postprocess.ts +263 -267
  31. package/src/standard/render/surface/index.ts +81 -12
  32. package/src/standard/render/surface/shaders.ts +265 -11
  33. package/src/standard/render/surface/structs.ts +10 -0
  34. package/src/standard/tween/tween.ts +44 -115
  35. package/src/standard/render/mesh/box.ts +0 -20
  36. package/src/standard/render/mesh/index.ts +0 -315
  37. package/src/standard/render/mesh/plane.ts +0 -11
  38. package/src/standard/render/mesh/sphere.ts +0 -40
  39. package/src/standard/render/mesh/unified.ts +0 -96
  40. package/src/standard/render/surface/compile.ts +0 -65
  41. 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
- export const STARS_WGSL = /* wgsl */ `
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 { NOISE_WGSL } from "./noise";
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
- export const MOON_WGSL = /* wgsl */ `
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
- export const CLOUDS_WGSL = /* wgsl */ `
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 = ${DEG_TO_RAD};
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 NdotL = max(dot(surface.normal, -scene.sunDirection.xyz), 0.0);
253
- let ambient = scene.ambientColor.rgb * scene.ambientColor.a;
254
- let sunDiffuse = scene.sunColor.rgb * NdotL;
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 lighting = (ambient + sunDiffuse) * diffuseWeight;
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 getFieldAccessor(bindingId: number): FieldAccessor | undefined {
67
- return fieldAccessors.get(bindingId);
68
- }
58
+ function getOrBindAccessor(tweenEid: number): FieldAccessor | undefined {
59
+ const existing = fieldAccessors.get(tweenEid);
60
+ if (existing) return existing;
69
61
 
70
- function parseTweenAttrs(attrs: Record<string, string>): Record<string, string> {
71
- if (attrs._value) {
72
- const parsed: Record<string, string> = {};
73
- for (const part of attrs._value.split(";")) {
74
- const colonIdx = part.indexOf(":");
75
- if (colonIdx === -1) continue;
76
- const key = part.slice(0, colonIdx).trim();
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
- easingIndex: [] as number[],
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
- easingIndex: 0,
119
+ easing: 0,
110
120
  }),
111
- adapter: (attrs: Record<string, string>, eid: number) => {
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("tween-target", {
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 = getFieldAccessor(tweenEid);
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 = getFieldAccessor(tweenEid);
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 = getFieldAccessor(tweenEid);
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.easingIndex[tweenEid]);
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.easingIndex[tweenEid] = getEasingIndex(options.easing ?? "linear");
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
- }