@plasius/gpu-renderer 0.2.2 → 0.2.3
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 +12 -5
- package/README.md +7 -3
- package/dist/index.cjs +143 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +143 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.d.ts +5 -0
- package/src/wavefront-compute.js +150 -8
package/CHANGELOG.md
CHANGED
|
@@ -23,16 +23,23 @@ All notable changes to this project will be documented in this file.
|
|
|
23
23
|
- **Security**
|
|
24
24
|
- (placeholder)
|
|
25
25
|
|
|
26
|
-
## [0.2.
|
|
26
|
+
## [0.2.3] - 2026-06-11
|
|
27
27
|
|
|
28
28
|
- **Added**
|
|
29
|
-
- (
|
|
29
|
+
- Added `updateCamera(...)` support for wavefront renderers so validation
|
|
30
|
+
views can animate camera movement without rebuilding mesh buffers.
|
|
30
31
|
|
|
31
32
|
- **Changed**
|
|
32
|
-
-
|
|
33
|
+
- Changed wavefront frame dispatch to split large tile/sample workloads into
|
|
34
|
+
bounded command submissions instead of encoding an entire high-resolution
|
|
35
|
+
frame into one command buffer.
|
|
33
36
|
|
|
34
37
|
- **Fixed**
|
|
35
|
-
-
|
|
38
|
+
- Fixed low-sample wavefront renders so non-emissive surface hits receive a
|
|
39
|
+
deterministic sky, sun, and portal-light estimate before random continuation.
|
|
40
|
+
- Reduced deterministic environment fill on direct surface hits so ambient
|
|
41
|
+
rescue lighting no longer washes dark materials toward the full scene
|
|
42
|
+
ambient colour.
|
|
36
43
|
|
|
37
44
|
- **Security**
|
|
38
45
|
- (placeholder)
|
|
@@ -326,4 +333,4 @@ All notable changes to this project will be documented in this file.
|
|
|
326
333
|
[0.1.12]: https://github.com/Plasius-LTD/gpu-renderer/releases/tag/v0.1.12
|
|
327
334
|
[0.1.14]: https://github.com/Plasius-LTD/gpu-renderer/releases/tag/v0.1.14
|
|
328
335
|
[0.2.1]: https://github.com/Plasius-LTD/gpu-renderer/releases/tag/v0.2.1
|
|
329
|
-
[0.2.
|
|
336
|
+
[0.2.3]: https://github.com/Plasius-LTD/gpu-renderer/releases/tag/v0.2.3
|
package/README.md
CHANGED
|
@@ -207,14 +207,18 @@ sampling, temporal accumulation, and better material PDFs are hardened.
|
|
|
207
207
|
For static mesh scenes, the GPU acceleration build is submitted once and then
|
|
208
208
|
reused by subsequent frames. Per-frame tracing writes one dynamic uniform slot
|
|
209
209
|
per tile/sample or post-process pass and batches tile tracing, tile output,
|
|
210
|
-
optional denoise, and presentation into
|
|
210
|
+
optional denoise, and presentation into bounded command submissions controlled
|
|
211
|
+
by `maxFramePassesPerSubmission` to keep 4K/high-spp command buffers from
|
|
212
|
+
becoming oversized. `updateCamera(...)` can update the per-frame camera uniforms
|
|
213
|
+
between renders without rebuilding mesh buffers. After each
|
|
211
214
|
primary-ray or compaction pass, the GPU writes the active-ray workgroup count
|
|
212
215
|
into the counter buffer and the encoder copies it into an indirect-dispatch
|
|
213
216
|
argument buffer. Intersection and surface-resolution passes therefore scale
|
|
214
217
|
with active continuation rays instead of the maximum tile capacity, while still
|
|
215
218
|
avoiding CPU readback between bounces. WebGPU
|
|
216
|
-
still preserves ordering between dependent bounce passes, but the renderer
|
|
217
|
-
|
|
219
|
+
still preserves ordering between dependent bounce passes, but the renderer
|
|
220
|
+
keeps CPU queue submissions bounded rather than forcing one submission per
|
|
221
|
+
tile/sample.
|
|
218
222
|
Environment-light portals can additionally guide and gate sky/HDRI contribution
|
|
219
223
|
through rectangular openings such as windows. `environmentPortalMode: "guide"`
|
|
220
224
|
biases diffuse continuation rays toward configured openings, while
|
package/dist/index.cjs
CHANGED
|
@@ -76,6 +76,7 @@ var DEFAULT_HEIGHT = 720;
|
|
|
76
76
|
var DEFAULT_MAX_DEPTH = 6;
|
|
77
77
|
var DEFAULT_TILE_SIZE = 128;
|
|
78
78
|
var DEFAULT_SAMPLES_PER_PIXEL = 1;
|
|
79
|
+
var DEFAULT_MAX_FRAME_PASSES_PER_SUBMISSION = 256;
|
|
79
80
|
var DEFAULT_SCENE_OBJECT_CAPACITY = 128;
|
|
80
81
|
var DEFAULT_ENVIRONMENT_PORTAL_CAPACITY = 32;
|
|
81
82
|
var WORKGROUP_SIZE = 64;
|
|
@@ -1106,6 +1107,15 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
|
|
|
1106
1107
|
1,
|
|
1107
1108
|
64
|
|
1108
1109
|
);
|
|
1110
|
+
const maxFramePassesPerSubmission = clamp(
|
|
1111
|
+
readPositiveInteger(
|
|
1112
|
+
"maxFramePassesPerSubmission",
|
|
1113
|
+
options.maxFramePassesPerSubmission,
|
|
1114
|
+
DEFAULT_MAX_FRAME_PASSES_PER_SUBMISSION
|
|
1115
|
+
),
|
|
1116
|
+
1,
|
|
1117
|
+
4096
|
|
1118
|
+
);
|
|
1109
1119
|
const tilePixelCapacity = readPositiveInteger(
|
|
1110
1120
|
"tilePixelCapacity",
|
|
1111
1121
|
options.tilePixelCapacity,
|
|
@@ -1173,6 +1183,7 @@ function createWavefrontPathTracingComputeConfig(options = {}) {
|
|
|
1173
1183
|
maxDepth,
|
|
1174
1184
|
tileSize,
|
|
1175
1185
|
samplesPerPixel,
|
|
1186
|
+
maxFramePassesPerSubmission,
|
|
1176
1187
|
tilePixelCapacity,
|
|
1177
1188
|
sceneObjects,
|
|
1178
1189
|
sceneObjectCount: sceneObjects.length,
|
|
@@ -2039,6 +2050,80 @@ fn terminal_surface_environment_contribution(ray: RayRecord, hit: HitRecord) ->
|
|
|
2039
2050
|
return clamp_sample_radiance(ray.throughput.xyz * surfaceColor * environmentFloor * materialFloor);
|
|
2040
2051
|
}
|
|
2041
2052
|
|
|
2053
|
+
fn direct_environment_portal_irradiance(origin: vec3<f32>, normal: vec3<f32>) -> vec3<f32> {
|
|
2054
|
+
if (config.environmentPortalCount == 0u || config.environmentPortalMode == 0u) {
|
|
2055
|
+
return vec3<f32>(0.0);
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
var irradiance = vec3<f32>(0.0);
|
|
2059
|
+
for (var portalIndex = 0u; portalIndex < config.environmentPortalCount; portalIndex = portalIndex + 1u) {
|
|
2060
|
+
let portal = environmentPortals[portalIndex];
|
|
2061
|
+
if (portal.kind != 1u) {
|
|
2062
|
+
continue;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
let toPortal = portal.position.xyz - origin;
|
|
2066
|
+
let distanceSquared = max(dot(toPortal, toPortal), 0.01);
|
|
2067
|
+
let direction = safe_normalize(toPortal, normal);
|
|
2068
|
+
let surfaceFacing = saturate(dot(normal, direction));
|
|
2069
|
+
if (surfaceFacing <= 0.0001) {
|
|
2070
|
+
continue;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
let portalNormal = safe_normalize(portal.normal.xyz, vec3<f32>(0.0, 0.0, 1.0));
|
|
2074
|
+
let twoSided = (portal.flags & 1u) != 0u;
|
|
2075
|
+
let portalFacing = select(
|
|
2076
|
+
saturate(dot(-direction, portalNormal)),
|
|
2077
|
+
max(abs(dot(direction, portalNormal)), 0.15),
|
|
2078
|
+
twoSided
|
|
2079
|
+
);
|
|
2080
|
+
let area = max(portal.position.w, 0.0001);
|
|
2081
|
+
let distanceFalloff = clamp(area / max(distanceSquared, area * 0.25), 0.0, 2.5);
|
|
2082
|
+
irradiance = irradiance +
|
|
2083
|
+
portal.color.rgb *
|
|
2084
|
+
portal.normal.w *
|
|
2085
|
+
portal.color.a *
|
|
2086
|
+
surfaceFacing *
|
|
2087
|
+
portalFacing *
|
|
2088
|
+
distanceFalloff;
|
|
2089
|
+
}
|
|
2090
|
+
return irradiance;
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
fn surface_direct_environment_contribution(ray: RayRecord, hit: HitRecord) -> vec3<f32> {
|
|
2094
|
+
let normal = safe_normalize(hit.shadingNormal.xyz, vec3<f32>(0.0, 1.0, 0.0));
|
|
2095
|
+
let origin = hit.position.xyz + normal * 0.003;
|
|
2096
|
+
let viewDirection = safe_normalize(-ray.direction.xyz, normal);
|
|
2097
|
+
let surfaceColor = clamp(max(hit.color.xyz, config.ambientColor.xyz * 0.35), vec3<f32>(0.0), vec3<f32>(1.0));
|
|
2098
|
+
let roughness = clamp(hit.material.x, 0.0, 1.0);
|
|
2099
|
+
let metallic = clamp(hit.material.y, 0.0, 1.0);
|
|
2100
|
+
|
|
2101
|
+
let normalEnvironment = gated_environment_radiance(origin, normal);
|
|
2102
|
+
let skyVisibility = 0.35 + saturate(normal.y * 0.5 + 0.5) * 0.45;
|
|
2103
|
+
let skyIrradiance = max(config.ambientColor.xyz * 0.72, normalEnvironment * skyVisibility * 0.16);
|
|
2104
|
+
|
|
2105
|
+
let sunDirection = safe_normalize(
|
|
2106
|
+
config.environmentSunDirectionIntensity.xyz,
|
|
2107
|
+
vec3<f32>(0.0, 1.0, 0.0)
|
|
2108
|
+
);
|
|
2109
|
+
let sunFacing = saturate(dot(normal, sunDirection));
|
|
2110
|
+
let sunRadiance = gated_environment_radiance(origin, sunDirection);
|
|
2111
|
+
let sunIrradiance = sunRadiance * sunFacing * 0.2;
|
|
2112
|
+
let portalIrradiance = direct_environment_portal_irradiance(origin, normal);
|
|
2113
|
+
|
|
2114
|
+
let diffuseWeight = select(1.0 - metallic * 0.65, 0.22, hit.materialKind == 1u);
|
|
2115
|
+
let diffuse = surfaceColor * (skyIrradiance + sunIrradiance + portalIrradiance) * diffuseWeight;
|
|
2116
|
+
|
|
2117
|
+
let halfVector = safe_normalize(sunDirection + viewDirection, normal);
|
|
2118
|
+
let specularPower = 8.0 + (1.0 - roughness) * 96.0;
|
|
2119
|
+
let specularFacing = pow(saturate(dot(normal, halfVector)), specularPower) * sunFacing;
|
|
2120
|
+
let specularTint = mix(vec3<f32>(0.04), surfaceColor, metallic);
|
|
2121
|
+
let specular = specularTint * sunRadiance * specularFacing * select(0.16, 0.48, hit.materialKind == 1u || hit.materialKind == 2u);
|
|
2122
|
+
|
|
2123
|
+
let bounceWeight = select(1.0, 0.38, ray.bounce > 0u);
|
|
2124
|
+
return clamp_sample_radiance(ray.throughput.xyz * (diffuse + specular) * bounceWeight);
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2042
2127
|
fn default_mesh_range() -> MeshRange {
|
|
2043
2128
|
return MeshRange(
|
|
2044
2129
|
0u,
|
|
@@ -2908,6 +2993,14 @@ fn resolveSurfaceRecords(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
|
2908
2993
|
return;
|
|
2909
2994
|
}
|
|
2910
2995
|
|
|
2996
|
+
let shouldEstimateDirectEnvironment =
|
|
2997
|
+
(hit.materialKind == 0u || hit.materialKind == 1u) && hit.material.z >= 0.95;
|
|
2998
|
+
if (shouldEstimateDirectEnvironment) {
|
|
2999
|
+
let directEnvironment = surface_direct_environment_contribution(ray, hit);
|
|
3000
|
+
accumulation[ray.rayId] =
|
|
3001
|
+
accumulation[ray.rayId] + vec4<f32>(directEnvironment * sample_weight(), 0.0);
|
|
3002
|
+
}
|
|
3003
|
+
|
|
2911
3004
|
if (ray.bounce + 1u >= config.maxDepth) {
|
|
2912
3005
|
let terminalEnvironment = terminal_surface_environment_contribution(ray, hit);
|
|
2913
3006
|
accumulation[ray.rayId] =
|
|
@@ -3583,6 +3676,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
3583
3676
|
let frame = 0;
|
|
3584
3677
|
let accelerationBuilt = !config.gpuAccelerationBuildRequired;
|
|
3585
3678
|
let accelerationBuildCount = 0;
|
|
3679
|
+
let activeCameraOptions = options.camera ?? DEFAULT_CAMERA;
|
|
3586
3680
|
function createFrameConfigWriter(frameIndex) {
|
|
3587
3681
|
let slot = 0;
|
|
3588
3682
|
return (tile, buildRange = {}) => {
|
|
@@ -3742,33 +3836,53 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
3742
3836
|
}
|
|
3743
3837
|
function dispatchFrame(frameIndex) {
|
|
3744
3838
|
const writeFrameConfig = createFrameConfigWriter(frameIndex);
|
|
3745
|
-
|
|
3746
|
-
|
|
3839
|
+
let submissionCount = 0;
|
|
3840
|
+
let encodedFramePasses = 0;
|
|
3841
|
+
let encoder = device.createCommandEncoder({
|
|
3842
|
+
label: `plasius.wavefront.frame.${frameIndex}.batched.${submissionCount + 1}`
|
|
3747
3843
|
});
|
|
3844
|
+
function submitCurrentEncoder() {
|
|
3845
|
+
if (encodedFramePasses <= 0) {
|
|
3846
|
+
return;
|
|
3847
|
+
}
|
|
3848
|
+
device.queue.submit([encoder.finish()]);
|
|
3849
|
+
submissionCount += 1;
|
|
3850
|
+
encodedFramePasses = 0;
|
|
3851
|
+
encoder = device.createCommandEncoder({
|
|
3852
|
+
label: `plasius.wavefront.frame.${frameIndex}.batched.${submissionCount + 1}`
|
|
3853
|
+
});
|
|
3854
|
+
}
|
|
3855
|
+
function reserveEncoder(passCount = 1) {
|
|
3856
|
+
if (encodedFramePasses > 0 && encodedFramePasses + passCount > config.maxFramePassesPerSubmission) {
|
|
3857
|
+
submitCurrentEncoder();
|
|
3858
|
+
}
|
|
3859
|
+
encodedFramePasses += passCount;
|
|
3860
|
+
return encoder;
|
|
3861
|
+
}
|
|
3748
3862
|
for (const tile of tiles) {
|
|
3749
3863
|
for (let sampleIndex = 0; sampleIndex < config.samplesPerPixel; sampleIndex += 1) {
|
|
3750
3864
|
const configOffset = writeFrameConfig(tile, {
|
|
3751
3865
|
sampleIndex,
|
|
3752
3866
|
sampleWeight: 1 / config.samplesPerPixel
|
|
3753
3867
|
});
|
|
3754
|
-
encodeTileSample(
|
|
3868
|
+
encodeTileSample(reserveEncoder(), tile, configOffset);
|
|
3755
3869
|
}
|
|
3756
3870
|
const outputConfigOffset = writeFrameConfig(tile, {
|
|
3757
3871
|
sampleIndex: 0,
|
|
3758
3872
|
sampleWeight: 1 / config.samplesPerPixel
|
|
3759
3873
|
});
|
|
3760
|
-
encodeTileOutput(
|
|
3874
|
+
encodeTileOutput(reserveEncoder(), tile, outputConfigOffset);
|
|
3761
3875
|
}
|
|
3762
3876
|
if (config.denoise) {
|
|
3763
3877
|
const denoiseConfigOffset = writeFrameConfig(
|
|
3764
3878
|
{ x: 0, y: 0, width: config.width, height: config.height },
|
|
3765
3879
|
{ sampleIndex: 0, sampleWeight: 1 / config.samplesPerPixel }
|
|
3766
3880
|
);
|
|
3767
|
-
encodeDenoise(
|
|
3881
|
+
encodeDenoise(reserveEncoder(), denoiseConfigOffset);
|
|
3768
3882
|
}
|
|
3769
|
-
encodePresent(
|
|
3770
|
-
|
|
3771
|
-
return
|
|
3883
|
+
encodePresent(reserveEncoder());
|
|
3884
|
+
submitCurrentEncoder();
|
|
3885
|
+
return submissionCount;
|
|
3772
3886
|
}
|
|
3773
3887
|
function renderOnce() {
|
|
3774
3888
|
frame += 1;
|
|
@@ -3783,6 +3897,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
3783
3897
|
tiles: tiles.length,
|
|
3784
3898
|
tileSize: config.tileSize,
|
|
3785
3899
|
samplesPerPixel: config.samplesPerPixel,
|
|
3900
|
+
maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
|
|
3786
3901
|
screenRays: config.width * config.height,
|
|
3787
3902
|
primaryRays: config.width * config.height * config.samplesPerPixel,
|
|
3788
3903
|
sceneObjectCount: config.sceneObjectCount,
|
|
@@ -3869,11 +3984,29 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
3869
3984
|
samplesPerPixel: config.samplesPerPixel,
|
|
3870
3985
|
sceneObjectCapacity: config.sceneObjectCapacity,
|
|
3871
3986
|
sceneObjects: packedScene.objects,
|
|
3987
|
+
camera: activeCameraOptions,
|
|
3872
3988
|
frameIndex: config.frameIndex
|
|
3873
3989
|
});
|
|
3874
3990
|
device.queue.writeBuffer(sceneObjectBuffer, 0, packedScene.buffer);
|
|
3875
3991
|
return config;
|
|
3876
3992
|
}
|
|
3993
|
+
function updateCamera(cameraOptions = {}) {
|
|
3994
|
+
activeCameraOptions = cameraOptions;
|
|
3995
|
+
config = createWavefrontPathTracingComputeConfig({
|
|
3996
|
+
...options,
|
|
3997
|
+
canvas,
|
|
3998
|
+
width: config.width,
|
|
3999
|
+
height: config.height,
|
|
4000
|
+
maxDepth: config.maxDepth,
|
|
4001
|
+
tileSize: config.tileSize,
|
|
4002
|
+
samplesPerPixel: config.samplesPerPixel,
|
|
4003
|
+
sceneObjectCapacity: config.sceneObjectCapacity,
|
|
4004
|
+
sceneObjects: packedScene.objects,
|
|
4005
|
+
camera: activeCameraOptions,
|
|
4006
|
+
frameIndex: config.frameIndex
|
|
4007
|
+
});
|
|
4008
|
+
return config;
|
|
4009
|
+
}
|
|
3877
4010
|
function getSnapshot() {
|
|
3878
4011
|
return Object.freeze({
|
|
3879
4012
|
frame,
|
|
@@ -3883,6 +4016,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
3883
4016
|
tiles: tiles.length,
|
|
3884
4017
|
tileSize: config.tileSize,
|
|
3885
4018
|
samplesPerPixel: config.samplesPerPixel,
|
|
4019
|
+
maxFramePassesPerSubmission: config.maxFramePassesPerSubmission,
|
|
3886
4020
|
sceneObjectCount: config.sceneObjectCount,
|
|
3887
4021
|
triangleCount: config.triangleCount,
|
|
3888
4022
|
emissiveTriangleCount: config.emissiveTriangleCount,
|
|
@@ -3930,6 +4064,7 @@ async function createWavefrontPathTracingComputeRenderer(options = {}) {
|
|
|
3930
4064
|
renderFrame,
|
|
3931
4065
|
readOutputProbe,
|
|
3932
4066
|
updateSceneObjects,
|
|
4067
|
+
updateCamera,
|
|
3933
4068
|
getSnapshot,
|
|
3934
4069
|
destroy
|
|
3935
4070
|
});
|