@plasius/gpu-renderer 0.2.1 → 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 CHANGED
@@ -23,6 +23,27 @@ All notable changes to this project will be documented in this file.
23
23
  - **Security**
24
24
  - (placeholder)
25
25
 
26
+ ## [0.2.3] - 2026-06-11
27
+
28
+ - **Added**
29
+ - Added `updateCamera(...)` support for wavefront renderers so validation
30
+ views can animate camera movement without rebuilding mesh buffers.
31
+
32
+ - **Changed**
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.
36
+
37
+ - **Fixed**
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.
43
+
44
+ - **Security**
45
+ - (placeholder)
46
+
26
47
  ## [0.2.1] - 2026-06-06
27
48
 
28
49
  - **Added**
@@ -312,3 +333,4 @@ All notable changes to this project will be documented in this file.
312
333
  [0.1.12]: https://github.com/Plasius-LTD/gpu-renderer/releases/tag/v0.1.12
313
334
  [0.1.14]: https://github.com/Plasius-LTD/gpu-renderer/releases/tag/v0.1.14
314
335
  [0.2.1]: https://github.com/Plasius-LTD/gpu-renderer/releases/tag/v0.2.1
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 a single command submission. After each
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 no
217
- longer forces one CPU queue submission per tile/sample.
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
- const encoder = device.createCommandEncoder({
3746
- label: `plasius.wavefront.frame.${frameIndex}.batched`
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(encoder, tile, configOffset);
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(encoder, tile, outputConfigOffset);
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(encoder, denoiseConfigOffset);
3881
+ encodeDenoise(reserveEncoder(), denoiseConfigOffset);
3768
3882
  }
3769
- encodePresent(encoder);
3770
- device.queue.submit([encoder.finish()]);
3771
- return 1;
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
  });