@opsydyn/effect-three 0.0.0-stage → 0.1.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 (166) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/LICENSE +21 -0
  3. package/README.md +191 -2
  4. package/dist/index-4045wme0.js +129 -0
  5. package/dist/index-4045wme0.js.map +10 -0
  6. package/dist/index-ef8pbv9g.js +5118 -0
  7. package/dist/index-ef8pbv9g.js.map +40 -0
  8. package/dist/index.d.ts +12 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +1361 -0
  11. package/dist/index.js.map +14 -0
  12. package/dist/kernel/animation.d.ts +14 -0
  13. package/dist/kernel/animation.d.ts.map +1 -0
  14. package/dist/kernel/behaviours.d.ts +16 -0
  15. package/dist/kernel/behaviours.d.ts.map +1 -0
  16. package/dist/kernel/bodies.d.ts +60 -0
  17. package/dist/kernel/bodies.d.ts.map +1 -0
  18. package/dist/kernel/bodySources.d.ts +47 -0
  19. package/dist/kernel/bodySources.d.ts.map +1 -0
  20. package/dist/kernel/build.d.ts +564 -0
  21. package/dist/kernel/build.d.ts.map +1 -0
  22. package/dist/kernel/cameraMotion.d.ts +39 -0
  23. package/dist/kernel/cameraMotion.d.ts.map +1 -0
  24. package/dist/kernel/clock.d.ts +17 -0
  25. package/dist/kernel/clock.d.ts.map +1 -0
  26. package/dist/kernel/contactShadowPasses.d.ts +47 -0
  27. package/dist/kernel/contactShadowPasses.d.ts.map +1 -0
  28. package/dist/kernel/description.d.ts +1120 -0
  29. package/dist/kernel/description.d.ts.map +1 -0
  30. package/dist/kernel/diff.d.ts +90 -0
  31. package/dist/kernel/diff.d.ts.map +1 -0
  32. package/dist/kernel/environmentPresets.d.ts +30 -0
  33. package/dist/kernel/environmentPresets.d.ts.map +1 -0
  34. package/dist/kernel/equality.d.ts +64 -0
  35. package/dist/kernel/equality.d.ts.map +1 -0
  36. package/dist/kernel/frames.d.ts +55 -0
  37. package/dist/kernel/frames.d.ts.map +1 -0
  38. package/dist/kernel/index.d.ts +37 -0
  39. package/dist/kernel/index.d.ts.map +1 -0
  40. package/dist/kernel/index.js +335 -0
  41. package/dist/kernel/index.js.map +9 -0
  42. package/dist/kernel/instances.d.ts +26 -0
  43. package/dist/kernel/instances.d.ts.map +1 -0
  44. package/dist/kernel/labels.d.ts +45 -0
  45. package/dist/kernel/labels.d.ts.map +1 -0
  46. package/dist/kernel/loop.d.ts +49 -0
  47. package/dist/kernel/loop.d.ts.map +1 -0
  48. package/dist/kernel/materials.d.ts +24 -0
  49. package/dist/kernel/materials.d.ts.map +1 -0
  50. package/dist/kernel/motion.d.ts +36 -0
  51. package/dist/kernel/motion.d.ts.map +1 -0
  52. package/dist/kernel/orbit.d.ts +127 -0
  53. package/dist/kernel/orbit.d.ts.map +1 -0
  54. package/dist/kernel/overlays.d.ts +16 -0
  55. package/dist/kernel/overlays.d.ts.map +1 -0
  56. package/dist/kernel/performance.d.ts +41 -0
  57. package/dist/kernel/performance.d.ts.map +1 -0
  58. package/dist/kernel/physics.d.ts +166 -0
  59. package/dist/kernel/physics.d.ts.map +1 -0
  60. package/dist/kernel/physicsDebug.d.ts +18 -0
  61. package/dist/kernel/physicsDebug.d.ts.map +1 -0
  62. package/dist/kernel/pointer.d.ts +357 -0
  63. package/dist/kernel/pointer.d.ts.map +1 -0
  64. package/dist/kernel/renderKit.d.ts +48 -0
  65. package/dist/kernel/renderKit.d.ts.map +1 -0
  66. package/dist/kernel/renderer.d.ts +83 -0
  67. package/dist/kernel/renderer.d.ts.map +1 -0
  68. package/dist/kernel/resources.d.ts +112 -0
  69. package/dist/kernel/resources.d.ts.map +1 -0
  70. package/dist/kernel/sceneGraph.d.ts +155 -0
  71. package/dist/kernel/sceneGraph.d.ts.map +1 -0
  72. package/dist/kernel/sceneIndex.d.ts +90 -0
  73. package/dist/kernel/sceneIndex.d.ts.map +1 -0
  74. package/dist/kernel/stage.d.ts +87 -0
  75. package/dist/kernel/stage.d.ts.map +1 -0
  76. package/dist/kernel/textLayout.d.ts +29 -0
  77. package/dist/kernel/textLayout.d.ts.map +1 -0
  78. package/dist/kernel/tween.d.ts +35 -0
  79. package/dist/kernel/tween.d.ts.map +1 -0
  80. package/dist/kernel/xrInput.d.ts +107 -0
  81. package/dist/kernel/xrInput.d.ts.map +1 -0
  82. package/dist/kernel/xrLocomotion.d.ts +38 -0
  83. package/dist/kernel/xrLocomotion.d.ts.map +1 -0
  84. package/dist/runtime/FrameSource.d.ts +29 -0
  85. package/dist/runtime/FrameSource.d.ts.map +1 -0
  86. package/dist/runtime/ResourceRegistry.d.ts +18 -0
  87. package/dist/runtime/ResourceRegistry.d.ts.map +1 -0
  88. package/dist/runtime/SceneRuntime.d.ts +68 -0
  89. package/dist/runtime/SceneRuntime.d.ts.map +1 -0
  90. package/dist/runtime/SceneSchema.d.ts +2536 -0
  91. package/dist/runtime/SceneSchema.d.ts.map +1 -0
  92. package/dist/runtime/errors.d.ts +19 -0
  93. package/dist/runtime/errors.d.ts.map +1 -0
  94. package/dist/runtime/index.d.ts +11 -0
  95. package/dist/runtime/index.d.ts.map +1 -0
  96. package/dist/webgl/contactShadows.d.ts +5 -0
  97. package/dist/webgl/contactShadows.d.ts.map +1 -0
  98. package/dist/webgl/index.d.ts +11 -0
  99. package/dist/webgl/index.d.ts.map +1 -0
  100. package/dist/webgl/index.js +239 -0
  101. package/dist/webgl/index.js.map +13 -0
  102. package/dist/webgl/shaderGrid.d.ts +27 -0
  103. package/dist/webgl/shaderGrid.d.ts.map +1 -0
  104. package/dist/webgl/sky.d.ts +4 -0
  105. package/dist/webgl/sky.d.ts.map +1 -0
  106. package/dist/webgpu/contactShadows.d.ts +5 -0
  107. package/dist/webgpu/contactShadows.d.ts.map +1 -0
  108. package/dist/webgpu/index.d.ts +10 -0
  109. package/dist/webgpu/index.d.ts.map +1 -0
  110. package/dist/webgpu/index.js +216 -0
  111. package/dist/webgpu/index.js.map +13 -0
  112. package/dist/webgpu/shaderGrid.d.ts +5 -0
  113. package/dist/webgpu/shaderGrid.d.ts.map +1 -0
  114. package/dist/webgpu/sky.d.ts +4 -0
  115. package/dist/webgpu/sky.d.ts.map +1 -0
  116. package/package.json +82 -4
  117. package/src/index.ts +11 -0
  118. package/src/kernel/animation.ts +24 -0
  119. package/src/kernel/behaviours.ts +36 -0
  120. package/src/kernel/bodies.ts +338 -0
  121. package/src/kernel/bodySources.ts +203 -0
  122. package/src/kernel/build.ts +1325 -0
  123. package/src/kernel/cameraMotion.ts +202 -0
  124. package/src/kernel/clock.ts +35 -0
  125. package/src/kernel/contactShadowPasses.ts +207 -0
  126. package/src/kernel/description.ts +1197 -0
  127. package/src/kernel/diff.ts +383 -0
  128. package/src/kernel/environmentPresets.ts +38 -0
  129. package/src/kernel/equality.ts +604 -0
  130. package/src/kernel/frames.ts +124 -0
  131. package/src/kernel/index.ts +274 -0
  132. package/src/kernel/instances.ts +97 -0
  133. package/src/kernel/labels.ts +48 -0
  134. package/src/kernel/loop.ts +248 -0
  135. package/src/kernel/materials.ts +258 -0
  136. package/src/kernel/motion.ts +75 -0
  137. package/src/kernel/orbit.ts +554 -0
  138. package/src/kernel/overlays.ts +130 -0
  139. package/src/kernel/performance.ts +101 -0
  140. package/src/kernel/physics.ts +361 -0
  141. package/src/kernel/physicsDebug.ts +68 -0
  142. package/src/kernel/pointer.ts +785 -0
  143. package/src/kernel/renderKit.ts +53 -0
  144. package/src/kernel/renderer.ts +136 -0
  145. package/src/kernel/resources.ts +504 -0
  146. package/src/kernel/sceneGraph.ts +1440 -0
  147. package/src/kernel/sceneIndex.ts +219 -0
  148. package/src/kernel/stage.ts +157 -0
  149. package/src/kernel/textLayout.ts +102 -0
  150. package/src/kernel/tween.ts +80 -0
  151. package/src/kernel/xrInput.ts +230 -0
  152. package/src/kernel/xrLocomotion.ts +72 -0
  153. package/src/runtime/FrameSource.ts +41 -0
  154. package/src/runtime/ResourceRegistry.ts +24 -0
  155. package/src/runtime/SceneRuntime.ts +229 -0
  156. package/src/runtime/SceneSchema.ts +983 -0
  157. package/src/runtime/errors.ts +18 -0
  158. package/src/runtime/index.ts +10 -0
  159. package/src/webgl/contactShadows.ts +67 -0
  160. package/src/webgl/index.ts +19 -0
  161. package/src/webgl/shaderGrid.ts +196 -0
  162. package/src/webgl/sky.ts +42 -0
  163. package/src/webgpu/contactShadows.ts +83 -0
  164. package/src/webgpu/index.ts +19 -0
  165. package/src/webgpu/shaderGrid.ts +158 -0
  166. package/src/webgpu/sky.ts +35 -0
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/webgpu/contactShadows.ts", "../src/webgpu/shaderGrid.ts", "../src/webgpu/sky.ts", "../src/webgpu/index.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * The WebGPU kit's contact shadow materials, in TSL: the WebGL kit's shadow-from-depth\n * output (colour `color · depth · 2`, alpha `1 − depth`) and three's horizontal and\n * vertical blur shaders (nine taps, their weights). The passes themselves are the\n * kernel's.\n *\n * @module\n */\nimport { type Color, NoBlending, Texture } from 'three';\nimport { depth, float, texture, uniform, uv, vec2 } from 'three/tsl';\nimport { MeshBasicNodeMaterial } from 'three/webgpu';\n\nimport {\n type BlurPass,\n createContactShadowPasses,\n type ShadowMaterials,\n} from '../kernel/contactShadowPasses.ts';\nimport type { SceneRenderer } from '../kernel/renderer.ts';\nimport type { ContactShadowPasses, ShadowedScene } from '../kernel/renderKit.ts';\n\n/** A material writing exactly what its nodes say: no blending, depth or tone mapping. */\nconst plainMaterial = (): MeshBasicNodeMaterial => {\n const material = new MeshBasicNodeMaterial();\n material.transparent = true;\n material.blending = NoBlending;\n material.depthTest = false;\n material.depthWrite = false;\n material.toneMapped = false;\n return material;\n};\n\nconst depthMaterial = (color: Color): MeshBasicNodeMaterial => {\n const material = plainMaterial();\n // Darkest where an object touches the plane: `depth` is linear for the orthographic camera.\n material.colorNode = uniform(color).mul(depth).mul(2);\n material.opacityNode = float(1).sub(depth);\n return material;\n};\n\n/** three's `HorizontalBlurShader` and `VerticalBlurShader` taps: offset and weight. */\nconst taps = [\n [-4, 0.051],\n [-3, 0.0918],\n [-2, 0.12245],\n [-1, 0.1531],\n [0, 0.1633],\n [1, 0.1531],\n [2, 0.12245],\n [3, 0.0918],\n [4, 0.051],\n] as const;\n\nconst blur = (direction: 'horizontal' | 'vertical'): BlurPass => {\n const source = texture(new Texture());\n const step = uniform(0);\n const along = direction === 'horizontal' ? vec2(step, 0) : vec2(0, step);\n const [first, ...rest] = taps.map(([offset, weight]) =>\n source.sample(uv().add(along.mul(offset))).mul(weight),\n );\n const sum = rest.reduce((total, tap) => total.add(tap), first ?? source);\n const material = plainMaterial();\n material.colorNode = sum.rgb;\n material.opacityNode = sum.a;\n return {\n material,\n set: (map, amount) => {\n source.value = map;\n step.value = amount;\n },\n };\n};\n\nconst webgpuShadowMaterials: ShadowMaterials = {\n depth: depthMaterial,\n blur,\n flippedTargets: true,\n};\n\n/** Creates the contact shadow passes for one scene graph and renderer, in TSL. */\nexport const createContactShadows = (\n graph: ShadowedScene,\n renderer: SceneRenderer,\n): ContactShadowPasses => createContactShadowPasses(graph, renderer, webgpuShadowMaterials);\n",
6
+ "/**\n * The WebGPU kit's shader grid: the WebGL kit's grid (Drei's `Grid`, `bf6f4ad`, MIT; after\n * Fyrestar's `THREE.InfiniteGridHelper` and threlte's `Grid`) written in TSL, so that\n * three's `WebGPURenderer` can draw it on either backend. Same lines, same fade, same\n * camera uniforms set in `onBeforeRender`.\n *\n * @module\n */\nimport {\n BackSide,\n Color,\n DoubleSide,\n FrontSide,\n Mesh,\n Plane,\n PlaneGeometry,\n type Side,\n Vector3,\n} from 'three';\nimport {\n abs,\n distance,\n float,\n fract,\n fwidth,\n min,\n mix,\n modelWorldMatrix,\n modelWorldMatrixInverse,\n positionGeometry,\n pow,\n select,\n uniform,\n varying,\n vec4,\n} from 'three/tsl';\nimport { MeshBasicNodeMaterial } from 'three/webgpu';\n\nimport type { MaterialSide, ShaderGridNode } from '../kernel/description.ts';\nimport type { HostedShaderGrid } from '../kernel/renderKit.ts';\n\nconst sides = { front: FrontSide, back: BackSide, double: DoubleSide } satisfies Record<\n MaterialSide,\n Side\n>;\n\n/** The grid's uniforms: one node each, written from the description. */\nconst createUniforms = () => ({\n cellSize: uniform(0.5),\n cellThickness: uniform(0.5),\n cellColor: uniform(new Color()),\n sectionSize: uniform(1),\n sectionThickness: uniform(1),\n sectionColor: uniform(new Color()),\n fadeDistance: uniform(100),\n fadeStrength: uniform(1),\n fadeFrom: uniform(1),\n infinite: uniform(0),\n followCamera: uniform(0),\n cameraOnPlane: uniform(new Vector3()),\n planeOrigin: uniform(new Vector3()),\n});\n\ntype Uniforms = ReturnType<typeof createUniforms>;\n\n/** The grid's material: the WebGL kit's vertex and fragment shaders, as TSL nodes. */\nconst gridMaterial = (u: Uniforms): MeshBasicNodeMaterial => {\n const material = new MeshBasicNodeMaterial({ transparent: true });\n // The plane geometry lies in XY; the grid lies in XZ. Infinite grids reach the fade.\n const lifted = positionGeometry.xzy.mul(\n select(u.infinite.greaterThan(0.5), u.fadeDistance.add(1), 1),\n );\n // Following the camera slides the plane by its foot's offset, in the grid's own space.\n const shift = modelWorldMatrixInverse.mul(vec4(u.cameraOnPlane.sub(u.planeOrigin), 0)).xyz;\n const local = select(u.followCamera.greaterThan(0.5), lifted.add(shift), lifted);\n material.positionNode = local;\n const gridPosition = varying(local);\n const worldPosition = varying(modelWorldMatrix.mul(vec4(local, 1)).xyz);\n // 1 on a line of a grid with this spacing, 0 between, antialiased over a pixel.\n const lines = (spacing: typeof u.cellSize, thickness: typeof u.cellThickness) => {\n const at = gridPosition.xz.div(spacing);\n const away = abs(fract(at.sub(0.5)).sub(0.5)).div(fwidth(at));\n return float(1).sub(min(min(away.x, away.y).add(1).sub(thickness), 1));\n };\n const cell = lines(u.cellSize, u.cellThickness);\n const section = lines(u.sectionSize, u.sectionThickness);\n const away = distance(u.cameraOnPlane.mul(u.fadeFrom), worldPosition);\n const fade = pow(float(1).sub(min(away.div(u.fadeDistance), 1)), u.fadeStrength);\n const alpha = cell.add(section).mul(fade);\n material.colorNode = mix(u.cellColor, u.sectionColor, min(1, u.sectionThickness.mul(section)));\n material.opacityNode = mix(alpha.mul(0.75), alpha, section);\n // Discard where there is no line, as the WebGL kit's shader does.\n material.alphaTest = 1e-4;\n return material;\n};\n\n/** Writes a node's settings into the uniforms and material. */\nconst writeUniforms = (\n u: Uniforms,\n material: MeshBasicNodeMaterial,\n node: ShaderGridNode,\n): void => {\n u.cellSize.value = node.cellSize;\n u.cellThickness.value = node.cellThickness;\n u.cellColor.value.setHex(node.cellColor);\n u.sectionSize.value = node.sectionSize;\n u.sectionThickness.value = node.sectionThickness;\n u.sectionColor.value.setHex(node.sectionColor);\n u.fadeDistance.value = node.fadeDistance;\n u.fadeStrength.value = node.fadeStrength;\n u.fadeFrom.value = node.fadeFrom;\n u.infinite.value = node.infinite ? 1 : 0;\n u.followCamera.value = node.followCamera ? 1 : 0;\n material.side = sides[node.side];\n};\n\nconst up = new Vector3(0, 1, 0);\nconst origin = new Vector3();\nconst plane = new Plane();\nconst cameraPosition = new Vector3();\n\n/** The WebGPU kit's shader grid: a plane with the grid material, placed and updated. */\nexport const hostShaderGrid = (node: ShaderGridNode): HostedShaderGrid => {\n const u = createUniforms();\n const material = gridMaterial(u);\n writeUniforms(u, material, node);\n const mesh = new Mesh(new PlaneGeometry(...node.size), material);\n // The shader moves vertices (infinite, following): the plane's own bounds mean nothing.\n mesh.frustumCulled = false;\n const place = (next: ShaderGridNode): void => {\n mesh.position.fromArray(next.position);\n mesh.rotation.fromArray([...next.rotation]);\n };\n place(node);\n mesh.onBeforeRender = (_renderer, _scene, camera) => {\n plane.setFromNormalAndCoplanarPoint(up, origin).applyMatrix4(mesh.matrixWorld);\n plane.projectPoint(\n cameraPosition.setFromMatrixPosition(camera.matrixWorld),\n u.cameraOnPlane.value,\n );\n u.planeOrigin.value.set(0, 0, 0).applyMatrix4(mesh.matrixWorld);\n };\n return {\n object: mesh,\n update: (previous, next) => {\n if (previous.size[0] !== next.size[0] || previous.size[1] !== next.size[1]) {\n mesh.geometry.dispose();\n mesh.geometry = new PlaneGeometry(...next.size);\n }\n writeUniforms(u, material, next);\n place(next);\n },\n dispose: () => {\n mesh.geometry.dispose();\n material.dispose();\n },\n };\n};\n",
7
+ "/**\n * The WebGPU kit's sky: three.js's `SkyMesh` (`addons/objects/SkyMesh.js` at 0.186.1), the\n * same Preetham sky as the WebGL kit's `Sky`, written in TSL. Never picked.\n *\n * @module\n */\nimport { SkyMesh } from 'three/addons/objects/SkyMesh.js';\n\nimport type { Sky } from '../kernel/description.ts';\nimport type { HostedSky } from '../kernel/renderKit.ts';\n\n/** Writes a sky's description into the mesh's uniforms and scale. */\nconst writeSky = (mesh: SkyMesh, description: Sky): void => {\n mesh.turbidity.value = description.turbidity;\n mesh.rayleigh.value = description.rayleigh;\n mesh.mieCoefficient.value = description.mieCoefficient;\n mesh.mieDirectionalG.value = description.mieDirectionalG;\n mesh.cloudCoverage.value = description.clouds;\n mesh.sunPosition.value.fromArray(description.sunPosition);\n mesh.scale.setScalar(description.distance);\n};\n\n/** Creates the sky mesh; the scene graph adds it and writes its description. */\nexport const hostSky = (): HostedSky => {\n const mesh = new SkyMesh();\n mesh.raycast = () => undefined;\n return {\n object: mesh,\n update: (description) => writeSky(mesh, description),\n dispose: () => {\n mesh.geometry.dispose();\n mesh.material.dispose();\n },\n };\n};\n",
8
+ "/**\n * The WebGPU render kit: the scene's shaded parts (the sky, the shader grid, contact\n * shadows) written in TSL, for three.js's `WebGPURenderer` on either of its backends.\n * The Foldkit adapter's `webgpuRendererFactory` carries it.\n *\n * @module\n */\nimport type { RenderKit } from '../kernel/renderKit.ts';\nimport { createContactShadows } from './contactShadows.ts';\nimport { hostShaderGrid } from './shaderGrid.ts';\nimport { hostSky } from './sky.ts';\n\nexport const webgpuKit: RenderKit = {\n // troika's labels are WebGL materials; real WebGPU rejects their draw calls.\n drawsLabels: false,\n sky: hostSky,\n shaderGrid: hostShaderGrid,\n contactShadows: createContactShadows,\n};\n"
9
+ ],
10
+ "mappings": "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",
11
+ "debugId": "A69C3B6B3C01FA2F64756E2164756E21",
12
+ "names": []
13
+ }
@@ -0,0 +1,5 @@
1
+ import type { ShaderGridNode } from '../kernel/description.ts';
2
+ import type { HostedShaderGrid } from '../kernel/renderKit.ts';
3
+ /** The WebGPU kit's shader grid: a plane with the grid material, placed and updated. */
4
+ export declare const hostShaderGrid: (node: ShaderGridNode) => HostedShaderGrid;
5
+ //# sourceMappingURL=shaderGrid.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shaderGrid.d.ts","sourceRoot":"","sources":["../../src/webgpu/shaderGrid.ts"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EAAgB,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC7E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAkF/D,wFAAwF;AACxF,eAAO,MAAM,cAAc,SAAU,cAAc,KAAG,gBAmCrD,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { HostedSky } from '../kernel/renderKit.ts';
2
+ /** Creates the sky mesh; the scene graph adds it and writes its description. */
3
+ export declare const hostSky: () => HostedSky;
4
+ //# sourceMappingURL=sky.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sky.d.ts","sourceRoot":"","sources":["../../src/webgpu/sky.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAaxD,gFAAgF;AAChF,eAAO,MAAM,OAAO,QAAO,SAW1B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,84 @@
1
1
  {
2
2
  "name": "@opsydyn/effect-three",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Effect-native declarative Three.js scenes: scoped resources, keyed reconciliation, testable frame loop.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/opsydyn/fold-kit-experiments.git",
9
+ "directory": "packages/effect-three"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "src",
14
+ "README.md",
15
+ "CHANGELOG.md",
16
+ "LICENSE",
17
+ "!dist/.tsbuildinfo",
18
+ "!dist/api-extractor",
19
+ "!dist/pack"
20
+ ],
21
+ "type": "module",
22
+ "sideEffects": false,
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "bun": "./src/index.ts",
27
+ "default": "./dist/index.js"
28
+ },
29
+ "./kernel": {
30
+ "types": "./dist/kernel/index.d.ts",
31
+ "bun": "./src/kernel/index.ts",
32
+ "default": "./dist/kernel/index.js"
33
+ },
34
+ "./webgl": {
35
+ "types": "./dist/webgl/index.d.ts",
36
+ "bun": "./src/webgl/index.ts",
37
+ "default": "./dist/webgl/index.js"
38
+ },
39
+ "./webgpu": {
40
+ "types": "./dist/webgpu/index.d.ts",
41
+ "bun": "./src/webgpu/index.ts",
42
+ "default": "./dist/webgpu/index.js"
43
+ },
44
+ "./package.json": "./package.json"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "scripts": {
50
+ "typecheck": "tsc -b && tsc -p tsconfig.test.json",
51
+ "lint": "bunx --bun oxlint --config oxlint.strict.ts",
52
+ "check:boundaries": "bun scripts/check-boundaries.ts",
53
+ "check:single-effect": "bun scripts/check-single-effect.ts",
54
+ "check": "bun run lint && bun run check:boundaries && bun run check:single-effect",
55
+ "test": "bun test --pass-with-no-tests ./src ./test --coverage --coverage-reporter=lcov --coverage-dir=coverage && bun scripts/check-coverage.ts coverage/lcov.info",
56
+ "build": "tsc -b && bun build ./src/index.ts ./src/kernel/index.ts ./src/webgl/index.ts ./src/webgpu/index.ts --root ./src --outdir ./dist --target browser --packages external --splitting --sourcemap=linked",
57
+ "bench": "bun bench/index.ts",
58
+ "bench:check": "bun bench/index.ts --check",
59
+ "api:report": "api-extractor run --local",
60
+ "api:check": "api-extractor run",
61
+ "pack:check": "bun run build && publint && rm -rf dist/pack && bun pm pack --destination dist/pack && attw dist/pack/*.tgz --profile esm-only"
62
+ },
63
+ "devDependencies": {
64
+ "@arethetypeswrong/cli": "0.18.5",
65
+ "@microsoft/api-extractor": "7.59.4",
66
+ "@opsydyn/oxlint-effect": "2.0.1",
67
+ "@types/bun": "1.4.3",
68
+ "@types/pngjs": "6.0.5",
69
+ "@types/three": "0.186.0",
70
+ "effect": "4.0.0",
71
+ "oxlint": "1.88.0",
72
+ "oxlint-tsgolint": "7.0.2004",
73
+ "pixelmatch": "8.0.0",
74
+ "playwright": "1.64.1",
75
+ "pngjs": "7.0.0",
76
+ "publint": "0.3.25",
77
+ "three": "0.186.1",
78
+ "typescript": "7.0.2"
79
+ },
80
+ "peerDependencies": {
81
+ "effect": "^4.0.0",
82
+ "three": ">=0.186.0 <0.187.0"
83
+ }
84
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Effect-native declarative Three.js scenes.
3
+ *
4
+ * - Authoring: `scene`, `mesh`, `group`, `box`, `standard`, `spin` and friends.
5
+ * - Runtime: `SceneRuntime.make` inside a `Scope`, with `ResourceRegistry` and
6
+ * `FrameSource` provided by the application.
7
+ *
8
+ * @module
9
+ */
10
+ export * from './kernel/index.ts';
11
+ export * from './runtime/index.ts';
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Clip playback as a pure function of scene time.
3
+ *
4
+ * @module
5
+ */
6
+ import type { Animation } from './description.ts';
7
+
8
+ /** Just under the clip's end: three wraps a looping action at exactly its duration. */
9
+ const endEpsilon = 1e-6;
10
+
11
+ /**
12
+ * The clip time to show `elapsed` seconds after playback started: wrapped for looping
13
+ * clips (backwards too, for negative speeds), held at the last frame otherwise.
14
+ */
15
+ export const clipTime = (animation: Animation, duration: number, elapsed: number): number => {
16
+ if (duration <= 0) return 0;
17
+ const t = elapsed * animation.speed;
18
+ if (animation.loop) return ((t % duration) + duration) % duration;
19
+ return Math.min(Math.max(t, 0), duration - endEpsilon);
20
+ };
21
+
22
+ /** Whether playback still changes the pose: always for loops, until the end otherwise. */
23
+ export const clipRunning = (animation: Animation, duration: number, elapsed: number): boolean =>
24
+ animation.loop || (animation.speed !== 0 && Math.abs(elapsed * animation.speed) < duration);
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Behaviours own animated properties (PLAN D4). Their output is a pure function of
3
+ * elapsed scene time, so a frame's result doesn't depend on how many frames ran before
4
+ * it: deterministic and frame-rate independent.
5
+ *
6
+ * @module
7
+ */
8
+ import type { Euler } from 'three';
9
+
10
+ import type { Rotation } from './description.ts';
11
+
12
+ /** The spin angle in radians after `elapsedSeconds`. */
13
+ export const spinAngle = (
14
+ spin: Extract<Rotation, { readonly _tag: 'Spin' }>,
15
+ elapsedSeconds: number,
16
+ ): number => spin.phase + spin.radiansPerSecond * elapsedSeconds;
17
+
18
+ /** Writes a rotation into a three.js Euler without allocating. */
19
+ export const writeRotation = (target: Euler, rotation: Rotation, elapsedSeconds: number): void => {
20
+ if (rotation._tag === 'Fixed') {
21
+ target.set(rotation.euler[0], rotation.euler[1], rotation.euler[2]);
22
+ return;
23
+ }
24
+ const angle = spinAngle(rotation, elapsedSeconds);
25
+ switch (rotation.axis) {
26
+ case 'x':
27
+ target.set(angle, 0, 0);
28
+ return;
29
+ case 'y':
30
+ target.set(0, angle, 0);
31
+ return;
32
+ case 'z':
33
+ target.set(0, 0, angle);
34
+ return;
35
+ }
36
+ };
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Rigid bodies for a scene graph's nodes: a physics world kept in step with the
3
+ * description, advanced on scene time, its poses written back to the nodes (or, for an
4
+ * instances node, to each instance's matrix).
5
+ *
6
+ * Behavioural reference: react-three-rapier at `ae5c3fe`, `src/components/Physics.tsx`:
7
+ * fixed steps from an accumulator, the frame delta clamped to 0.5 s, nothing stepped while
8
+ * paused, and each body placed between its last two simulated poses (`interpolate`),
9
+ * converted from world space into its parent's space.
10
+ *
11
+ * @module
12
+ */
13
+ import { Euler, Quaternion, type Scene } from 'three';
14
+
15
+ import { bodyId, type BodySource } from './bodySources.ts';
16
+ import type { Body, Joint, Physics, Vec3 } from './description.ts';
17
+ import { bodyEquals, collidersEqual, jointEquals, physicsEquals } from './equality.ts';
18
+ import type { BodyPose, PhysicsEngine, PhysicsJoint, PhysicsWorld, Quat } from './physics.ts';
19
+ import { createPhysicsDebug } from './physicsDebug.ts';
20
+
21
+ /** Two bodies began (`started`) or stopped touching: their nodes' keys, and instances'. */
22
+ export type BodyCollision = {
23
+ readonly a: string;
24
+ readonly aInstance?: string;
25
+ /** The part of body `a` that touched (a mesh's or collider's name), when it has one. */
26
+ readonly aPart?: string;
27
+ readonly b: string;
28
+ readonly bInstance?: string;
29
+ readonly bPart?: string;
30
+ readonly started: boolean;
31
+ };
32
+
33
+ /**
34
+ * Two bodies pushing on each other harder than a threshold: their nodes' keys (and
35
+ * instances'), and Rapier's contact force payload, as react-three-rapier's
36
+ * `onContactForce`.
37
+ */
38
+ export type BodyContactForce = {
39
+ readonly a: string;
40
+ readonly aInstance?: string;
41
+ readonly aPart?: string;
42
+ readonly b: string;
43
+ readonly bInstance?: string;
44
+ readonly bPart?: string;
45
+ readonly totalForce: Vec3;
46
+ readonly totalForceMagnitude: number;
47
+ readonly maxForceDirection: Vec3;
48
+ readonly maxForceMagnitude: number;
49
+ };
50
+
51
+ type Hosted = {
52
+ source: BodySource;
53
+ /** What its colliders were made from; a change makes them again. */
54
+ signature: string;
55
+ previous: BodyPose;
56
+ current: BodyPose;
57
+ /** Where it was last placed, between `previous` and `current`. */
58
+ shown: BodyPose;
59
+ };
60
+
61
+ export type BodyHost = {
62
+ /** Brings the world in line with the description's physics and its bodies. */
63
+ readonly sync: (physics: Physics | null, sources: Iterable<BodySource>) => void;
64
+ /** Steps the world to scene time `elapsed` and moves dynamic bodies to their poses. */
65
+ readonly tick: (elapsed: number, animating: boolean) => void;
66
+ /** Whether the simulation still moves anything. */
67
+ readonly needsFrame: () => boolean;
68
+ /** Collisions since the last call. */
69
+ readonly drainCollisions: () => ReadonlyArray<BodyCollision>;
70
+ /** Contact forces over their thresholds since the last call. */
71
+ readonly drainContactForces: () => ReadonlyArray<BodyContactForce>;
72
+ readonly dispose: () => void;
73
+ };
74
+
75
+ const from = new Quaternion();
76
+ const to = new Quaternion();
77
+ const euler = new Euler();
78
+
79
+ const quatOf = (angles: Vec3): Quat => {
80
+ from.setFromEuler(euler.fromArray([...angles]));
81
+ return [from.x, from.y, from.z, from.w];
82
+ };
83
+
84
+ /** A joint for the engine: a fixed joint's frames turned into quaternions. */
85
+ const physicsJoint = ({ anchorA, anchorB, kind, contacts }: Joint): PhysicsJoint => ({
86
+ anchorA,
87
+ anchorB,
88
+ contacts,
89
+ kind:
90
+ kind._tag === 'Fixed'
91
+ ? { _tag: 'Fixed', frameA: quatOf(kind.frameA), frameB: quatOf(kind.frameB) }
92
+ : kind,
93
+ });
94
+
95
+ /** The pose `alpha` of the way from `a` to `b`: positions lerped, rotations slerped. */
96
+ const between = (a: BodyPose, b: BodyPose, alpha: number): BodyPose => {
97
+ if (alpha >= 1) return b;
98
+ from.fromArray(a.quaternion);
99
+ to.fromArray(b.quaternion);
100
+ from.slerp(to, alpha);
101
+ return {
102
+ position: [
103
+ a.position[0] + (b.position[0] - a.position[0]) * alpha,
104
+ a.position[1] + (b.position[1] - a.position[1]) * alpha,
105
+ a.position[2] + (b.position[2] - a.position[2]) * alpha,
106
+ ],
107
+ quaternion: [from.x, from.y, from.z, from.w],
108
+ };
109
+ };
110
+
111
+ /** A change that needs a new body: another holder or kind, or colliders made otherwise. */
112
+ const remade = (hosted: Hosted, source: BodySource, signature: string): boolean =>
113
+ hosted.source.holder !== source.holder ||
114
+ hosted.source.body.collider !== source.body.collider ||
115
+ !collidersEqual(hosted.source.body.colliders, source.body.colliders) ||
116
+ hosted.signature !== signature;
117
+
118
+ /** Body settings other than its impulse differ. */
119
+ const resettled = (a: Body, b: Body): boolean =>
120
+ !bodyEquals({ ...a, impulse: null }, { ...b, impulse: null });
121
+
122
+ /** Bodies `a` and `b` (by id) named by their nodes, instances and parts, where they have them. */
123
+ const named = (
124
+ a: BodySource | undefined,
125
+ b: BodySource | undefined,
126
+ ids: {
127
+ readonly a: string;
128
+ readonly b: string;
129
+ readonly aPart?: string;
130
+ readonly bPart?: string;
131
+ },
132
+ ): Pick<BodyCollision, 'a' | 'aInstance' | 'aPart' | 'b' | 'bInstance' | 'bPart'> => {
133
+ const aInstance = a?.instance;
134
+ const bInstance = b?.instance;
135
+ return {
136
+ a: a?.key ?? ids.a,
137
+ ...(aInstance === undefined ? {} : { aInstance }),
138
+ ...(ids.aPart === undefined ? {} : { aPart: ids.aPart }),
139
+ b: b?.key ?? ids.b,
140
+ ...(bInstance === undefined ? {} : { bInstance }),
141
+ ...(ids.bPart === undefined ? {} : { bPart: ids.bPart }),
142
+ };
143
+ };
144
+
145
+ /** Creates the body host for a scene. Without an engine, bodies are ignored. */
146
+ export const createBodyHost = (engine: PhysicsEngine | undefined, scene: Scene): BodyHost => {
147
+ let world: PhysicsWorld | undefined;
148
+ let settings: Physics | null = null;
149
+ const hosted = new Map<string, Hosted>();
150
+ /** Joints made, by key. */
151
+ const joined = new Map<string, Joint>();
152
+ /** Bodies (re)made by the current sync: their joints went with the old ones. */
153
+ const fresh = new Set<string>();
154
+ let collisions: Array<BodyCollision> = [];
155
+ let contactForces: Array<BodyContactForce> = [];
156
+ const debug = createPhysicsDebug(scene);
157
+ let last: number | undefined;
158
+ let accumulator = 0;
159
+
160
+ /** Draws the world's lines while the description asks for them. */
161
+ const drawDebug = (): void => {
162
+ if (world !== undefined && settings?.debug === true) debug.show(world.debugLines());
163
+ else debug.hide();
164
+ };
165
+
166
+ const dispose = (): void => {
167
+ debug.hide();
168
+ world?.dispose();
169
+ world = undefined;
170
+ settings = null;
171
+ hosted.clear();
172
+ joined.clear();
173
+ accumulator = 0;
174
+ last = undefined;
175
+ };
176
+
177
+ const add = (active: PhysicsWorld, source: BodySource, signature: string): void => {
178
+ const pose = source.pose();
179
+ active.add(source.id, source.body, source.colliders(), pose);
180
+ if (source.body.impulse !== null) active.applyImpulse(source.id, source.body.impulse.impulse);
181
+ hosted.set(source.id, { source, signature, previous: pose, current: pose, shown: pose });
182
+ fresh.add(source.id);
183
+ };
184
+
185
+ const syncBody = (active: PhysicsWorld, source: BodySource): void => {
186
+ const existing = hosted.get(source.id);
187
+ const signature = source.signature();
188
+ if (existing === undefined || remade(existing, source, signature)) {
189
+ if (existing !== undefined) active.remove(source.id);
190
+ add(active, source, signature);
191
+ return;
192
+ }
193
+ const before = existing.source.body;
194
+ const { body } = source;
195
+ if (resettled(before, body)) active.update(source.id, body);
196
+ if (body.type !== 'kinematic' && existing.source.placement !== source.placement) {
197
+ const pose = source.described();
198
+ active.teleport(source.id, pose);
199
+ existing.previous = pose;
200
+ existing.current = pose;
201
+ existing.shown = pose;
202
+ source.place(pose);
203
+ } else if (body.type === 'dynamic') {
204
+ // A description change elsewhere (an instance's colour, say) may have rewritten it.
205
+ source.place(existing.shown);
206
+ }
207
+ if (body.impulse !== null && body.impulse.id !== before.impulse?.id) {
208
+ active.applyImpulse(source.id, body.impulse.impulse);
209
+ }
210
+ existing.source = source;
211
+ };
212
+
213
+ /** Makes each joint whose bodies both exist, again when either was remade or it changed. */
214
+ const syncJoints = (active: PhysicsWorld, joints: ReadonlyArray<Joint>): void => {
215
+ const seen = new Set<string>();
216
+ for (const joint of joints) {
217
+ seen.add(joint.key);
218
+ const a = bodyId(joint.a.key, joint.a.instance);
219
+ const b = bodyId(joint.b.key, joint.b.instance);
220
+ const ready = hosted.has(a) && hosted.has(b);
221
+ const made = joined.get(joint.key);
222
+ if (
223
+ made !== undefined &&
224
+ ready &&
225
+ !fresh.has(a) &&
226
+ !fresh.has(b) &&
227
+ jointEquals(made, joint)
228
+ ) {
229
+ continue;
230
+ }
231
+ if (made !== undefined) {
232
+ active.removeJoint(joint.key);
233
+ joined.delete(joint.key);
234
+ }
235
+ if (ready) {
236
+ active.addJoint(joint.key, physicsJoint(joint), a, b);
237
+ joined.set(joint.key, joint);
238
+ }
239
+ }
240
+ for (const key of joined.keys()) {
241
+ if (seen.has(key)) continue;
242
+ active.removeJoint(key);
243
+ joined.delete(key);
244
+ }
245
+ };
246
+
247
+ const sync: BodyHost['sync'] = (physics, sources) => {
248
+ if (physics === null || engine === undefined) {
249
+ dispose();
250
+ return;
251
+ }
252
+ if (world === undefined) world = engine.createWorld(physics);
253
+ // Joints are made below, not configured.
254
+ else if (!physicsEquals(settings && { ...settings, joints: physics.joints }, physics)) {
255
+ world.configure(physics);
256
+ }
257
+ settings = physics;
258
+ scene.updateMatrixWorld();
259
+ fresh.clear();
260
+ const seen = new Set<string>();
261
+ for (const source of sources) {
262
+ seen.add(source.id);
263
+ syncBody(world, source);
264
+ }
265
+ for (const id of hosted.keys()) {
266
+ if (seen.has(id)) continue;
267
+ world.remove(id);
268
+ hosted.delete(id);
269
+ }
270
+ syncJoints(world, physics.joints);
271
+ drawDebug();
272
+ };
273
+
274
+ const kinematicTargets = (active: PhysicsWorld): void => {
275
+ for (const [id, { source }] of hosted) {
276
+ if (source.body.type === 'kinematic') active.moveKinematic(id, source.pose());
277
+ }
278
+ };
279
+
280
+ const step = (active: PhysicsWorld, seconds: number): void => {
281
+ for (const body of hosted.values()) body.previous = body.current;
282
+ kinematicTargets(active);
283
+ for (const collision of active.step(seconds)) {
284
+ collisions.push({
285
+ ...named(hosted.get(collision.a)?.source, hosted.get(collision.b)?.source, collision),
286
+ started: collision.started,
287
+ });
288
+ }
289
+ for (const force of active.contactForces()) {
290
+ const { a, b, aPart, bPart, ...payload } = force;
291
+ contactForces.push({
292
+ ...named(hosted.get(a)?.source, hosted.get(b)?.source, force),
293
+ ...payload,
294
+ });
295
+ }
296
+ for (const [id, body] of hosted) body.current = active.pose(id) ?? body.current;
297
+ };
298
+
299
+ const tick: BodyHost['tick'] = (elapsed, animating) => {
300
+ const delta = last === undefined ? 0 : Math.min(0.5, Math.max(0, elapsed - last));
301
+ last = elapsed;
302
+ if (world === undefined || settings === null || settings.paused || !animating) return;
303
+ const timeStep = settings.timeStep;
304
+ accumulator += delta;
305
+ scene.updateMatrixWorld();
306
+ while (accumulator >= timeStep) {
307
+ step(world, timeStep);
308
+ accumulator -= timeStep;
309
+ }
310
+ const alpha = settings.interpolate ? accumulator / timeStep : 1;
311
+ for (const body of hosted.values()) {
312
+ if (body.source.body.type === 'dynamic') {
313
+ body.shown = between(body.previous, body.current, alpha);
314
+ body.source.place(body.shown);
315
+ }
316
+ }
317
+ if (settings.debug) drawDebug();
318
+ };
319
+
320
+ return {
321
+ sync,
322
+ tick,
323
+ needsFrame: () => world !== undefined && settings !== null && !settings.paused && world.awake(),
324
+ drainCollisions: () => {
325
+ if (collisions.length === 0) return collisions;
326
+ const drained = collisions;
327
+ collisions = [];
328
+ return drained;
329
+ },
330
+ drainContactForces: () => {
331
+ if (contactForces.length === 0) return contactForces;
332
+ const drained = contactForces;
333
+ contactForces = [];
334
+ return drained;
335
+ },
336
+ dispose,
337
+ };
338
+ };