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.
- package/LICENSE.txt +0 -0
- package/README.md +0 -0
- package/dist/Renderer.cjs +20844 -0
- package/dist/Renderer.cjs.map +1 -0
- package/dist/Renderer.js +20827 -0
- package/dist/Renderer.js.map +1 -0
- package/dist/client.cjs +91 -260
- package/dist/client.cjs.map +1 -1
- package/dist/client.js +68 -215
- package/dist/client.js.map +1 -1
- package/dist/server.cjs +165 -432
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +117 -370
- package/dist/server.js.map +1 -1
- package/dist/terminal.cjs +113 -200
- package/dist/terminal.cjs.map +1 -1
- package/dist/terminal.js +50 -51
- package/dist/terminal.js.map +1 -1
- package/dist/utils-CRhi1BDa.cjs +259 -0
- package/dist/utils-CRhi1BDa.cjs.map +1 -0
- package/dist/utils-D7tXt6-2.js +260 -0
- package/dist/utils-D7tXt6-2.js.map +1 -0
- package/package.json +19 -15
- package/src/{client.ts → network/client.js} +170 -403
- package/src/{compress-browser.ts → network/compress-browser.js} +2 -4
- package/src/{compress-node.ts → network/compress-node.js} +8 -14
- package/src/{server.ts → network/server.js} +229 -317
- package/src/{terminal.js → network/terminal.js} +0 -0
- package/src/{topazcube.ts → network/topazcube.js} +2 -2
- package/src/network/utils.js +375 -0
- package/src/renderer/Camera.js +191 -0
- package/src/renderer/DebugUI.js +703 -0
- package/src/renderer/Geometry.js +1049 -0
- package/src/renderer/Material.js +64 -0
- package/src/renderer/Mesh.js +211 -0
- package/src/renderer/Node.js +112 -0
- package/src/renderer/Pipeline.js +645 -0
- package/src/renderer/Renderer.js +1496 -0
- package/src/renderer/Skin.js +792 -0
- package/src/renderer/Texture.js +584 -0
- package/src/renderer/core/AssetManager.js +394 -0
- package/src/renderer/core/CullingSystem.js +308 -0
- package/src/renderer/core/EntityManager.js +541 -0
- package/src/renderer/core/InstanceManager.js +343 -0
- package/src/renderer/core/ParticleEmitter.js +358 -0
- package/src/renderer/core/ParticleSystem.js +564 -0
- package/src/renderer/core/SpriteSystem.js +349 -0
- package/src/renderer/gltf.js +563 -0
- package/src/renderer/math.js +161 -0
- package/src/renderer/rendering/HistoryBufferManager.js +333 -0
- package/src/renderer/rendering/ProbeCapture.js +1495 -0
- package/src/renderer/rendering/ReflectionProbeManager.js +352 -0
- package/src/renderer/rendering/RenderGraph.js +2258 -0
- package/src/renderer/rendering/passes/AOPass.js +308 -0
- package/src/renderer/rendering/passes/AmbientCapturePass.js +593 -0
- package/src/renderer/rendering/passes/BasePass.js +101 -0
- package/src/renderer/rendering/passes/BloomPass.js +420 -0
- package/src/renderer/rendering/passes/CRTPass.js +724 -0
- package/src/renderer/rendering/passes/FogPass.js +445 -0
- package/src/renderer/rendering/passes/GBufferPass.js +730 -0
- package/src/renderer/rendering/passes/HiZPass.js +744 -0
- package/src/renderer/rendering/passes/LightingPass.js +753 -0
- package/src/renderer/rendering/passes/ParticlePass.js +841 -0
- package/src/renderer/rendering/passes/PlanarReflectionPass.js +456 -0
- package/src/renderer/rendering/passes/PostProcessPass.js +405 -0
- package/src/renderer/rendering/passes/ReflectionPass.js +157 -0
- package/src/renderer/rendering/passes/RenderPostPass.js +364 -0
- package/src/renderer/rendering/passes/SSGIPass.js +266 -0
- package/src/renderer/rendering/passes/SSGITilePass.js +305 -0
- package/src/renderer/rendering/passes/ShadowPass.js +2072 -0
- package/src/renderer/rendering/passes/TransparentPass.js +831 -0
- package/src/renderer/rendering/passes/VolumetricFogPass.js +715 -0
- package/src/renderer/rendering/shaders/ao.wgsl +182 -0
- package/src/renderer/rendering/shaders/bloom.wgsl +97 -0
- package/src/renderer/rendering/shaders/bloom_blur.wgsl +80 -0
- package/src/renderer/rendering/shaders/crt.wgsl +455 -0
- package/src/renderer/rendering/shaders/depth_copy.wgsl +17 -0
- package/src/renderer/rendering/shaders/geometry.wgsl +580 -0
- package/src/renderer/rendering/shaders/hiz_reduce.wgsl +114 -0
- package/src/renderer/rendering/shaders/light_culling.wgsl +204 -0
- package/src/renderer/rendering/shaders/lighting.wgsl +932 -0
- package/src/renderer/rendering/shaders/lighting_common.wgsl +143 -0
- package/src/renderer/rendering/shaders/particle_render.wgsl +672 -0
- package/src/renderer/rendering/shaders/particle_simulate.wgsl +440 -0
- package/src/renderer/rendering/shaders/postproc.wgsl +293 -0
- package/src/renderer/rendering/shaders/render_post.wgsl +289 -0
- package/src/renderer/rendering/shaders/shadow.wgsl +117 -0
- package/src/renderer/rendering/shaders/ssgi.wgsl +266 -0
- package/src/renderer/rendering/shaders/ssgi_accumulate.wgsl +114 -0
- package/src/renderer/rendering/shaders/ssgi_propagate.wgsl +132 -0
- package/src/renderer/rendering/shaders/volumetric_blur.wgsl +80 -0
- package/src/renderer/rendering/shaders/volumetric_composite.wgsl +80 -0
- package/src/renderer/rendering/shaders/volumetric_raymarch.wgsl +634 -0
- package/src/renderer/utils/BoundingSphere.js +439 -0
- package/src/renderer/utils/Frustum.js +281 -0
- package/src/renderer/utils/Raycaster.js +761 -0
- package/dist/client.d.cts +0 -211
- package/dist/client.d.ts +0 -211
- package/dist/server.d.cts +0 -120
- package/dist/server.d.ts +0 -120
- package/dist/terminal.d.cts +0 -64
- package/dist/terminal.d.ts +0 -64
- 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 }
|