topazcube 0.1.31 → 0.1.35

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 (103) hide show
  1. package/LICENSE.txt +0 -0
  2. package/README.md +0 -0
  3. package/dist/Renderer.cjs +20844 -0
  4. package/dist/Renderer.cjs.map +1 -0
  5. package/dist/Renderer.js +20827 -0
  6. package/dist/Renderer.js.map +1 -0
  7. package/dist/client.cjs +91 -260
  8. package/dist/client.cjs.map +1 -1
  9. package/dist/client.js +68 -215
  10. package/dist/client.js.map +1 -1
  11. package/dist/server.cjs +165 -432
  12. package/dist/server.cjs.map +1 -1
  13. package/dist/server.js +117 -370
  14. package/dist/server.js.map +1 -1
  15. package/dist/terminal.cjs +113 -200
  16. package/dist/terminal.cjs.map +1 -1
  17. package/dist/terminal.js +50 -51
  18. package/dist/terminal.js.map +1 -1
  19. package/dist/utils-CRhi1BDa.cjs +259 -0
  20. package/dist/utils-CRhi1BDa.cjs.map +1 -0
  21. package/dist/utils-D7tXt6-2.js +260 -0
  22. package/dist/utils-D7tXt6-2.js.map +1 -0
  23. package/package.json +19 -15
  24. package/src/{client.ts → network/client.js} +170 -403
  25. package/src/{compress-browser.ts → network/compress-browser.js} +2 -4
  26. package/src/{compress-node.ts → network/compress-node.js} +8 -14
  27. package/src/{server.ts → network/server.js} +229 -317
  28. package/src/{terminal.js → network/terminal.js} +0 -0
  29. package/src/{topazcube.ts → network/topazcube.js} +2 -2
  30. package/src/network/utils.js +375 -0
  31. package/src/renderer/Camera.js +191 -0
  32. package/src/renderer/DebugUI.js +703 -0
  33. package/src/renderer/Geometry.js +1049 -0
  34. package/src/renderer/Material.js +64 -0
  35. package/src/renderer/Mesh.js +211 -0
  36. package/src/renderer/Node.js +112 -0
  37. package/src/renderer/Pipeline.js +645 -0
  38. package/src/renderer/Renderer.js +1496 -0
  39. package/src/renderer/Skin.js +792 -0
  40. package/src/renderer/Texture.js +584 -0
  41. package/src/renderer/core/AssetManager.js +394 -0
  42. package/src/renderer/core/CullingSystem.js +308 -0
  43. package/src/renderer/core/EntityManager.js +541 -0
  44. package/src/renderer/core/InstanceManager.js +343 -0
  45. package/src/renderer/core/ParticleEmitter.js +358 -0
  46. package/src/renderer/core/ParticleSystem.js +564 -0
  47. package/src/renderer/core/SpriteSystem.js +349 -0
  48. package/src/renderer/gltf.js +563 -0
  49. package/src/renderer/math.js +161 -0
  50. package/src/renderer/rendering/HistoryBufferManager.js +333 -0
  51. package/src/renderer/rendering/ProbeCapture.js +1495 -0
  52. package/src/renderer/rendering/ReflectionProbeManager.js +352 -0
  53. package/src/renderer/rendering/RenderGraph.js +2258 -0
  54. package/src/renderer/rendering/passes/AOPass.js +308 -0
  55. package/src/renderer/rendering/passes/AmbientCapturePass.js +593 -0
  56. package/src/renderer/rendering/passes/BasePass.js +101 -0
  57. package/src/renderer/rendering/passes/BloomPass.js +420 -0
  58. package/src/renderer/rendering/passes/CRTPass.js +724 -0
  59. package/src/renderer/rendering/passes/FogPass.js +445 -0
  60. package/src/renderer/rendering/passes/GBufferPass.js +730 -0
  61. package/src/renderer/rendering/passes/HiZPass.js +744 -0
  62. package/src/renderer/rendering/passes/LightingPass.js +753 -0
  63. package/src/renderer/rendering/passes/ParticlePass.js +841 -0
  64. package/src/renderer/rendering/passes/PlanarReflectionPass.js +456 -0
  65. package/src/renderer/rendering/passes/PostProcessPass.js +405 -0
  66. package/src/renderer/rendering/passes/ReflectionPass.js +157 -0
  67. package/src/renderer/rendering/passes/RenderPostPass.js +364 -0
  68. package/src/renderer/rendering/passes/SSGIPass.js +266 -0
  69. package/src/renderer/rendering/passes/SSGITilePass.js +305 -0
  70. package/src/renderer/rendering/passes/ShadowPass.js +2072 -0
  71. package/src/renderer/rendering/passes/TransparentPass.js +831 -0
  72. package/src/renderer/rendering/passes/VolumetricFogPass.js +715 -0
  73. package/src/renderer/rendering/shaders/ao.wgsl +182 -0
  74. package/src/renderer/rendering/shaders/bloom.wgsl +97 -0
  75. package/src/renderer/rendering/shaders/bloom_blur.wgsl +80 -0
  76. package/src/renderer/rendering/shaders/crt.wgsl +455 -0
  77. package/src/renderer/rendering/shaders/depth_copy.wgsl +17 -0
  78. package/src/renderer/rendering/shaders/geometry.wgsl +580 -0
  79. package/src/renderer/rendering/shaders/hiz_reduce.wgsl +114 -0
  80. package/src/renderer/rendering/shaders/light_culling.wgsl +204 -0
  81. package/src/renderer/rendering/shaders/lighting.wgsl +932 -0
  82. package/src/renderer/rendering/shaders/lighting_common.wgsl +143 -0
  83. package/src/renderer/rendering/shaders/particle_render.wgsl +672 -0
  84. package/src/renderer/rendering/shaders/particle_simulate.wgsl +440 -0
  85. package/src/renderer/rendering/shaders/postproc.wgsl +293 -0
  86. package/src/renderer/rendering/shaders/render_post.wgsl +289 -0
  87. package/src/renderer/rendering/shaders/shadow.wgsl +117 -0
  88. package/src/renderer/rendering/shaders/ssgi.wgsl +266 -0
  89. package/src/renderer/rendering/shaders/ssgi_accumulate.wgsl +114 -0
  90. package/src/renderer/rendering/shaders/ssgi_propagate.wgsl +132 -0
  91. package/src/renderer/rendering/shaders/volumetric_blur.wgsl +80 -0
  92. package/src/renderer/rendering/shaders/volumetric_composite.wgsl +80 -0
  93. package/src/renderer/rendering/shaders/volumetric_raymarch.wgsl +634 -0
  94. package/src/renderer/utils/BoundingSphere.js +439 -0
  95. package/src/renderer/utils/Frustum.js +281 -0
  96. package/src/renderer/utils/Raycaster.js +761 -0
  97. package/dist/client.d.cts +0 -211
  98. package/dist/client.d.ts +0 -211
  99. package/dist/server.d.cts +0 -120
  100. package/dist/server.d.ts +0 -120
  101. package/dist/terminal.d.cts +0 -64
  102. package/dist/terminal.d.ts +0 -64
  103. package/src/utils.ts +0 -403
@@ -0,0 +1,584 @@
1
+ // Texture class for WebGPU texture management
2
+
3
+
4
+ // https://enkimute.github.io/hdrpng.js/
5
+ //From https://raw.githubusercontent.com/enkimute/hdrpng.js/refs/heads/master/hdrpng.js
6
+
7
+ function loadHDR( url ) {
8
+ function m(a,b) { for (var i in b) a[i]=b[i]; return a; };
9
+
10
+ let p = new Promise((resolve, reject) => {
11
+ var req = m(new XMLHttpRequest(),{responseType:"arraybuffer"});
12
+ req.onerror = reject.bind(req,false);
13
+ req.onload = function() {
14
+ if (this.status>=400) return this.onerror();
15
+ var header='',pos=0,d8=new Uint8Array(this.response),format;
16
+ // read header.
17
+ while (!header.match(/\n\n[^\n]+\n/g)) header += String.fromCharCode(d8[pos++]);
18
+ // check format.
19
+ format = header.match(/FORMAT=(.*)$/m)[1];
20
+ if (format!='32-bit_rle_rgbe') return console.warn('unknown format : '+format),this.onerror();
21
+ // parse resolution
22
+ var rez=header.split(/\n/).reverse()[1].split(' '), width=rez[3]*1, height=rez[1]*1;
23
+ // Create image.
24
+ var img=new Uint8Array(width*height*4),ipos=0;
25
+ // Read all scanlines
26
+ for (var j=0; j<height; j++) {
27
+ var rgbe=d8.slice(pos,pos+=4),scanline=[];
28
+ if (rgbe[0]!=2||(rgbe[1]!=2)||(rgbe[2]&0x80)) {
29
+ var len=width,rs=0; pos-=4; while (len>0) {
30
+ img.set(d8.slice(pos,pos+=4),ipos);
31
+ if (img[ipos]==1&&img[ipos+1]==1&&img[ipos+2]==1) {
32
+ for (img[ipos+3]<<rs; i>0; i--) {
33
+ img.set(img.slice(ipos-4,ipos),ipos);
34
+ ipos+=4;
35
+ len--
36
+ }
37
+ rs+=8;
38
+ } else { len--; ipos+=4; rs=0; }
39
+ }
40
+ } else {
41
+ if ((rgbe[2]<<8)+rgbe[3]!=width) return console.warn('HDR line mismatch ..'),this.onerror();
42
+ for (var i=0;i<4;i++) {
43
+ var ptr=i*width,ptr_end=(i+1)*width,buf,count;
44
+ while (ptr<ptr_end){
45
+ buf = d8.slice(pos,pos+=2);
46
+ if (buf[0] > 128) { count = buf[0]-128; while(count-- > 0) scanline[ptr++] = buf[1]; }
47
+ else { count = buf[0]-1; scanline[ptr++]=buf[1]; while(count-->0) scanline[ptr++]=d8[pos++]; }
48
+ }
49
+ }
50
+ for (var i=0;i<width;i++) { img[ipos++]=scanline[i]; img[ipos++]=scanline[i+width]; img[ipos++]=scanline[i+2*width]; img[ipos++]=scanline[i+3*width]; }
51
+ }
52
+ }
53
+ resolve({data:img,width: width,height: height});
54
+ }
55
+ req.open("GET",url,true);
56
+ req.send(null);
57
+ return req;
58
+ })
59
+ return p
60
+ }
61
+
62
+ //https://webgpufundamentals.org/webgpu/lessons/webgpu-importing-textures.html
63
+
64
+ const numMipLevels = (...sizes) => {
65
+ const maxSize = Math.max(sizes[0], sizes[1]);
66
+ const mipFactor = 2
67
+ return 1 + Math.floor(Math.log(maxSize) / Math.log(mipFactor));
68
+ }
69
+
70
+ const generateMips = (() => {
71
+ let sampler, module, pipeline;
72
+ let modules = {};
73
+ const pipelines = {};
74
+ const samplers = {};
75
+
76
+ return function generateMips(device, texture, rgbe = false) {
77
+ let v = rgbe ? 'rgbe' : 'rgb';
78
+ v = v + texture.format
79
+ if (modules[v]) {
80
+ module = modules[v]
81
+ pipeline = pipelines[v]
82
+ sampler = samplers[v]
83
+ } else {
84
+ let code = ''
85
+ code = `
86
+ struct VSOutput {
87
+ @builtin(position) position: vec4f,
88
+ @location(0) texcoord: vec2f,
89
+ };
90
+
91
+ @vertex fn vs(
92
+ @builtin(vertex_index) vertexIndex : u32
93
+ ) -> VSOutput {
94
+ let pos = array(
95
+ // 1st triangle
96
+ vec2f( 0.0, 0.0), // center
97
+ vec2f( 1.0, 0.0), // right, center
98
+ vec2f( 0.0, 1.0), // center, top
99
+
100
+ // 2nd triangle
101
+ vec2f( 0.0, 1.0), // center, top
102
+ vec2f( 1.0, 0.0), // right, center
103
+ vec2f( 1.0, 1.0), // right, top
104
+ );
105
+
106
+ var vsOutput: VSOutput;
107
+ let xy = pos[vertexIndex];
108
+ vsOutput.position = vec4f(xy * 2.0 - 1.0, 0.0, 1.0);
109
+ vsOutput.texcoord = vec2f(xy.x, 1.0 - xy.y);
110
+ return vsOutput;
111
+ }
112
+
113
+ @group(0) @binding(0) var ourSampler: sampler;
114
+ @group(0) @binding(1) var ourTexture: texture_2d<f32>;
115
+
116
+ fn gaussian3x3(uv: vec2f) -> vec4f {
117
+ let texelSize = 1.0 / vec2f(textureDimensions(ourTexture));
118
+ var color = vec3f(0.0);
119
+
120
+ // 3x3 Gaussian kernel weights
121
+ let weights = array(
122
+ 0.0625, 0.125, 0.0625, // 1/16, 2/16, 1/16
123
+ 0.125, 0.25, 0.125, // 2/16, 4/16, 2/16
124
+ 0.0625, 0.125, 0.0625 // 1/16, 2/16, 1/16
125
+ );
126
+
127
+ for (var i = -1; i <= 1; i++) {
128
+ for (var j = -1; j <= 1; j++) {
129
+ let offset = vec2f(f32(i), f32(j)) * texelSize * 1.0;
130
+ let weight = weights[(i + 1) * 3 + (j + 1)];
131
+
132
+ var rgbe = textureSample(ourTexture, ourSampler, uv + offset);
133
+ var rgb = rgbe.rgb * pow(2.0, rgbe.a * 255.0 - 128.0);
134
+ color += rgb * weight;
135
+ }
136
+ }
137
+
138
+ // Encode back to RGBE format
139
+ let maxComponent = max(max(color.r, color.g), color.b);
140
+ let exponent = ceil(log2(maxComponent));
141
+ let mantissa = color * pow(2.0, -exponent);
142
+ return vec4f(mantissa, (exponent + 128.0) / 255.0);
143
+ }
144
+
145
+ @fragment fn fs(fsInput: VSOutput) -> @location(0) vec4f {
146
+ `
147
+ if (rgbe) {
148
+ code += `return gaussian3x3(fsInput.texcoord);\n`
149
+ } else {
150
+ code += `return textureSample(ourTexture, ourSampler, fsInput.texcoord);\n`
151
+ }
152
+ code += `
153
+ }
154
+ `
155
+ module = device.createShaderModule({
156
+ label: 'textured quad shaders for mip level generation',
157
+ code: code
158
+ });
159
+ modules[v] = module
160
+
161
+ sampler = device.createSampler({
162
+ minFilter: 'linear',
163
+ magFilter: 'linear',
164
+ addressModeU: 'mirror-repeat',
165
+ addressModeV: 'mirror-repeat',
166
+ });
167
+ samplers[v] = sampler
168
+
169
+ pipeline = device.createRenderPipeline({
170
+ label: 'mip level generator pipeline',
171
+ layout: 'auto',
172
+ vertex: {
173
+ module,
174
+ },
175
+ fragment: {
176
+ module,
177
+ targets: [{ format: texture.format }],
178
+ },
179
+ });
180
+ pipelines[v] = pipeline
181
+ }
182
+
183
+ const encoder = device.createCommandEncoder({
184
+ label: 'mip gen encoder',
185
+ });
186
+
187
+ let width = texture.width;
188
+ let height = texture.height;
189
+ let baseMipLevel = 0;
190
+ while (width > 1 || height > 1) {
191
+ width = Math.max(1, width / 2 | 0);
192
+ height = Math.max(1, height / 2 | 0);
193
+
194
+ const bindGroup = device.createBindGroup({
195
+ layout: pipeline.getBindGroupLayout(0),
196
+ entries: [
197
+ { binding: 0, resource: sampler },
198
+ { binding: 1, resource: texture.createView({baseMipLevel, mipLevelCount: 1}) },
199
+ ],
200
+ });
201
+
202
+ ++baseMipLevel;
203
+
204
+ const renderPassDescriptor = {
205
+ label: 'our basic canvas renderPass',
206
+ colorAttachments: [
207
+ {
208
+ view: texture.createView({baseMipLevel, mipLevelCount: 1}),
209
+ loadOp: 'clear',
210
+ storeOp: 'store',
211
+ },
212
+ ],
213
+ };
214
+
215
+ const pass = encoder.beginRenderPass(renderPassDescriptor);
216
+ pass.setPipeline(pipeline);
217
+ pass.setBindGroup(0, bindGroup);
218
+ pass.draw(6); // call our vertex shader 6 times
219
+ pass.end();
220
+ }
221
+
222
+ const commandBuffer = encoder.finish();
223
+ device.queue.submit([commandBuffer]);
224
+ };
225
+ })();
226
+
227
+ class Texture {
228
+ texture = null
229
+ sampler = null
230
+ engine = null
231
+ sourceUrl = ''
232
+
233
+ constructor(engine = null) {
234
+ this.engine = engine
235
+ }
236
+
237
+ // Static async factory method
238
+ static async fromImage(engine, url_or_image, options = {}) {
239
+ options = {
240
+ flipY: true,
241
+ srgb: true, // Whether to interpret image as sRGB (true) or linear (false)
242
+ generateMips: true,
243
+ ...options
244
+ }
245
+ const { device, options: webgpuOptions } = engine
246
+
247
+ let imageBitmap
248
+ let isHDR = false
249
+ if (typeof url_or_image === 'string') {
250
+ if (url_or_image.endsWith('.hdr')) {
251
+ isHDR = true
252
+ imageBitmap = await loadHDR(url_or_image)
253
+ } else {
254
+ const response = await fetch(url_or_image)
255
+ imageBitmap = await createImageBitmap(await response.blob())
256
+ }
257
+ } else {
258
+ imageBitmap = url_or_image
259
+ }
260
+
261
+ let format = options.srgb ? "rgba8unorm-srgb" : "rgba8unorm"
262
+ if (isHDR) {
263
+ format = "rgba8unorm"
264
+ }
265
+ const texture = await device.createTexture({
266
+ size: [imageBitmap.width, imageBitmap.height, 1],
267
+ mipLevelCount: options.generateMips ? numMipLevels(imageBitmap.width, imageBitmap.height) : 1,
268
+ format: format,
269
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
270
+ })
271
+
272
+ let source = imageBitmap
273
+ if (isHDR) {
274
+ source = imageBitmap.data
275
+ device.queue.writeTexture(
276
+ { texture, mipLevel: 0, origin: [0, 0, 0], aspect: "all" },
277
+ source,
278
+ { offset: 0, bytesPerRow: imageBitmap.width * 4, rowsPerImage: imageBitmap.height },
279
+ [ imageBitmap.width, imageBitmap.height, 1 ]
280
+ )
281
+ } else {
282
+ device.queue.copyExternalImageToTexture(
283
+ { source: source, flipY: options.flipY },
284
+ { texture: texture },
285
+ [imageBitmap.width, imageBitmap.height]
286
+ )
287
+ }
288
+
289
+ let mipCount = options.generateMips ? numMipLevels(imageBitmap.width, imageBitmap.height) : 1
290
+ if (options.generateMips) {
291
+ generateMips(device, texture, isHDR)
292
+ }
293
+
294
+ let t = Texture._createTexture(engine, texture, {
295
+ forceLinear: isHDR,
296
+ addressModeU: options.addressModeU,
297
+ addressModeV: options.addressModeV
298
+ })
299
+ t.isHDR = isHDR
300
+ t.mipCount = mipCount
301
+ if (typeof url_or_image === 'string') {
302
+ t.sourceUrl = url_or_image
303
+ }
304
+ return t
305
+ }
306
+
307
+ static async fromColor(engine, color) {
308
+ // Parse hex color to rgb values
309
+ color = color.replace('#', '')
310
+ const r = parseInt(color.substring(0,2), 16) / 255
311
+ const g = parseInt(color.substring(2,4), 16) / 255
312
+ const b = parseInt(color.substring(4,6), 16) / 255
313
+
314
+ return Texture.fromRGBA(engine, r, g, b, 1.0)
315
+ }
316
+
317
+ /**
318
+ * Create a texture from raw RGBA Uint8Array data
319
+ * @param {Engine} engine
320
+ * @param {Uint8Array} data - RGBA pixel data (4 bytes per pixel)
321
+ * @param {number} width - Texture width
322
+ * @param {number} height - Texture height
323
+ * @param {Object} options - { srgb, generateMips }
324
+ */
325
+ static async fromRawData(engine, data, width, height, options = {}) {
326
+ options = {
327
+ srgb: true,
328
+ generateMips: false,
329
+ ...options
330
+ }
331
+ const { device } = engine
332
+
333
+ const format = options.srgb ? "rgba8unorm-srgb" : "rgba8unorm"
334
+ const mipCount = options.generateMips ? numMipLevels(width, height) : 1
335
+
336
+ const texture = await device.createTexture({
337
+ size: [width, height, 1],
338
+ mipLevelCount: mipCount,
339
+ format: format,
340
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
341
+ })
342
+
343
+ device.queue.writeTexture(
344
+ { texture, mipLevel: 0, origin: [0, 0, 0] },
345
+ data,
346
+ { bytesPerRow: width * 4, rowsPerImage: height },
347
+ [width, height, 1]
348
+ )
349
+
350
+ if (options.generateMips && mipCount > 1) {
351
+ generateMips(device, texture, false)
352
+ }
353
+
354
+ let t = Texture._createTexture(engine, texture)
355
+ t.mipCount = mipCount
356
+ return t
357
+ }
358
+
359
+ /**
360
+ * Create a 1x1 texture from RGBA values (0-1 range)
361
+ */
362
+ static async fromRGBA(engine, r, g, b, a = 1.0) {
363
+ const { device } = engine
364
+
365
+ // Create 1x1 pixel data with the color (as Uint8)
366
+ const colorData = new Uint8Array([
367
+ Math.round(r * 255),
368
+ Math.round(g * 255),
369
+ Math.round(b * 255),
370
+ Math.round(a * 255)
371
+ ])
372
+
373
+ const texture = await device.createTexture({
374
+ size: [1, 1],
375
+ format: "rgba8unorm",
376
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
377
+ })
378
+
379
+ device.queue.writeTexture(
380
+ { texture },
381
+ colorData,
382
+ { bytesPerRow: 4 },
383
+ [1, 1]
384
+ )
385
+
386
+ let t = Texture._createTexture(engine, texture)
387
+ return t
388
+ }
389
+
390
+ /**
391
+ * Load HDR texture from RGBM JPG pair (RGB with sRGB gamma + log multiplier)
392
+ * RGB stores actual color values (gamma corrected) - values <= 1.0 stored directly
393
+ * Multiplier: black = 1.0, white = 32768, logarithmic encoding
394
+ * @param {string} rgbUrl - URL to RGB JPG (e.g., 'probe_01.jpg')
395
+ * @param {string} multUrl - URL to multiplier JPG (e.g., 'probe_01.mult.jpg'), or null to auto-derive
396
+ * @param {Object} options - { generateMips: true }
397
+ */
398
+ static async fromJPGPair(engine, rgbUrl, multUrl = null, options = {}) {
399
+ options = {
400
+ generateMips: true,
401
+ ...options
402
+ }
403
+ const { device } = engine
404
+
405
+ // Auto-derive multiplier URL if not provided
406
+ if (!multUrl) {
407
+ multUrl = rgbUrl.replace(/\.jpg$/i, '.mult.jpg')
408
+ }
409
+
410
+ // Load both images
411
+ const [rgbResponse, multResponse] = await Promise.all([
412
+ fetch(rgbUrl),
413
+ fetch(multUrl)
414
+ ])
415
+
416
+ const [rgbBlob, multBlob] = await Promise.all([
417
+ rgbResponse.blob(),
418
+ multResponse.blob()
419
+ ])
420
+
421
+ const [rgbBitmap, multBitmap] = await Promise.all([
422
+ createImageBitmap(rgbBlob),
423
+ createImageBitmap(multBlob)
424
+ ])
425
+
426
+ const width = rgbBitmap.width
427
+ const height = rgbBitmap.height
428
+
429
+ // Draw to canvases to get pixel data
430
+ const rgbCanvas = document.createElement('canvas')
431
+ rgbCanvas.width = width
432
+ rgbCanvas.height = height
433
+ const rgbCtx = rgbCanvas.getContext('2d')
434
+ rgbCtx.drawImage(rgbBitmap, 0, 0)
435
+ const rgbImageData = rgbCtx.getImageData(0, 0, width, height)
436
+
437
+ const multCanvas = document.createElement('canvas')
438
+ multCanvas.width = width
439
+ multCanvas.height = height
440
+ const multCtx = multCanvas.getContext('2d')
441
+ multCtx.drawImage(multBitmap, 0, 0)
442
+ const multImageData = multCtx.getImageData(0, 0, width, height)
443
+
444
+ // RGBM decoding parameters (must match encoder in ProbeCapture.saveAsJPG)
445
+ const LOG_MULT_MAX = 15 // log2(32768)
446
+ const SRGB_GAMMA = 2.2
447
+
448
+ // Convert RGBM to RGBE for GPU texture
449
+ // We store as RGBE because that's what the shader expects
450
+ const rgbeData = new Uint8Array(width * height * 4)
451
+ for (let i = 0; i < width * height; i++) {
452
+ const idx = i * 4
453
+
454
+ // Get sRGB gamma encoded RGB (0-255 -> 0-1)
455
+ const rGamma = rgbImageData.data[idx] / 255
456
+ const gGamma = rgbImageData.data[idx + 1] / 255
457
+ const bGamma = rgbImageData.data[idx + 2] / 255
458
+
459
+ // Convert from sRGB to linear
460
+ const rLinear = Math.pow(rGamma, SRGB_GAMMA)
461
+ const gLinear = Math.pow(gGamma, SRGB_GAMMA)
462
+ const bLinear = Math.pow(bGamma, SRGB_GAMMA)
463
+
464
+ // Decode multiplier: 0 = 1.0, 255 = 32768, logarithmic
465
+ const multByte = multImageData.data[idx]
466
+ const multNorm = multByte / 255 // 0 to 1
467
+ const logMult = multNorm * LOG_MULT_MAX // 0 to 15
468
+ const multiplier = Math.pow(2, logMult) // 1 to 32768
469
+
470
+ // Reconstruct HDR color
471
+ const r = rLinear * multiplier
472
+ const g = gLinear * multiplier
473
+ const b = bLinear * multiplier
474
+
475
+ // Convert to RGBE for GPU
476
+ const maxVal = Math.max(r, g, b)
477
+ if (maxVal < 1e-32) {
478
+ rgbeData[idx] = 0
479
+ rgbeData[idx + 1] = 0
480
+ rgbeData[idx + 2] = 0
481
+ rgbeData[idx + 3] = 0
482
+ } else {
483
+ const exp = Math.ceil(Math.log2(maxVal))
484
+ const scale = Math.pow(2, -exp) * 255
485
+
486
+ rgbeData[idx] = Math.min(255, Math.max(0, Math.round(r * scale)))
487
+ rgbeData[idx + 1] = Math.min(255, Math.max(0, Math.round(g * scale)))
488
+ rgbeData[idx + 2] = Math.min(255, Math.max(0, Math.round(b * scale)))
489
+ rgbeData[idx + 3] = exp + 128
490
+ }
491
+ }
492
+
493
+ // Create texture
494
+ const mipCount = options.generateMips ? numMipLevels(width, height) : 1
495
+ const texture = device.createTexture({
496
+ size: [width, height],
497
+ mipLevelCount: mipCount,
498
+ format: 'rgba8unorm',
499
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
500
+ })
501
+
502
+ device.queue.writeTexture(
503
+ { texture },
504
+ rgbeData,
505
+ { bytesPerRow: width * 4 },
506
+ [width, height]
507
+ )
508
+
509
+ // Generate mips with RGBE-aware filtering
510
+ if (options.generateMips) {
511
+ generateMips(device, texture, true) // true = RGBE mode
512
+ }
513
+
514
+ let t = Texture._createTexture(engine, texture, {forceLinear: true})
515
+ t.isHDR = true
516
+ t.mipCount = mipCount
517
+ return t
518
+ }
519
+
520
+ static async renderTarget(engine, format = 'rgba8unorm', width = null, height = null) {
521
+ const { device, canvas, options } = engine
522
+ const w = width ?? canvas.width
523
+ const h = height ?? canvas.height
524
+ const texture = await device.createTexture({
525
+ size: [w, h],
526
+ format: format,
527
+ //sampleCount: options.msaa > 1 ? options.msaa : 1,
528
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
529
+ });
530
+ let t = Texture._createTexture(engine, texture)
531
+ t.renderTarget = true
532
+ t.format = format
533
+ return t
534
+ }
535
+
536
+ static async depth(engine, width = null, height = null) {
537
+ const { device, canvas, options } = engine
538
+ const w = width ?? canvas.width
539
+ const h = height ?? canvas.height
540
+
541
+ const texture = await device.createTexture({
542
+ size: [w, h, 1],
543
+ //sampleCount: options.msaa > 1 ? options.msaa : 1,
544
+ format: "depth32float",
545
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC,
546
+ })
547
+
548
+ let t = Texture._createTexture(engine, texture)
549
+ t.depth = true
550
+ return t
551
+ }
552
+
553
+ static _createTexture(engine, texture, options = {}) {
554
+ const { device, canvas, settings } = engine
555
+ let t = new Texture(engine)
556
+ t.texture = texture
557
+ t.view = texture.createView()
558
+ // Default to mirror-repeat to avoid black border artifacts
559
+ const addressModeU = options.addressModeU || 'mirror-repeat'
560
+ const addressModeV = options.addressModeV || 'mirror-repeat'
561
+ if (settings.rendering.nearestFiltering && !options.forceLinear) {
562
+ t.sampler = device.createSampler({
563
+ magFilter: "nearest",
564
+ minFilter: "nearest",
565
+ addressModeU: addressModeU,
566
+ addressModeV: addressModeV,
567
+ mipmapFilter: "nearest",
568
+ maxAnisotropy: 1,
569
+ })
570
+ } else {
571
+ t.sampler = device.createSampler({
572
+ magFilter: "linear",
573
+ minFilter: "linear",
574
+ addressModeU: addressModeU,
575
+ addressModeV: addressModeV,
576
+ mipmapFilter: "linear",
577
+ maxAnisotropy: 1,
578
+ })
579
+ }
580
+ return t
581
+ }
582
+ }
583
+
584
+ export { Texture, generateMips, numMipLevels }