@plasius/gpu-renderer 0.2.56 → 0.2.58
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/CHANGELOG.md +34 -0
- package/README.md +57 -0
- package/dist/index.cjs +6972 -6478
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +6972 -6478
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/gpu-allocation-budget.js +92 -0
- package/src/index.d.ts +34 -0
- package/src/wavefront-bind-groups.js +2 -0
- package/src/wavefront-compute.js +40 -10
- package/src/wavefront-config.js +22 -2
- package/src/wavefront-instance-shader.js +88 -0
- package/src/wavefront-instance-texture.js +43 -0
- package/src/wavefront-instances.js +107 -0
- package/src/wavefront-mesh-sources.js +2 -2
- package/src/wavefront-pipelines.js +6 -5
- package/src/wavefront-runtime-support.js +8 -6
- package/src/wavefront-shader-bvh.js +4 -2
- package/src/wavefront-shader-kernels.js +1 -1
- package/src/wavefront-shader-layout.js +1 -0
- package/src/wavefront-shaders.js +5 -1
- package/src/wavefront-upload.js +12 -0
package/package.json
CHANGED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Accounts for requested WebGPU resource storage, not driver overhead or free VRAM.
|
|
2
|
+
const formats = Object.freeze({
|
|
3
|
+
r8unorm:1, r8snorm:1, r8uint:1, r8sint:1,
|
|
4
|
+
rg8unorm:2, rg8snorm:2, rg8uint:2, rg8sint:2, r16float:2, r16uint:2, r16sint:2,
|
|
5
|
+
rgba8unorm:4, 'rgba8unorm-srgb':4, bgra8unorm:4, 'bgra8unorm-srgb':4,
|
|
6
|
+
rgba8snorm:4, rgba8uint:4, rgba8sint:4, rg16float:4, rg16uint:4, rg16sint:4,
|
|
7
|
+
r32float:4, r32uint:4, r32sint:4, rgb10a2unorm:4, rg11b10ufloat:4,
|
|
8
|
+
rgba16float:8, rgba16uint:8, rgba16sint:8, rg32float:8, rg32uint:8, rg32sint:8,
|
|
9
|
+
rgba32float:16, rgba32uint:16, rgba32sint:16,
|
|
10
|
+
});
|
|
11
|
+
function integer(value, name, minimum=1) {
|
|
12
|
+
if(!Number.isSafeInteger(value)||value<minimum) throw new Error(`${name} must be a safe integer >= ${minimum}.`);
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
function within(value,limit,name) {
|
|
16
|
+
if(Number.isFinite(limit)&&value>limit) throw new Error(`GPU ${name} limit exceeded: requested ${value}, available ${limit}.`);
|
|
17
|
+
}
|
|
18
|
+
function textureBytes(descriptor,limits) {
|
|
19
|
+
const size=descriptor.size;
|
|
20
|
+
const array=Array.isArray(size)||ArrayBuffer.isView(size);
|
|
21
|
+
const width=integer(array?size[0]:size?.width,'Texture width');
|
|
22
|
+
const height=integer((array?size[1]:size?.height)??1,'Texture height');
|
|
23
|
+
const layers=integer((array?size[2]:size?.depthOrArrayLayers)??1,'Texture depth/layers');
|
|
24
|
+
const dimension=descriptor.dimension??'2d';
|
|
25
|
+
if(!['1d','2d','3d'].includes(dimension)) throw new Error('Unsupported GPU texture dimension.');
|
|
26
|
+
const limit=limits[dimension==='3d'?'maxTextureDimension3D':dimension==='1d'?'maxTextureDimension1D':'maxTextureDimension2D'];
|
|
27
|
+
within(Math.max(width,height,dimension==='3d'?layers:1),limit,'texture dimension');
|
|
28
|
+
if(dimension!=='3d') within(layers,limits.maxTextureArrayLayers,'texture layers');
|
|
29
|
+
const bpp=formats[descriptor.format];
|
|
30
|
+
if(!bpp) throw new Error(`GPU memory accounting does not support texture format ${descriptor.format}.`);
|
|
31
|
+
const levels=integer(descriptor.mipLevelCount??1,'Mip count');
|
|
32
|
+
const samples=integer(descriptor.sampleCount??1,'Sample count');
|
|
33
|
+
if(levels>Math.floor(Math.log2(Math.max(width,height,dimension==='3d'?layers:1)))+1) throw new Error('Invalid texture mip count.');
|
|
34
|
+
let bytes=0;
|
|
35
|
+
for(let level=0;level<levels;level++) bytes+=Math.max(1,Math.floor(width/2**level))*Math.max(1,Math.floor(height/2**level))*
|
|
36
|
+
(dimension==='3d'?Math.max(1,Math.floor(layers/2**level)):layers)*bpp*samples;
|
|
37
|
+
return integer(bytes,'Texture allocation');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createGpuAllocationTracker(rawDevice,{budgetBytes,storageUsage=globalThis.GPUBufferUsage?.STORAGE??128}={}) {
|
|
41
|
+
if(budgetBytes!==undefined) integer(budgetBytes,'GPU memory budget');
|
|
42
|
+
const records=new Map();let allocatedBytes=0,peakAllocatedBytes=0;
|
|
43
|
+
function allocate(kind,descriptor,bytes,create) {
|
|
44
|
+
integer(bytes,'GPU allocation',0);
|
|
45
|
+
const total=integer(allocatedBytes+bytes,'GPU allocation total',0);
|
|
46
|
+
if(budgetBytes!==undefined&&total>budgetBytes) throw new Error(`GPU memory budget exceeded: ${total} requested bytes, budget ${budgetBytes}. Reduce quality or scene resources.`);
|
|
47
|
+
const resource=create(descriptor),originalDestroy=resource.destroy.bind(resource);
|
|
48
|
+
const release=()=>{if(!records.has(resource))return;originalDestroy();records.delete(resource);allocatedBytes-=bytes;};
|
|
49
|
+
try{Object.defineProperty(resource,'destroy',{value:release,configurable:true});}
|
|
50
|
+
catch(error){originalDestroy();throw error;}
|
|
51
|
+
records.set(resource,{kind,label:descriptor.label??'',bytes,release});allocatedBytes=total;peakAllocatedBytes=Math.max(total,peakAllocatedBytes);
|
|
52
|
+
return resource;
|
|
53
|
+
}
|
|
54
|
+
const destroy=()=>{for(const record of [...records.values()])record.release();};
|
|
55
|
+
const methods={
|
|
56
|
+
createBuffer(descriptor){
|
|
57
|
+
const bytes=integer(descriptor.size,'Buffer size',0);
|
|
58
|
+
within(bytes,rawDevice.limits?.maxBufferSize,'buffer size');
|
|
59
|
+
if((descriptor.usage??0)&storageUsage) within(bytes,rawDevice.limits?.maxStorageBufferBindingSize,'storage binding size');
|
|
60
|
+
return allocate('buffer',descriptor,bytes,d=>rawDevice.createBuffer(d));
|
|
61
|
+
},
|
|
62
|
+
createTexture(descriptor){return allocate('texture',descriptor,textureBytes(descriptor,rawDevice.limits??{}),d=>rawDevice.createTexture(d));},
|
|
63
|
+
createQuerySet(descriptor){return allocate('query-set',descriptor,integer(descriptor.count,'Query count')*8,d=>rawDevice.createQuerySet(d));},
|
|
64
|
+
destroy(){destroy();rawDevice.destroy();},
|
|
65
|
+
};
|
|
66
|
+
const device=new Proxy(rawDevice,{get(target,key){
|
|
67
|
+
if(Object.hasOwn(methods,key))return methods[key];
|
|
68
|
+
const value=Reflect.get(target,key,target);return typeof value==='function'?value.bind(target):value;
|
|
69
|
+
}});
|
|
70
|
+
return Object.freeze({device,destroy,snapshot:()=>Object.freeze({
|
|
71
|
+
accounting:'requested-resource-bytes',budgetBytes:budgetBytes??null,allocatedBytes,peakAllocatedBytes,
|
|
72
|
+
resources:Object.freeze([...records.values()].map(({kind,label,bytes})=>Object.freeze({kind,label,bytes}))),
|
|
73
|
+
})});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Pop both scopes before awaiting either, so rejection cannot leave a scope open. */
|
|
77
|
+
export function openGpuAllocationErrorScopes(device) {
|
|
78
|
+
if(typeof device.pushErrorScope!=='function'||typeof device.popErrorScope!=='function') return async()=>{};
|
|
79
|
+
let count=0;
|
|
80
|
+
try{device.pushErrorScope('out-of-memory');count++;device.pushErrorScope('validation');count++;}
|
|
81
|
+
catch(error){while(count-->0)Promise.resolve(device.popErrorScope()).catch(()=>{});throw error;}
|
|
82
|
+
return async()=>{
|
|
83
|
+
const completions=[];
|
|
84
|
+
while(count>0){count--;try{completions.push(Promise.resolve(device.popErrorScope()));}catch(error){completions.push(Promise.reject(error));}}
|
|
85
|
+
if(!completions.length)return;
|
|
86
|
+
let timer;
|
|
87
|
+
try{
|
|
88
|
+
const errors=await Promise.race([Promise.all(completions),new Promise((_,reject)=>{timer=setTimeout(()=>reject(new Error('GPU allocation validation timed out.')),10000);})]);
|
|
89
|
+
const error=errors.find(Boolean);if(error)throw new Error(`GPU allocation failed: ${error.message??String(error)}`);
|
|
90
|
+
}finally{clearTimeout(timer);}
|
|
91
|
+
};
|
|
92
|
+
}
|
package/src/index.d.ts
CHANGED
|
@@ -899,6 +899,11 @@ export interface WavefrontPathTracingComputeConfig {
|
|
|
899
899
|
readonly gpuAccelerationBuildRequired: boolean;
|
|
900
900
|
readonly gpuMeshSource: WavefrontGpuMeshSource;
|
|
901
901
|
readonly gpuMaterialSource: WavefrontGpuMaterialSource;
|
|
902
|
+
readonly instancing: Readonly<{
|
|
903
|
+
instanceCount: number; geometryCount: number; instanceBytes: number;
|
|
904
|
+
tlasNodeCount: number; blasNodeCount: number; buffer: ArrayBuffer;
|
|
905
|
+
acceleration: WavefrontMeshAcceleration;
|
|
906
|
+
}> | null;
|
|
902
907
|
readonly meshAcceleration: WavefrontMeshAcceleration;
|
|
903
908
|
readonly emissiveTriangleIndices: WavefrontEmissiveTriangleIndexSource;
|
|
904
909
|
readonly emissiveTriangleCount: number;
|
|
@@ -1045,6 +1050,10 @@ export interface WavefrontPathTracingMemoryEstimate {
|
|
|
1045
1050
|
readonly pathVertexBytes: number;
|
|
1046
1051
|
readonly sceneObjectBytes: number;
|
|
1047
1052
|
readonly triangleBytes: number;
|
|
1053
|
+
/** Logical 144-byte records, separate from totalHotBufferBytes and before texture row padding. */
|
|
1054
|
+
readonly instanceBytes: number;
|
|
1055
|
+
/** Requested instance texture storage including row padding. */
|
|
1056
|
+
readonly instanceTextureBytes: number;
|
|
1048
1057
|
readonly materialTableBytes: number;
|
|
1049
1058
|
readonly bvhNodeBytes: number;
|
|
1050
1059
|
readonly bvhCombinedBytes: number;
|
|
@@ -1102,6 +1111,21 @@ export interface WavefrontGpuParallelismDiagnostics {
|
|
|
1102
1111
|
readonly coreUtilizationStatus: "not-exposed-by-webgpu";
|
|
1103
1112
|
}
|
|
1104
1113
|
|
|
1114
|
+
/** Static column-major affine transform referencing a unique mesh. */
|
|
1115
|
+
export interface WavefrontMeshInstanceInput {
|
|
1116
|
+
readonly id: number;
|
|
1117
|
+
readonly meshId: number;
|
|
1118
|
+
readonly transform: readonly number[] | Float32Array;
|
|
1119
|
+
}
|
|
1120
|
+
export interface WavefrontGpuAllocationSnapshot {
|
|
1121
|
+
/** Requested resource storage; excludes driver overhead and externally owned resources. */
|
|
1122
|
+
readonly accounting: "requested-resource-bytes";
|
|
1123
|
+
readonly budgetBytes: number | null;
|
|
1124
|
+
readonly allocatedBytes: number;
|
|
1125
|
+
readonly peakAllocatedBytes: number;
|
|
1126
|
+
readonly resources: readonly Readonly<{kind: string; label: string; bytes: number}>[];
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1105
1129
|
export interface CreateWavefrontPathTracingComputeRendererOptions {
|
|
1106
1130
|
/** Accumulate linear radiance between frames until camera/scene/reset changes. Default false. */
|
|
1107
1131
|
readonly progressive?: boolean;
|
|
@@ -1127,6 +1151,10 @@ export interface CreateWavefrontPathTracingComputeRendererOptions {
|
|
|
1127
1151
|
readonly mediums?: readonly WavefrontMediumInput[];
|
|
1128
1152
|
readonly mesh?: WavefrontMeshInput;
|
|
1129
1153
|
readonly meshes?: readonly WavefrontMeshInput[];
|
|
1154
|
+
/** Opt-in shared BLAS/TLAS instances. Every mesh must be referenced. Static, non-emissive meshes; CPU BLAS construction. */
|
|
1155
|
+
readonly instances?: readonly WavefrontMeshInstanceInput[];
|
|
1156
|
+
/** Positive renderer-owned resource byte budget. Device limits apply independently. */
|
|
1157
|
+
readonly memoryBudgetBytes?: number;
|
|
1130
1158
|
readonly triangleCapacity?: number;
|
|
1131
1159
|
readonly bvhNodeCapacity?: number;
|
|
1132
1160
|
readonly bvhLeafSortCapacity?: number;
|
|
@@ -1203,6 +1231,8 @@ export interface WavefrontPathTracingComputeRenderer {
|
|
|
1203
1231
|
maxFramePassesPerSubmission: number;
|
|
1204
1232
|
sceneObjectCount: number;
|
|
1205
1233
|
triangleCount: number;
|
|
1234
|
+
instanceCount: number;
|
|
1235
|
+
geometryCount: number;
|
|
1206
1236
|
emissiveTriangleCount: number;
|
|
1207
1237
|
environmentPortalCount: number;
|
|
1208
1238
|
environmentPortalMode: 0 | 1 | 2;
|
|
@@ -1221,6 +1251,7 @@ export interface WavefrontPathTracingComputeRenderer {
|
|
|
1221
1251
|
frameConfigSlots: number;
|
|
1222
1252
|
gpuParallelism: WavefrontGpuParallelismDiagnostics;
|
|
1223
1253
|
memory: WavefrontPathTracingMemoryEstimate;
|
|
1254
|
+
allocations: WavefrontGpuAllocationSnapshot;
|
|
1224
1255
|
}>;
|
|
1225
1256
|
destroy(): void;
|
|
1226
1257
|
}
|
|
@@ -1388,6 +1419,8 @@ export function estimateWavefrontPathTracingMemory(options?: {
|
|
|
1388
1419
|
maxDepth?: number;
|
|
1389
1420
|
sceneObjectCapacity?: number;
|
|
1390
1421
|
triangleCapacity?: number;
|
|
1422
|
+
instanceBytes?: number;
|
|
1423
|
+
instanceTextureBytes?: number;
|
|
1391
1424
|
materialCapacity?: number;
|
|
1392
1425
|
bvhNodeCapacity?: number;
|
|
1393
1426
|
bvhLeafSortCapacity?: number;
|
|
@@ -1557,6 +1590,7 @@ export function renderWavefrontPathTracingComputeFrame(
|
|
|
1557
1590
|
}
|
|
1558
1591
|
): Promise<WavefrontPathTracingComputeFrameStats>;
|
|
1559
1592
|
export function createWavefrontPathTracingComputeShaderSource(options?: {
|
|
1593
|
+
readonly instanced?: boolean;
|
|
1560
1594
|
workgroupSize?: number;
|
|
1561
1595
|
outputTextureFormat?: GPUTextureFormat | "rgba8unorm";
|
|
1562
1596
|
}): string;
|
|
@@ -12,6 +12,7 @@ export function createWavefrontTraceBindGroup({
|
|
|
12
12
|
counterBuffer,
|
|
13
13
|
outputView,
|
|
14
14
|
triangleBuffer,
|
|
15
|
+
instanceView,
|
|
15
16
|
bvhNodeBuffer,
|
|
16
17
|
radianceView,
|
|
17
18
|
environmentPortalBuffer,
|
|
@@ -70,6 +71,7 @@ export function createWavefrontTraceBindGroup({
|
|
|
70
71
|
{ binding: 42, resource: extensionAtlasResources.iridescence.view },
|
|
71
72
|
{ binding: 43, resource: extensionAtlasResources.iridescenceThickness.view },
|
|
72
73
|
{ binding: 44, resource: extensionAtlasResources.anisotropy.view },
|
|
74
|
+
...(instanceView ? [{binding:47, resource:instanceView}] : []),
|
|
73
75
|
],
|
|
74
76
|
});
|
|
75
77
|
}
|
package/src/wavefront-compute.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { uploadWavefrontBuffer } from './wavefront-upload.js';
|
|
2
|
+
import { planInstanceTexture, uploadInstanceTexture } from './wavefront-instance-texture.js';
|
|
3
|
+
import { createGpuAllocationTracker, openGpuAllocationErrorScopes } from './gpu-allocation-budget.js';
|
|
1
4
|
import { createWavefrontProgressiveResources } from './wavefront-progressive.js';
|
|
2
5
|
import {
|
|
3
6
|
createGpuParallelismCounters,
|
|
@@ -190,11 +193,16 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
190
193
|
throw new Error("Unable to acquire a WebGPU adapter for wavefront path tracing.");
|
|
191
194
|
}
|
|
192
195
|
|
|
193
|
-
const
|
|
196
|
+
const rawDevice = await adapter.requestDevice(
|
|
194
197
|
createWavefrontDeviceDescriptor(adapter, options, initialConfig.memory)
|
|
195
198
|
);
|
|
199
|
+
const allocationTracker = createGpuAllocationTracker(rawDevice, {budgetBytes:options.memoryBudgetBytes,storageUsage:constants.buffer.STORAGE});
|
|
200
|
+
const device = allocationTracker.device;
|
|
201
|
+
let context, finishAllocationScopes;
|
|
202
|
+
try {
|
|
203
|
+
finishAllocationScopes = openGpuAllocationErrorScopes(rawDevice);
|
|
196
204
|
const gpuAdapterParallelism = createGpuAdapterParallelismDiagnostics(adapter, device);
|
|
197
|
-
|
|
205
|
+
context = canvas.getContext("webgpu");
|
|
198
206
|
if (!context || typeof context.configure !== "function") {
|
|
199
207
|
throw new Error("Canvas WebGPU context does not support configure().");
|
|
200
208
|
}
|
|
@@ -226,13 +234,21 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
226
234
|
canvas.width = config.width;
|
|
227
235
|
canvas.height = config.height;
|
|
228
236
|
context.configure({
|
|
229
|
-
device,
|
|
237
|
+
device: rawDevice,
|
|
230
238
|
format,
|
|
231
239
|
alphaMode: options.alpha === true ? "premultiplied" : "opaque",
|
|
232
240
|
});
|
|
233
241
|
|
|
234
242
|
const bufferUsage = constants.buffer.STORAGE | constants.buffer.COPY_DST;
|
|
235
243
|
const memory = config.memory;
|
|
244
|
+
let instanceTexture = null, instanceView = null;
|
|
245
|
+
if(config.instancing) {
|
|
246
|
+
const layout=planInstanceTexture(config.instancing.instanceCount,device.limits?.maxTextureDimension2D);
|
|
247
|
+
instanceTexture=device.createTexture({label:"plasius.wavefront.instances",size:{width:layout.width,height:layout.height},
|
|
248
|
+
format:"rgba32uint",usage:constants.texture.TEXTURE_BINDING|constants.texture.COPY_DST});
|
|
249
|
+
await uploadInstanceTexture(device,instanceTexture,config.instancing.buffer,layout);
|
|
250
|
+
instanceView=instanceTexture.createView();
|
|
251
|
+
}
|
|
236
252
|
const activeQueue = createBuffer(device, bufferUsage, memory.queueBytes, "plasius.wavefront.activeQueue");
|
|
237
253
|
const nextQueue = createBuffer(device, bufferUsage, memory.queueBytes, "plasius.wavefront.nextQueue");
|
|
238
254
|
const hitBuffer = createBuffer(device, bufferUsage, memory.hitBytes, "plasius.wavefront.hitBuffer");
|
|
@@ -354,11 +370,11 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
354
370
|
packedBvhNodeFloats[nodeOffset + 2] = totalEmissiveWeight;
|
|
355
371
|
packedBvhNodeUints[nodeOffset + 8] = triangleIndex;
|
|
356
372
|
});
|
|
357
|
-
device
|
|
358
|
-
device
|
|
359
|
-
device
|
|
360
|
-
device
|
|
361
|
-
device
|
|
373
|
+
await uploadWavefrontBuffer(device, triangleBuffer, packedTriangles.buffer);
|
|
374
|
+
await uploadWavefrontBuffer(device, bvhNodeBuffer, packedBvhNodes.buffer);
|
|
375
|
+
await uploadWavefrontBuffer(device, meshVertexBuffer, config.gpuMeshSource.vertices.buffer);
|
|
376
|
+
await uploadWavefrontBuffer(device, meshIndexBuffer, config.gpuMeshSource.indices.buffer);
|
|
377
|
+
await uploadWavefrontBuffer(device, meshRangeBuffer, config.gpuMeshSource.meshes.buffer);
|
|
362
378
|
const packedEnvironmentPortals = packEnvironmentPortals(
|
|
363
379
|
config.environmentPortals,
|
|
364
380
|
Math.max(1, config.environmentPortalCapacity)
|
|
@@ -501,7 +517,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
501
517
|
},
|
|
502
518
|
computePipelines: pipelines,
|
|
503
519
|
presentPipeline,
|
|
504
|
-
} = await createWavefrontPipelineResources({ device, constants, format });
|
|
520
|
+
} = await createWavefrontPipelineResources({ device, constants, format, instanced:!!config.instancing });
|
|
505
521
|
|
|
506
522
|
function createTraceBindGroups() {
|
|
507
523
|
return createWavefrontTraceBindGroups({
|
|
@@ -516,6 +532,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
516
532
|
counterBuffer,
|
|
517
533
|
outputView,
|
|
518
534
|
triangleBuffer,
|
|
535
|
+
instanceView,
|
|
519
536
|
bvhNodeBuffer,
|
|
520
537
|
radianceView,
|
|
521
538
|
environmentPortalBuffer,
|
|
@@ -1058,6 +1075,8 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
1058
1075
|
maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
|
|
1059
1076
|
sceneObjectCount: config.sceneObjectCount,
|
|
1060
1077
|
triangleCount: config.triangleCount,
|
|
1078
|
+
instanceCount: config.instancing?.instanceCount ?? 0,
|
|
1079
|
+
geometryCount: config.instancing?.geometryCount ?? config.gpuMeshSource.meshes.count,
|
|
1061
1080
|
emissiveTriangleCount: config.emissiveTriangleCount,
|
|
1062
1081
|
environmentPortalCount: config.environmentPortalCount,
|
|
1063
1082
|
environmentPortalMode: config.environmentPortalMode,
|
|
@@ -1076,6 +1095,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
1076
1095
|
frameConfigSlots: frameConfigSlotCount,
|
|
1077
1096
|
gpuParallelism: lastGpuParallelism,
|
|
1078
1097
|
memory: config.memory,
|
|
1098
|
+
allocations: allocationTracker.snapshot(),
|
|
1079
1099
|
});
|
|
1080
1100
|
}
|
|
1081
1101
|
|
|
@@ -1089,6 +1109,7 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
1089
1109
|
pathVertexBuffer.destroy?.();
|
|
1090
1110
|
sceneObjectBuffer.destroy?.();
|
|
1091
1111
|
triangleBuffer.destroy?.();
|
|
1112
|
+
instanceTexture?.destroy?.();
|
|
1092
1113
|
bvhNodeBuffer.destroy?.();
|
|
1093
1114
|
meshVertexBuffer.destroy?.();
|
|
1094
1115
|
meshIndexBuffer.destroy?.();
|
|
@@ -1133,12 +1154,14 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
1133
1154
|
}
|
|
1134
1155
|
});
|
|
1135
1156
|
context.unconfigure?.();
|
|
1157
|
+
allocationTracker.destroy();
|
|
1136
1158
|
}
|
|
1137
1159
|
|
|
1160
|
+
await finishAllocationScopes();
|
|
1138
1161
|
return Object.freeze({
|
|
1139
1162
|
canvas,
|
|
1140
1163
|
context,
|
|
1141
|
-
device,
|
|
1164
|
+
device: rawDevice,
|
|
1142
1165
|
format,
|
|
1143
1166
|
config,
|
|
1144
1167
|
renderOnce,
|
|
@@ -1150,6 +1173,13 @@ export async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
1150
1173
|
getSnapshot,
|
|
1151
1174
|
destroy,
|
|
1152
1175
|
});
|
|
1176
|
+
} catch(error) {
|
|
1177
|
+
try { await finishAllocationScopes?.(); } catch { /* Preserve the initiating failure. */ }
|
|
1178
|
+
context?.unconfigure?.();
|
|
1179
|
+
allocationTracker.destroy();
|
|
1180
|
+
rawDevice.destroy?.();
|
|
1181
|
+
throw error;
|
|
1182
|
+
}
|
|
1153
1183
|
}
|
|
1154
1184
|
|
|
1155
1185
|
export async function renderWavefrontPathTracingComputeFrame(options = {}) {
|
package/src/wavefront-config.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { planInstanceTexture } from './wavefront-instance-texture.js';
|
|
2
|
+
import { WAVEFRONT_INSTANCE_BYTES, prepareWavefrontInstances, buildWavefrontInstances } from './wavefront-instances.js';
|
|
1
3
|
import {
|
|
2
4
|
ACCUMULATION_RECORD_BYTES,
|
|
3
5
|
BVH_LEAF_REF_RECORD_BYTES,
|
|
@@ -357,6 +359,8 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
|
|
|
357
359
|
const progressiveBytes = options.progressive === true ? width * height * 16 + 16 : 0;
|
|
358
360
|
const pathVertexBytes = tilePixelCapacity * (maxDepth + 1) * PATH_VERTEX_RECORD_BYTES;
|
|
359
361
|
const sceneObjectBytes = sceneObjectCapacity * SCENE_OBJECT_RECORD_BYTES;
|
|
362
|
+
const instanceBytes = readNonNegativeInteger("instanceBytes", options.instanceBytes, 0);
|
|
363
|
+
const instanceTextureBytes = readNonNegativeInteger("instanceTextureBytes", options.instanceTextureBytes, instanceBytes);
|
|
360
364
|
const triangleBytes = allocatedRecordBytes(triangleCapacity, TRIANGLE_RECORD_BYTES);
|
|
361
365
|
const materialTableBytes = 0;
|
|
362
366
|
const bvhNodeBytes = bvhNodeCapacity * BVH_NODE_RECORD_BYTES;
|
|
@@ -431,6 +435,8 @@ export function estimateWavefrontPathTracingMemory(options = {}) {
|
|
|
431
435
|
pathVertexBytes,
|
|
432
436
|
sceneObjectBytes,
|
|
433
437
|
triangleBytes,
|
|
438
|
+
instanceBytes,
|
|
439
|
+
instanceTextureBytes,
|
|
434
440
|
materialTableBytes,
|
|
435
441
|
bvhNodeBytes,
|
|
436
442
|
bvhCombinedBytes,
|
|
@@ -461,6 +467,8 @@ export function estimateWavefrontPathTracingMemoryForConfig(config, limits = {})
|
|
|
461
467
|
maxDepth: config.maxDepth,
|
|
462
468
|
sceneObjectCapacity: config.sceneObjectCapacity,
|
|
463
469
|
triangleCapacity: config.triangleCapacity,
|
|
470
|
+
instanceBytes: config.instancing?.instanceBytes ?? 0,
|
|
471
|
+
instanceTextureBytes: config.instancing ? planInstanceTexture(config.instancing.instanceCount, limits.maxTextureDimension2D).bytes : 0,
|
|
464
472
|
bvhNodeCapacity: config.bvhNodeCapacity,
|
|
465
473
|
bvhLeafSortCapacity: config.bvhLeafSortCapacity,
|
|
466
474
|
emissiveTriangleCapacity: config.emissiveTriangleCapacity,
|
|
@@ -482,6 +490,7 @@ export function estimateWavefrontPathTracingMemoryForConfig(config, limits = {})
|
|
|
482
490
|
export function createWavefrontPathTracingComputeConfig(options = {}) {
|
|
483
491
|
assertAnalyticDisplayQualityPolicy(options);
|
|
484
492
|
const accelerationBuildMode = resolveAccelerationBuildMode(options);
|
|
493
|
+
if(options.instances !== undefined && accelerationBuildMode === "gpu") throw new Error("Instance BLAS construction currently requires cpu-upload acceleration.");
|
|
485
494
|
const canvas = options.canvas;
|
|
486
495
|
const width = readPositiveInteger("width", options.width, getCanvasDimension(canvas, "width", DEFAULT_WIDTH));
|
|
487
496
|
const height = readPositiveInteger("height", options.height, getCanvasDimension(canvas, "height", DEFAULT_HEIGHT));
|
|
@@ -512,6 +521,14 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
|
|
|
512
521
|
);
|
|
513
522
|
const meshes = normalizeMeshes(options);
|
|
514
523
|
const meshSourceShape = estimateMeshSourceShape(meshes);
|
|
524
|
+
if(options.memoryBudgetBytes !== undefined) {
|
|
525
|
+
if(!Number.isSafeInteger(options.memoryBudgetBytes)||options.memoryBudgetBytes<=0) throw new Error("GPU memory budget must be a positive safe integer.");
|
|
526
|
+
const count = options.instances?.length ?? 0;
|
|
527
|
+
const instanceMinimumBytes = count * WAVEFRONT_INSTANCE_BYTES + Math.max(0, count * 2 - 1) * BVH_NODE_RECORD_BYTES;
|
|
528
|
+
const geometryMinimumBytes = meshSourceShape.triangleCount * TRIANGLE_RECORD_BYTES + instanceMinimumBytes;
|
|
529
|
+
if(!Number.isSafeInteger(geometryMinimumBytes)||geometryMinimumBytes>options.memoryBudgetBytes) throw new Error("Unique geometry exceeds the GPU memory budget before acceleration construction.");
|
|
530
|
+
}
|
|
531
|
+
const instancePlan = options.instances === undefined ? null : prepareWavefrontInstances(meshes, options.instances);
|
|
515
532
|
const gpuMaterialSource =
|
|
516
533
|
meshes.length > 0
|
|
517
534
|
? createWavefrontGpuMaterialSource(meshes)
|
|
@@ -520,10 +537,11 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
|
|
|
520
537
|
meshes.length > 0
|
|
521
538
|
? createWavefrontGpuMeshSource(meshes, gpuMaterialSource)
|
|
522
539
|
: createWavefrontGpuMeshSource([]);
|
|
523
|
-
const
|
|
540
|
+
const instancing = instancePlan ? buildWavefrontInstances(meshes, instancePlan, gpuMaterialSource) : null;
|
|
541
|
+
const meshAcceleration = instancing?.acceleration ?? (
|
|
524
542
|
accelerationBuildMode === "cpu-upload"
|
|
525
543
|
? createWavefrontMeshAcceleration(meshes, gpuMaterialSource)
|
|
526
|
-
: Object.freeze({ nodes: Object.freeze([]), triangles: Object.freeze([]) });
|
|
544
|
+
: Object.freeze({ nodes: Object.freeze([]), triangles: Object.freeze([]) }));
|
|
527
545
|
const emissiveTriangleIndices = createWavefrontEmissiveTriangleIndexSource(
|
|
528
546
|
meshes,
|
|
529
547
|
options.emissiveTriangleCapacity
|
|
@@ -625,6 +643,7 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
|
|
|
625
643
|
gpuAccelerationBuildRequired: accelerationBuildMode === "gpu" && triangleCount > 0,
|
|
626
644
|
gpuMeshSource,
|
|
627
645
|
gpuMaterialSource,
|
|
646
|
+
instancing,
|
|
628
647
|
meshAcceleration,
|
|
629
648
|
emissiveTriangleIndices,
|
|
630
649
|
emissiveTriangleCount: emissiveTriangleIndices.count,
|
|
@@ -663,6 +682,7 @@ export function createWavefrontPathTracingComputeConfig(options = {}) {
|
|
|
663
682
|
tilePixelCapacity,
|
|
664
683
|
maxDepth,
|
|
665
684
|
sceneObjectCapacity,
|
|
685
|
+
instanceBytes: instancing?.instanceBytes ?? 0,
|
|
666
686
|
triangleCapacity,
|
|
667
687
|
bvhNodeCapacity,
|
|
668
688
|
bvhLeafSortCapacity,
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export const WAVEFRONT_INSTANCE_SHADER_WGSL = `
|
|
2
|
+
struct MeshInstance {
|
|
3
|
+
world: mat4x4<f32>,
|
|
4
|
+
inverse: mat4x4<f32>,
|
|
5
|
+
objectId: u32,
|
|
6
|
+
root: u32,
|
|
7
|
+
handedness: f32,
|
|
8
|
+
_pad: u32,
|
|
9
|
+
};
|
|
10
|
+
@group(0) @binding(47) var meshInstances: texture_2d<u32>;
|
|
11
|
+
fn read_mesh_instance(index: u32) -> MeshInstance {
|
|
12
|
+
let recordsPerRow = textureDimensions(meshInstances).x / 9u;
|
|
13
|
+
let x = i32((index % recordsPerRow) * 9u);
|
|
14
|
+
let y = i32(index / recordsPerRow);
|
|
15
|
+
let metadata = textureLoad(meshInstances, vec2<i32>(x + 8, y), 0);
|
|
16
|
+
return MeshInstance(
|
|
17
|
+
mat4x4<f32>(bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(x, y), 0)),
|
|
18
|
+
bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(x + 1, y), 0)),
|
|
19
|
+
bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(x + 2, y), 0)),
|
|
20
|
+
bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(x + 3, y), 0))),
|
|
21
|
+
mat4x4<f32>(bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(x + 4, y), 0)),
|
|
22
|
+
bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(x + 5, y), 0)),
|
|
23
|
+
bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(x + 6, y), 0)),
|
|
24
|
+
bitcast<vec4<f32>>(textureLoad(meshInstances, vec2<i32>(x + 7, y), 0))),
|
|
25
|
+
metadata.x, metadata.y, bitcast<f32>(metadata.z), 0u);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
fn instance_normal(instance: MeshInstance, normal: vec3<f32>) -> vec3<f32> {
|
|
29
|
+
let normalMatrix = transpose(mat3x3<f32>(instance.inverse[0].xyz, instance.inverse[1].xyz, instance.inverse[2].xyz));
|
|
30
|
+
return safe_normalize(normalMatrix * normal, vec3<f32>(0.0, 1.0, 0.0));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fn triangle_for_candidate(candidate: Candidate) -> TriangleRecord {
|
|
34
|
+
var triangle = triangles[candidate.triangleIndex];
|
|
35
|
+
let instance = read_mesh_instance(candidate.instanceIndex);
|
|
36
|
+
triangle.v0 = instance.world * vec4<f32>(triangle.v0.xyz, 1.0);
|
|
37
|
+
triangle.v1 = instance.world * vec4<f32>(triangle.v1.xyz, 1.0);
|
|
38
|
+
triangle.v2 = instance.world * vec4<f32>(triangle.v2.xyz, 1.0);
|
|
39
|
+
triangle.meshId = instance.objectId;
|
|
40
|
+
return triangle;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fn intersect_bvh(ray: RayRecord, initialNearest: f32) -> Candidate {
|
|
44
|
+
var nearest = initialNearest;
|
|
45
|
+
var best = no_candidate();
|
|
46
|
+
var stack = array<u32, 64>();
|
|
47
|
+
var stackSize = 1u;
|
|
48
|
+
stack[0] = 0u;
|
|
49
|
+
loop {
|
|
50
|
+
if (stackSize == 0u) { break; }
|
|
51
|
+
stackSize -= 1u;
|
|
52
|
+
let node = bvhNodes[stack[stackSize]];
|
|
53
|
+
if (!intersect_bounds(ray, node.boundsMin.xyz, node.boundsMax.xyz, nearest)) { continue; }
|
|
54
|
+
if (node.triangleCount == 0u) {
|
|
55
|
+
stack[stackSize] = node.childOrFirst;
|
|
56
|
+
stack[stackSize + 1u] = node.rightChild;
|
|
57
|
+
stackSize += 2u;
|
|
58
|
+
} else {
|
|
59
|
+
let instanceIndex = node.childOrFirst;
|
|
60
|
+
let instance = read_mesh_instance(instanceIndex);
|
|
61
|
+
var localRay = ray;
|
|
62
|
+
localRay.origin = instance.inverse * vec4<f32>(ray.origin.xyz, 1.0);
|
|
63
|
+
// Keep this direction unnormalized so t remains the world-space ray parameter.
|
|
64
|
+
localRay.direction = instance.inverse * vec4<f32>(ray.direction.xyz, 0.0);
|
|
65
|
+
var candidate = intersect_geometry_bvh(localRay, nearest, instance.root);
|
|
66
|
+
if (candidate.hit == 1u && candidate.distance < nearest) {
|
|
67
|
+
candidate.geometricNormal = instance_normal(instance, candidate.geometricNormal);
|
|
68
|
+
candidate.shadingNormal = repair_shading_normal(candidate.geometricNormal,
|
|
69
|
+
instance_normal(instance, candidate.shadingNormal));
|
|
70
|
+
if (instance.handedness < 0.0) { candidate.frontFace = 1u - candidate.frontFace; }
|
|
71
|
+
candidate.instanceIndex = instanceIndex;
|
|
72
|
+
nearest = candidate.distance;
|
|
73
|
+
best = candidate;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return best;
|
|
78
|
+
}
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
export const WAVEFRONT_UNINSTANCED_SHADER_WGSL = `
|
|
82
|
+
fn triangle_for_candidate(candidate: Candidate) -> TriangleRecord {
|
|
83
|
+
return triangles[candidate.triangleIndex];
|
|
84
|
+
}
|
|
85
|
+
fn intersect_bvh(ray: RayRecord, initialNearest: f32) -> Candidate {
|
|
86
|
+
return intersect_geometry_bvh(ray, initialNearest, 0u);
|
|
87
|
+
}
|
|
88
|
+
`;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { waitForSubmittedGpuWork } from './wavefront-gpu-synchronization.js';
|
|
2
|
+
import { WAVEFRONT_INSTANCE_BYTES } from './wavefront-instances.js';
|
|
3
|
+
|
|
4
|
+
const TEXELS_PER_RECORD = WAVEFRONT_INSTANCE_BYTES / 16;
|
|
5
|
+
|
|
6
|
+
/** Whole records never cross rows; limits come from the granted GPU device. */
|
|
7
|
+
export function planInstanceTexture(count, maxDimension = 8192) {
|
|
8
|
+
if (!Number.isSafeInteger(count) || count < 1 || count >= 0xffffffff) {
|
|
9
|
+
throw new Error('Instance count must fit unsigned shader addressing.');
|
|
10
|
+
}
|
|
11
|
+
if (!Number.isSafeInteger(maxDimension) || maxDimension < TEXELS_PER_RECORD) {
|
|
12
|
+
throw new Error('GPU instance texture dimension cannot hold one record.');
|
|
13
|
+
}
|
|
14
|
+
const recordsPerRow = Math.min(count, Math.floor(maxDimension / TEXELS_PER_RECORD));
|
|
15
|
+
const width = recordsPerRow * TEXELS_PER_RECORD, height = Math.ceil(count / recordsPerRow);
|
|
16
|
+
if (height > maxDimension) throw new Error('GPU instance texture dimension limit exceeded.');
|
|
17
|
+
const bytes = width * height * 16;
|
|
18
|
+
if (!Number.isSafeInteger(bytes)) throw new Error('Instance texture byte size is out of range.');
|
|
19
|
+
return Object.freeze({ count, recordsPerRow, width, height, bytes });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Queue writes have no 256-byte row-alignment requirement. Bound staging by rows. */
|
|
23
|
+
export async function uploadInstanceTexture(device, texture, buffer, layout, { chunkBytes = 8 * 1024 * 1024 } = {}) {
|
|
24
|
+
const bytesPerRow = layout.width * 16;
|
|
25
|
+
if (!Number.isSafeInteger(chunkBytes) || chunkBytes < bytesPerRow) {
|
|
26
|
+
throw new Error('Instance upload chunk must hold at least one texture row.');
|
|
27
|
+
}
|
|
28
|
+
if (buffer.byteLength !== layout.count * WAVEFRONT_INSTANCE_BYTES) {
|
|
29
|
+
throw new Error('Instance upload does not match its record count.');
|
|
30
|
+
}
|
|
31
|
+
const source = new Uint8Array(buffer), rowsPerChunk = Math.floor(chunkBytes / bytesPerRow);
|
|
32
|
+
for (let y = 0; y < layout.height; y += rowsPerChunk) {
|
|
33
|
+
const rows = Math.min(rowsPerChunk, layout.height - y), offset = y * bytesPerRow;
|
|
34
|
+
const length = rows * bytesPerRow;
|
|
35
|
+
let chunk = source.subarray(offset, offset + length);
|
|
36
|
+
if (chunk.byteLength < length) {
|
|
37
|
+
const padded = new Uint8Array(length); padded.set(chunk); chunk = padded;
|
|
38
|
+
}
|
|
39
|
+
device.queue.writeTexture({ texture, origin: { x: 0, y, z: 0 } }, chunk,
|
|
40
|
+
{ bytesPerRow, rowsPerImage: rows }, { width: layout.width, height: rows, depthOrArrayLayers: 1 });
|
|
41
|
+
await waitForSubmittedGpuWork(device, { timeoutMs: 10000, maxWaitMs: 10000, allowTimeout: false });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { buildWavefrontBoundsBvh, createWavefrontMeshAcceleration } from './wavefront-mesh-sources.js';
|
|
2
|
+
import { emissionPower, MATERIAL_EMISSIVE } from './wavefront-core.js';
|
|
3
|
+
|
|
4
|
+
export const WAVEFRONT_INSTANCE_BYTES = 144;
|
|
5
|
+
const freeze = Object.freeze;
|
|
6
|
+
const uint = value => Number.isInteger(value) && value >= 0 && value < 0xffffffff;
|
|
7
|
+
|
|
8
|
+
function affineTransform(value) {
|
|
9
|
+
if ((!Array.isArray(value) && !ArrayBuffer.isView(value)) || value.length !== 16 ||
|
|
10
|
+
!Array.from(value).every(n => Number.isFinite(n) && Math.abs(n) <= 1e12) ||
|
|
11
|
+
value[3] !== 0 || value[7] !== 0 || value[11] !== 0 || value[15] !== 1) {
|
|
12
|
+
throw new Error('Instance transform must be a finite affine column-major matrix.');
|
|
13
|
+
}
|
|
14
|
+
// Round to the actual shader representation before inversion and bounds construction.
|
|
15
|
+
const m = Array.from(value, Math.fround);
|
|
16
|
+
const [a,b,c,,d,e,f,,g,h,i] = m;
|
|
17
|
+
const determinant = a*(e*i-f*h)-d*(b*i-c*h)+g*(b*f-c*e);
|
|
18
|
+
if (!Number.isFinite(determinant) || Math.abs(determinant) < 1e-12) {
|
|
19
|
+
throw new Error('Instance transform must be invertible.');
|
|
20
|
+
}
|
|
21
|
+
const inv = [(e*i-f*h)/determinant,(c*h-b*i)/determinant,(b*f-c*e)/determinant,0,
|
|
22
|
+
(f*g-d*i)/determinant,(a*i-c*g)/determinant,(c*d-a*f)/determinant,0,
|
|
23
|
+
(d*h-e*g)/determinant,(b*g-a*h)/determinant,(a*e-b*d)/determinant,0,0,0,0,1];
|
|
24
|
+
for (let r=0;r<3;r++) inv[12+r]=-(inv[r]*m[12]+inv[4+r]*m[13]+inv[8+r]*m[14]);
|
|
25
|
+
if (!inv.every(n=>Number.isFinite(n) && Math.abs(n)<=1e12)) throw new Error('Instance inverse transform is out of range.');
|
|
26
|
+
return {transform:freeze(m),inverse:freeze(inv.map(n=>n===0?0:n)),handedness:Math.sign(determinant)};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function transformInstancePoint(m, p) {
|
|
30
|
+
return [0,1,2].map(r=>m[r]*p[0]+m[4+r]*p[1]+m[8+r]*p[2]+m[12+r]);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function meshBounds(mesh) {
|
|
34
|
+
const localMin=[Infinity,Infinity,Infinity],localMax=[-Infinity,-Infinity,-Infinity];
|
|
35
|
+
for(let i=0;i<mesh.positions.length;i+=3) for(let a=0;a<3;a++) {
|
|
36
|
+
localMin[a]=Math.min(localMin[a],Math.fround(mesh.positions[i+a]));localMax[a]=Math.max(localMax[a],Math.fround(mesh.positions[i+a]));
|
|
37
|
+
}
|
|
38
|
+
return {min:localMin,max:localMax};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function boundsOf(local, transform) {
|
|
42
|
+
const localMin=local.min,localMax=local.max;
|
|
43
|
+
const min=[Infinity,Infinity,Infinity],max=[-Infinity,-Infinity,-Infinity];
|
|
44
|
+
for(let corner=0;corner<8;corner++) {
|
|
45
|
+
const p=transformInstancePoint(transform,[0,1,2].map(a=>(corner&(1<<a))?localMax[a]:localMin[a]));
|
|
46
|
+
for(let a=0;a<3;a++){min[a]=Math.min(min[a],p[a]);max[a]=Math.max(max[a],p[a]);}
|
|
47
|
+
}
|
|
48
|
+
if(![...min,...max].every(Number.isFinite)) throw new Error('Instance mesh has invalid bounds.');
|
|
49
|
+
return freeze({min:freeze(min),max:freeze(max)});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function prepareWavefrontInstances(meshes, instances) {
|
|
53
|
+
if(!Array.isArray(instances)||instances.length===0) throw new Error('Instances must be a nonempty array.');
|
|
54
|
+
const byId=new Map(),localBounds=new Map();
|
|
55
|
+
for(const mesh of meshes) {
|
|
56
|
+
if(!uint(mesh.id)||byId.has(mesh.id)) throw new Error('Instanced mesh IDs must be unique unsigned integers.');
|
|
57
|
+
if(mesh.materialKind===MATERIAL_EMISSIVE||emissionPower(mesh.emission??[0,0,0,1])>0.0001) {
|
|
58
|
+
throw new Error('Instanced emissive meshes require per-instance light sampling, which is not supported yet.');
|
|
59
|
+
}
|
|
60
|
+
byId.set(mesh.id,mesh);localBounds.set(mesh.id,meshBounds(mesh));
|
|
61
|
+
}
|
|
62
|
+
const ids=new Set(),used=new Set();
|
|
63
|
+
const records=instances.map(instance=>{
|
|
64
|
+
if(!instance||!uint(instance.id)||ids.has(instance.id)) throw new Error('Instance IDs must be unique unsigned integers.');
|
|
65
|
+
const mesh=byId.get(instance.meshId);
|
|
66
|
+
if(!mesh) throw new Error('Instance references an unknown mesh.');
|
|
67
|
+
ids.add(instance.id);used.add(mesh.id);
|
|
68
|
+
const affine=affineTransform(instance.transform);
|
|
69
|
+
const bounds=boundsOf(localBounds.get(mesh.id),affine.transform);
|
|
70
|
+
return freeze({...affine,id:instance.id,meshId:mesh.id,bounds,
|
|
71
|
+
centroid:freeze(bounds.min.map((v,a)=>(v+bounds.max[a])/2))});
|
|
72
|
+
});
|
|
73
|
+
if(used.size!==meshes.length) throw new Error('Every instanced mesh must have at least one instance.');
|
|
74
|
+
return freeze({records:freeze(records),geometryCount:meshes.length});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function buildWavefrontInstances(meshes, plan, materialSource) {
|
|
78
|
+
const tlas=buildWavefrontBoundsBvh(plan.records,1);
|
|
79
|
+
// Outward padding covers f32 matrix multiplication and packed-bound rounding.
|
|
80
|
+
const nodes=tlas.nodes.map(node=>{
|
|
81
|
+
const pad=node.bounds.min.map((v,a)=>Math.max(1,Math.abs(v),Math.abs(node.bounds.max[a]))*1e-6);
|
|
82
|
+
return freeze({...node,bounds:freeze({min:freeze(node.bounds.min.map((v,a)=>v-pad[a])),
|
|
83
|
+
max:freeze(node.bounds.max.map((v,a)=>v+pad[a]))})});
|
|
84
|
+
}),triangles=[],roots=new Map();
|
|
85
|
+
for(const [meshIndex,mesh] of meshes.entries()) {
|
|
86
|
+
const blas=createWavefrontMeshAcceleration([mesh],materialSource);
|
|
87
|
+
const nodeOffset=nodes.length,triangleOffset=triangles.length;
|
|
88
|
+
roots.set(mesh.id,nodeOffset);
|
|
89
|
+
for(const node of blas.nodes) nodes.push(freeze({...node,
|
|
90
|
+
firstTriangle:node.firstTriangle+(node.triangleCount>0?triangleOffset:nodeOffset),
|
|
91
|
+
leftChild:node.leftChild+nodeOffset,rightChild:node.rightChild+nodeOffset}));
|
|
92
|
+
for(const triangle of blas.triangles) triangles.push(freeze({...triangle,
|
|
93
|
+
triangleId:triangle.triangleId+triangleOffset,materialSlot:meshIndex}));
|
|
94
|
+
}
|
|
95
|
+
const records=tlas.triangles.map(record=>freeze({...record,root:roots.get(record.meshId)}));
|
|
96
|
+
const buffer=new ArrayBuffer(records.length*WAVEFRONT_INSTANCE_BYTES);
|
|
97
|
+
const floats=new Float32Array(buffer),uints=new Uint32Array(buffer);
|
|
98
|
+
records.forEach((record,index)=>{
|
|
99
|
+
const offset=index*36;
|
|
100
|
+
floats.set(record.transform,offset);floats.set(record.inverse,offset+16);
|
|
101
|
+
uints[offset+32]=record.id;uints[offset+33]=record.root;floats[offset+34]=record.handedness;
|
|
102
|
+
});
|
|
103
|
+
return freeze({records:freeze(records),buffer,instanceCount:records.length,
|
|
104
|
+
geometryCount:meshes.length,instanceBytes:buffer.byteLength,tlasNodeCount:tlas.nodes.length,
|
|
105
|
+
blasNodeCount:nodes.length-tlas.nodes.length,
|
|
106
|
+
acceleration:freeze({nodes:freeze(nodes),triangles:freeze(triangles)})});
|
|
107
|
+
}
|