@tinypoly/gpu-renderer 0.0.0-stage → 0.0.1

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 (204) hide show
  1. package/ARCHITECTURE.md +339 -0
  2. package/LICENSE +21 -0
  3. package/README.md +284 -2
  4. package/dist/accumulator.d.ts +27 -0
  5. package/dist/accumulator.d.ts.map +1 -0
  6. package/dist/accumulator.js +57 -0
  7. package/dist/accumulator.js.map +1 -0
  8. package/dist/bvh.d.ts +26 -0
  9. package/dist/bvh.d.ts.map +1 -0
  10. package/dist/bvh.js +296 -0
  11. package/dist/bvh.js.map +1 -0
  12. package/dist/bvhBuilder.d.ts +72 -0
  13. package/dist/bvhBuilder.d.ts.map +1 -0
  14. package/dist/bvhBuilder.js +126 -0
  15. package/dist/bvhBuilder.js.map +1 -0
  16. package/dist/bvhWorker.d.ts +2 -0
  17. package/dist/bvhWorker.d.ts.map +1 -0
  18. package/dist/bvhWorker.js +8 -0
  19. package/dist/bvhWorker.js.map +1 -0
  20. package/dist/camera.d.ts +29 -0
  21. package/dist/camera.d.ts.map +1 -0
  22. package/dist/camera.js +48 -0
  23. package/dist/camera.js.map +1 -0
  24. package/dist/canvas.d.ts +9 -0
  25. package/dist/canvas.d.ts.map +1 -0
  26. package/dist/canvas.js +11 -0
  27. package/dist/canvas.js.map +1 -0
  28. package/dist/denoise.d.ts +69 -0
  29. package/dist/denoise.d.ts.map +1 -0
  30. package/dist/denoise.js +455 -0
  31. package/dist/denoise.js.map +1 -0
  32. package/dist/device.d.ts +37 -0
  33. package/dist/device.d.ts.map +1 -0
  34. package/dist/device.js +52 -0
  35. package/dist/device.js.map +1 -0
  36. package/dist/dispatchScheduler.d.ts +43 -0
  37. package/dist/dispatchScheduler.d.ts.map +1 -0
  38. package/dist/dispatchScheduler.js +49 -0
  39. package/dist/dispatchScheduler.js.map +1 -0
  40. package/dist/environment.d.ts +39 -0
  41. package/dist/environment.d.ts.map +1 -0
  42. package/dist/environment.js +168 -0
  43. package/dist/environment.js.map +1 -0
  44. package/dist/frameLayout.d.ts +4 -0
  45. package/dist/frameLayout.d.ts.map +1 -0
  46. package/dist/frameLayout.js +58 -0
  47. package/dist/frameLayout.js.map +1 -0
  48. package/dist/geometry.d.ts +75 -0
  49. package/dist/geometry.d.ts.map +1 -0
  50. package/dist/geometry.js +799 -0
  51. package/dist/geometry.js.map +1 -0
  52. package/dist/index.d.ts +27 -0
  53. package/dist/index.d.ts.map +1 -0
  54. package/dist/index.js +19 -0
  55. package/dist/index.js.map +1 -0
  56. package/dist/lights.d.ts +64 -0
  57. package/dist/lights.d.ts.map +1 -0
  58. package/dist/lights.js +264 -0
  59. package/dist/lights.js.map +1 -0
  60. package/dist/materialLayout.d.ts +143 -0
  61. package/dist/materialLayout.d.ts.map +1 -0
  62. package/dist/materialLayout.js +297 -0
  63. package/dist/materialLayout.js.map +1 -0
  64. package/dist/math.d.ts +10 -0
  65. package/dist/math.d.ts.map +1 -0
  66. package/dist/math.js +40 -0
  67. package/dist/math.js.map +1 -0
  68. package/dist/pathTracePass.d.ts +27 -0
  69. package/dist/pathTracePass.d.ts.map +1 -0
  70. package/dist/pathTracePass.js +83 -0
  71. package/dist/pathTracePass.js.map +1 -0
  72. package/dist/planarCapture.d.ts +38 -0
  73. package/dist/planarCapture.d.ts.map +1 -0
  74. package/dist/planarCapture.js +163 -0
  75. package/dist/planarCapture.js.map +1 -0
  76. package/dist/presentPass.d.ts +31 -0
  77. package/dist/presentPass.d.ts.map +1 -0
  78. package/dist/presentPass.js +78 -0
  79. package/dist/presentPass.js.map +1 -0
  80. package/dist/renderer.d.ts +209 -0
  81. package/dist/renderer.d.ts.map +1 -0
  82. package/dist/renderer.js +847 -0
  83. package/dist/renderer.js.map +1 -0
  84. package/dist/sceneAssets.d.ts +24 -0
  85. package/dist/sceneAssets.d.ts.map +1 -0
  86. package/dist/sceneAssets.js +77 -0
  87. package/dist/sceneAssets.js.map +1 -0
  88. package/dist/sceneUpload.d.ts +61 -0
  89. package/dist/sceneUpload.d.ts.map +1 -0
  90. package/dist/sceneUpload.js +98 -0
  91. package/dist/sceneUpload.js.map +1 -0
  92. package/dist/shaderModule.d.ts +7 -0
  93. package/dist/shaderModule.d.ts.map +1 -0
  94. package/dist/shaderModule.js +19 -0
  95. package/dist/shaderModule.js.map +1 -0
  96. package/dist/shaderProgram.d.ts +124 -0
  97. package/dist/shaderProgram.d.ts.map +1 -0
  98. package/dist/shaderProgram.js +1030 -0
  99. package/dist/shaderProgram.js.map +1 -0
  100. package/dist/structLayout.d.ts +28 -0
  101. package/dist/structLayout.d.ts.map +1 -0
  102. package/dist/structLayout.js +58 -0
  103. package/dist/structLayout.js.map +1 -0
  104. package/dist/texturePack.d.ts +54 -0
  105. package/dist/texturePack.d.ts.map +1 -0
  106. package/dist/texturePack.js +86 -0
  107. package/dist/texturePack.js.map +1 -0
  108. package/dist/upstream.d.ts +7 -0
  109. package/dist/upstream.d.ts.map +1 -0
  110. package/dist/upstream.js +11 -0
  111. package/dist/upstream.js.map +1 -0
  112. package/dist/volumetric.d.ts +10 -0
  113. package/dist/volumetric.d.ts.map +1 -0
  114. package/dist/volumetric.js +34 -0
  115. package/dist/volumetric.js.map +1 -0
  116. package/dist/wgsl/bsdf.wgsl.d.ts +13 -0
  117. package/dist/wgsl/bsdf.wgsl.d.ts.map +1 -0
  118. package/dist/wgsl/bsdf.wgsl.js +619 -0
  119. package/dist/wgsl/bsdf.wgsl.js.map +1 -0
  120. package/dist/wgsl/bvh.wgsl.d.ts +3 -0
  121. package/dist/wgsl/bvh.wgsl.d.ts.map +1 -0
  122. package/dist/wgsl/bvh.wgsl.js +218 -0
  123. package/dist/wgsl/bvh.wgsl.js.map +1 -0
  124. package/dist/wgsl/common.wgsl.d.ts +3 -0
  125. package/dist/wgsl/common.wgsl.d.ts.map +1 -0
  126. package/dist/wgsl/common.wgsl.js +193 -0
  127. package/dist/wgsl/common.wgsl.js.map +1 -0
  128. package/dist/wgsl/environment.wgsl.d.ts +10 -0
  129. package/dist/wgsl/environment.wgsl.d.ts.map +1 -0
  130. package/dist/wgsl/environment.wgsl.js +145 -0
  131. package/dist/wgsl/environment.wgsl.js.map +1 -0
  132. package/dist/wgsl/lights.wgsl.d.ts +10 -0
  133. package/dist/wgsl/lights.wgsl.d.ts.map +1 -0
  134. package/dist/wgsl/lights.wgsl.js +434 -0
  135. package/dist/wgsl/lights.wgsl.js.map +1 -0
  136. package/dist/wgsl/pathTrace.wgsl.d.ts +14 -0
  137. package/dist/wgsl/pathTrace.wgsl.d.ts.map +1 -0
  138. package/dist/wgsl/pathTrace.wgsl.js +1020 -0
  139. package/dist/wgsl/pathTrace.wgsl.js.map +1 -0
  140. package/dist/wgsl/present.wgsl.d.ts +7 -0
  141. package/dist/wgsl/present.wgsl.d.ts.map +1 -0
  142. package/dist/wgsl/present.wgsl.js +154 -0
  143. package/dist/wgsl/present.wgsl.js.map +1 -0
  144. package/dist/wgsl/random.wgsl.d.ts +9 -0
  145. package/dist/wgsl/random.wgsl.d.ts.map +1 -0
  146. package/dist/wgsl/random.wgsl.js +91 -0
  147. package/dist/wgsl/random.wgsl.js.map +1 -0
  148. package/dist/wgsl/sampling.wgsl.d.ts +3 -0
  149. package/dist/wgsl/sampling.wgsl.d.ts.map +1 -0
  150. package/dist/wgsl/sampling.wgsl.js +31 -0
  151. package/dist/wgsl/sampling.wgsl.js.map +1 -0
  152. package/dist/wgsl/shaderSupport.wgsl.d.ts +6 -0
  153. package/dist/wgsl/shaderSupport.wgsl.d.ts.map +1 -0
  154. package/dist/wgsl/shaderSupport.wgsl.js +120 -0
  155. package/dist/wgsl/shaderSupport.wgsl.js.map +1 -0
  156. package/dist/wgsl/surface.wgsl.d.ts +10 -0
  157. package/dist/wgsl/surface.wgsl.d.ts.map +1 -0
  158. package/dist/wgsl/surface.wgsl.js +794 -0
  159. package/dist/wgsl/surface.wgsl.js.map +1 -0
  160. package/dist/wgsl/texture.wgsl.d.ts +11 -0
  161. package/dist/wgsl/texture.wgsl.d.ts.map +1 -0
  162. package/dist/wgsl/texture.wgsl.js +132 -0
  163. package/dist/wgsl/texture.wgsl.js.map +1 -0
  164. package/package.json +77 -3
  165. package/src/accumulator.ts +64 -0
  166. package/src/bvh.ts +336 -0
  167. package/src/bvhBuilder.ts +178 -0
  168. package/src/bvhWorker.ts +13 -0
  169. package/src/camera.ts +77 -0
  170. package/src/canvas.ts +13 -0
  171. package/src/denoise.ts +514 -0
  172. package/src/device.ts +81 -0
  173. package/src/dispatchScheduler.ts +71 -0
  174. package/src/environment.ts +233 -0
  175. package/src/frameLayout.ts +59 -0
  176. package/src/geometry.ts +1067 -0
  177. package/src/index.ts +64 -0
  178. package/src/lights.ts +365 -0
  179. package/src/materialLayout.ts +408 -0
  180. package/src/math.ts +48 -0
  181. package/src/pathTracePass.ts +97 -0
  182. package/src/planarCapture.ts +225 -0
  183. package/src/presentPass.ts +99 -0
  184. package/src/renderer.ts +1145 -0
  185. package/src/sceneAssets.ts +118 -0
  186. package/src/sceneUpload.ts +185 -0
  187. package/src/shaderModule.ts +19 -0
  188. package/src/shaderProgram.ts +1370 -0
  189. package/src/structLayout.ts +93 -0
  190. package/src/texturePack.ts +152 -0
  191. package/src/upstream.ts +56 -0
  192. package/src/volumetric.ts +42 -0
  193. package/src/wgsl/bsdf.wgsl.ts +618 -0
  194. package/src/wgsl/bvh.wgsl.ts +217 -0
  195. package/src/wgsl/common.wgsl.ts +193 -0
  196. package/src/wgsl/environment.wgsl.ts +144 -0
  197. package/src/wgsl/lights.wgsl.ts +433 -0
  198. package/src/wgsl/pathTrace.wgsl.ts +1019 -0
  199. package/src/wgsl/present.wgsl.ts +153 -0
  200. package/src/wgsl/random.wgsl.ts +90 -0
  201. package/src/wgsl/sampling.wgsl.ts +30 -0
  202. package/src/wgsl/shaderSupport.wgsl.ts +119 -0
  203. package/src/wgsl/surface.wgsl.ts +793 -0
  204. package/src/wgsl/texture.wgsl.ts +131 -0
@@ -0,0 +1,93 @@
1
+ /**
2
+ * One description of a struct, from which both the WGSL declaration and the TypeScript writer come. The kernel's
3
+ * `Material` and `Frame` structs have dozens of fields each; keeping their word offsets by hand in two languages is the
4
+ * kind of mistake that compiles on both sides and then reads the wrong float on the device, where nothing can be printed.
5
+ *
6
+ * The alignment rules are WGSL's own for the host-shareable types used here.
7
+ */
8
+ export type FieldType = "f32" | "u32" | "vec2u" | "vec4f" | "mat4f";
9
+
10
+ export interface StructField {
11
+ name: string;
12
+ type: FieldType;
13
+ /** One line of documentation, carried into the generated WGSL. */
14
+ doc?: string;
15
+ }
16
+
17
+ export interface StructLayout {
18
+ name: string;
19
+ /** The WGSL declaration. */
20
+ wgsl: string;
21
+ /** Size in 32 bit words, rounded up to the struct's alignment, which is also its array stride. */
22
+ words: number;
23
+ /** Where each field starts, in words. */
24
+ offsets: Record<string, number>;
25
+ types: Record<string, FieldType>;
26
+ }
27
+
28
+ const SPEC: Record<FieldType, { words: number; align: number; wgsl: string }> = {
29
+ f32: { words: 1, align: 1, wgsl: "f32" },
30
+ u32: { words: 1, align: 1, wgsl: "u32" },
31
+ vec2u: { words: 2, align: 2, wgsl: "vec2<u32>" },
32
+ vec4f: { words: 4, align: 4, wgsl: "vec4<f32>" },
33
+ mat4f: { words: 16, align: 4, wgsl: "mat4x4<f32>" },
34
+ };
35
+
36
+ const roundUp = (value: number, align: number) => Math.ceil(value / align) * align;
37
+
38
+ export function layoutStruct(name: string, fields: readonly StructField[]): StructLayout {
39
+ const offsets: Record<string, number> = {};
40
+ const types: Record<string, FieldType> = {};
41
+ const lines: string[] = [];
42
+ let at = 0;
43
+ let align = 1;
44
+
45
+ for (const field of fields) {
46
+ const spec = SPEC[field.type];
47
+
48
+ if (field.name in offsets) throw new Error(`Duplicate field ${field.name} in ${name}.`);
49
+
50
+ at = roundUp(at, spec.align);
51
+ offsets[field.name] = at;
52
+ types[field.name] = field.type;
53
+ at += spec.words;
54
+ align = Math.max(align, spec.align);
55
+
56
+ if (field.doc) lines.push(` // ${field.doc}`);
57
+
58
+ lines.push(` ${field.name}: ${spec.wgsl},`);
59
+ }
60
+
61
+ // A struct holding a vec4 is aligned to 16 bytes; anything smaller to 4, which a uniform still pads to 16.
62
+ const words = roundUp(at, Math.max(align, 4));
63
+
64
+ return { name, wgsl: `struct ${name} {\n${lines.join("\n")}\n};\n`, words, offsets, types };
65
+ }
66
+
67
+ /** Writes named values into a struct's words. Missing fields stay zero; unknown ones are an error. */
68
+ export function writeStruct(
69
+ layout: StructLayout,
70
+ target: ArrayBuffer,
71
+ wordOffset: number,
72
+ values: Record<string, number | readonly number[] | ArrayLike<number> | boolean>) {
73
+ const floats = new Float32Array(target);
74
+ const words = new Uint32Array(target);
75
+
76
+ for (const [name, value] of Object.entries(values)) {
77
+ const offset = layout.offsets[name];
78
+
79
+ if (offset === undefined) throw new Error(`${layout.name} has no field ${name}.`);
80
+
81
+ const at = wordOffset + offset;
82
+ const type = layout.types[name];
83
+
84
+ if (type === "u32" || type === "vec2u") {
85
+ if (typeof value === "number" || typeof value === "boolean") words[at] = Number(value) >>> 0;
86
+ else for (let i = 0; i < Math.min(value.length, SPEC[type].words); i++) words[at + i] = value[i] >>> 0;
87
+ } else if (typeof value === "number" || typeof value === "boolean") {
88
+ floats[at] = Number(value);
89
+ } else {
90
+ for (let i = 0; i < Math.min(value.length, SPEC[type].words); i++) floats[at + i] = value[i];
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,152 @@
1
+ import type { SerializedScene, SerializedTexture } from "./upstream.js";
2
+
3
+ /**
4
+ * Every texture of a scene in one storage buffer, led by a table saying where each one lives. Core WebGPU has no binding
5
+ * arrays and `texture_2d_array` wants one size and one format for every layer, which a Three.js scene does not provide,
6
+ * so the texels are read by hand. See the decision in ARCHITECTURE.md.
7
+ *
8
+ * Byte textures keep their eight bits per channel, four to a word, and are decoded from sRGB as they are read. Float
9
+ * textures keep all four, because their range is the reason they are float at all.
10
+ */
11
+ export interface PackedTextures {
12
+ /**
13
+ * One buffer for all of it: `TEXTURE_WORDS` words per texture first, as `TextureInfo` in `wgsl/common.wgsl.ts`, then
14
+ * the texels each entry points at. A byte texture takes one word per texel, RGBA in the low to high bytes; a float
15
+ * texture takes four, each the bits of one linear channel. Keeping the two in one binding is what holds the shader
16
+ * inside the eight storage buffers a WebGPU device has to offer.
17
+ */
18
+ data: Uint32Array;
19
+ count: number;
20
+ /** The index in the table of each texture, by the id the materials refer to it with. */
21
+ indexOf: Map<string, number>;
22
+ /** Where the words a caller appended begin, which is how the environment's distribution rides along. */
23
+ extraOffset: number;
24
+ }
25
+
26
+ /** Textures and raw words that are not part of the scene snapshot, such as the environment and its distribution. */
27
+ export interface TextureExtras {
28
+ textures?: readonly SerializedTexture[];
29
+ words?: Uint32Array;
30
+ }
31
+
32
+ /** Sixteen words per entry: see `TextureInfo` in `wgsl/common.wgsl.ts`. */
33
+ export const TEXTURE_WORDS = 16;
34
+
35
+ /** What a material's map index means when the material has no such map. */
36
+ export const NO_TEXTURE = 0xffffffff;
37
+
38
+ const TEXTURE_FLOAT = 1;
39
+ const TEXTURE_SRGB = 2;
40
+ const TEXTURE_NEAREST = 4;
41
+ const TEXTURE_FLIP_Y = 8;
42
+ /** An environment map whose mapping bends the view ray instead of mirroring it. */
43
+ const TEXTURE_REFRACTION = 16;
44
+
45
+ /** Three's own wrap constants, which the shader compares against. */
46
+ const REPEAT = 1000;
47
+ const MIRRORED_REPEAT = 1002;
48
+
49
+ /** How many UV sets the geometry carries, and so the highest channel a texture may ask for. */
50
+ export const UV_CHANNELS = 2;
51
+
52
+ const texelCount = (texture: SerializedTexture) => Math.max(0, texture.width) * Math.max(0, texture.height);
53
+
54
+ const isFloat = (texture: SerializedTexture) => texture.data instanceof Float32Array;
55
+
56
+ /**
57
+ * Whether the shader has to decode this texture's colour from sRGB. Only byte textures can still be encoded: a float
58
+ * texture was converted to linear when the scene was serialized, and had its colour space rewritten to say so.
59
+ */
60
+ const isEncoded = (texture: SerializedTexture) => !isFloat(texture) && texture.colorSpace === "srgb";
61
+
62
+ /**
63
+ * Where everything will go, before anything is written: which index each texture gets and where the words after the
64
+ * texels begin. Materials and lights point into that tail by absolute offset, so they need it before the buffer exists.
65
+ */
66
+ export interface TexturePlan {
67
+ textures: SerializedTexture[];
68
+ indexOf: Map<string, number>;
69
+ tableWords: number;
70
+ /** Where the words appended after the texels begin. */
71
+ extraOffset: number;
72
+ }
73
+
74
+ export function planTextures(scene: SerializedScene, extraTextures: readonly SerializedTexture[] = []): TexturePlan {
75
+ const seen = new Set<string>();
76
+
77
+ // A texture id appears once, whoever refers to it; the first copy wins.
78
+ const textures = [...scene.textures ?? [], ...extraTextures]
79
+ .filter(texture => texelCount(texture) > 0 && texture.data.length >= texelCount(texture) * 4)
80
+ .filter(texture => !seen.has(texture.id) && !!seen.add(texture.id));
81
+
82
+ const indexOf = new Map(textures.map((texture, index) => [texture.id, index]));
83
+ const tableWords = Math.max(1, textures.length) * TEXTURE_WORDS;
84
+ const texelWords = textures.reduce((sum, texture) => sum + texelCount(texture) * (isFloat(texture) ? 4 : 1), 0);
85
+
86
+ return { textures, indexOf, tableWords, extraOffset: tableWords + texelWords };
87
+ }
88
+
89
+ export function packTextures(scene: SerializedScene, extras: TextureExtras = {}): PackedTextures {
90
+ return writeTextures(planTextures(scene, extras.textures), extras.words ?? new Uint32Array(0));
91
+ }
92
+
93
+ /** The buffer a plan describes, with `extraWords` appended at its `extraOffset`. */
94
+ export function writeTextures(plan: TexturePlan, extraWords: Uint32Array): PackedTextures {
95
+ const { textures, indexOf, tableWords } = plan;
96
+
97
+ // WebGPU rejects an empty binding, and a scene with no textures at all is ordinary.
98
+ const data = new Uint32Array(Math.max(4, plan.extraOffset + extraWords.length));
99
+ const words = data;
100
+ const floats = new Float32Array(data.buffer);
101
+ let texelAt = tableWords;
102
+
103
+ textures.forEach((texture, index) => {
104
+ const at = index * TEXTURE_WORDS;
105
+ const texels = texelCount(texture);
106
+ const float = isFloat(texture);
107
+
108
+ words[at] = texture.width;
109
+ words[at + 1] = texture.height;
110
+ words[at + 2] = texelAt;
111
+ words[at + 3] = (float ? TEXTURE_FLOAT : 0)
112
+ | (isEncoded(texture) ? TEXTURE_SRGB : 0)
113
+ | (texture.nearest ? TEXTURE_NEAREST : 0)
114
+ | (texture.flipY ? TEXTURE_FLIP_Y : 0)
115
+ | (texture.refraction ? TEXTURE_REFRACTION : 0);
116
+ words[at + 4] = texture.wrapS;
117
+ words[at + 5] = texture.wrapT;
118
+ words[at + 6] = Math.min(Math.max(0, texture.channel | 0), UV_CHANNELS - 1);
119
+ words[at + 7] = 0;
120
+
121
+ // Three's Matrix3 arrives column major, so a row of the affine transform reads across the columns.
122
+ const matrix = texture.matrix?.length === 9 ? texture.matrix : [1, 0, 0, 0, 1, 0, 0, 0, 1];
123
+
124
+ floats.set([matrix[0], matrix[3], matrix[6], 0], at + 8);
125
+ floats.set([matrix[1], matrix[4], matrix[7], 0], at + 12);
126
+
127
+ if (float) {
128
+ // The channels keep their bits and the shader reads them back as floats.
129
+ floats.set((texture.data as Float32Array).subarray(0, texels * 4), texelAt);
130
+ texelAt += texels * 4;
131
+ } else {
132
+ const source = texture.data as Uint8Array;
133
+
134
+ for (let texel = 0; texel < texels; texel++) {
135
+ const offset = texel * 4;
136
+
137
+ words[texelAt + texel] = source[offset]
138
+ | (source[offset + 1] << 8)
139
+ | (source[offset + 2] << 16)
140
+ | (source[offset + 3] << 24);
141
+ }
142
+
143
+ texelAt += texels;
144
+ }
145
+ });
146
+
147
+ data.set(extraWords, texelAt);
148
+
149
+ return { data, count: textures.length, indexOf, extraOffset: texelAt };
150
+ }
151
+
152
+ export { MIRRORED_REPEAT, REPEAT };
@@ -0,0 +1,56 @@
1
+ // Everything this renderer takes from @tinypoly/cpu-renderer passes through here, so a version bump has one place to
2
+ // audit. The sibling package owns the scene snapshot, the settings and the tone mapping; this one owns the GPU.
3
+
4
+ export {
5
+ DEFAULT_RENDER_ENVIRONMENT,
6
+ DEFAULT_RENDER_SETTINGS,
7
+ mergeSettings,
8
+ sceneSignature,
9
+ serializeCamera,
10
+ serializeScene,
11
+ } from "@tinypoly/cpu-renderer";
12
+
13
+ // The renderer's own public shapes, so a host can swap one renderer for the other without touching its types.
14
+ export type {
15
+ Bucket,
16
+ CameraState,
17
+ Listener,
18
+ RenderBackgroundMode,
19
+ RenderCamera,
20
+ RenderEnvironment,
21
+ RenderImage,
22
+ RenderPhase,
23
+ RenderPixels,
24
+ RenderScene,
25
+ RenderSettings,
26
+ RenderSettingsInput,
27
+ RenderToneMapping,
28
+ SerializedScene,
29
+ SerializedSceneResult,
30
+ } from "@tinypoly/cpu-renderer";
31
+
32
+ // The engine entry point is the lower level one, and the sibling's README warns that it moves more often.
33
+ export { EventEmitter } from "@tinypoly/cpu-renderer/engine";
34
+
35
+ export { encodeColor, srgbToLinear } from "@tinypoly/cpu-renderer/engine";
36
+
37
+ // The GLSL interpreter runs vertex shaders here, where a vertex's position has to be known before the hierarchy is
38
+ // built; the same interpreter bakes a procedural sky and a material's compute simulation, exactly as the CPU renderer
39
+ // does. Only fragment shaders are translated to WGSL, because only they run per sample.
40
+ export {
41
+ CpuComputePipeline,
42
+ CpuEnvironment,
43
+ CpuShader,
44
+ matrixValue,
45
+ sampleTexture,
46
+ } from "@tinypoly/cpu-renderer/engine";
47
+
48
+ export type {
49
+ SerializedFog,
50
+ SerializedLight,
51
+ SerializedMaterial,
52
+ SerializedMesh,
53
+ SerializedShader,
54
+ SerializedTexture,
55
+ UniformValue,
56
+ } from "@tinypoly/cpu-renderer/engine";
@@ -0,0 +1,42 @@
1
+ import type { SerializedLight } from "./upstream.js";
2
+
3
+ /**
4
+ * The CPU renderer's volumetric lights (`userData.cpuRenderer.volumetric` on a point or spot light): light scattered by
5
+ * the air along every view ray, marched as `volumetricLight.js` marches it and added over the image the same way. The
6
+ * kernel reads one record of `VOLUMETRIC_WORDS` floats per light from the texture buffer's tail.
7
+ */
8
+ export const VOLUMETRIC_WORDS = 20;
9
+
10
+ const clamp = (value: number, low = 0, high = 1) => Math.min(high, Math.max(low, value));
11
+
12
+ function normalize(v: readonly number[]): number[] {
13
+ const length = Math.hypot(v[0], v[1], v[2]) || 1;
14
+
15
+ return [v[0] / length, v[1] / length, v[2] / length];
16
+ }
17
+
18
+ /** The records, or an empty array when no light scatters. */
19
+ export function packVolumetricLights(lights: readonly SerializedLight[]): Float32Array {
20
+ const volumetric = lights.filter(light => light.volumetric && (light.type === "PointLight" || light.type === "SpotLight"));
21
+ const records = new Float32Array(volumetric.length * VOLUMETRIC_WORDS);
22
+
23
+ volumetric.forEach((light, index) => {
24
+ const density = Math.max(0, light.medium?.density ?? 0.01);
25
+ const spread = clamp(light.medium?.spread ?? 0);
26
+ // Scattered light reaches past the cone: up to 1.5 rad (86 degrees) off the axis with a full spread.
27
+ const outer = light.angle + spread * Math.max(0, 1.5 - light.angle);
28
+ const axis = normalize([0, 1, 2].map(k => light.target[k] - light.position[k]));
29
+ const radius = light.distance > 0 ? light.distance : -3 * Math.log(0.0001);
30
+ const spot = light.type === "SpotLight";
31
+
32
+ records.set([
33
+ ...light.position, spot ? 1 : 0,
34
+ ...axis, radius,
35
+ ...light.color.map(channel => channel * light.intensity), density,
36
+ light.angle, light.penumbra, light.distance, outer,
37
+ spread, clamp(light.medium?.anisotropy ?? 0, -0.95, 0.95), light.castShadow ? 1 : 0, 0,
38
+ ], index * VOLUMETRIC_WORDS);
39
+ });
40
+
41
+ return records;
42
+ }