@plasius/gpu-renderer 0.2.47 → 0.2.48

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/dist/index.cjs CHANGED
@@ -87,6 +87,7 @@ __export(index_exports, {
87
87
  supportsWebGpu: () => supportsWebGpu,
88
88
  traceWavefrontReferenceTriangles: () => traceWavefrontReferenceTriangles,
89
89
  transitionMediumStack: () => transitionMediumStack,
90
+ validateNativeSurfaceMaterials: () => validateNativeMaterials,
90
91
  wavefrontMaterialKinds: () => wavefrontMaterialKinds,
91
92
  wavefrontPathTracingComputeLimits: () => wavefrontPathTracingComputeLimits,
92
93
  wavefrontSceneObjectKinds: () => wavefrontSceneObjectKinds
@@ -651,12 +652,18 @@ struct Frame {
651
652
  @group(0) @binding(0) var<uniform> frame: Frame;
652
653
  @group(1) @binding(0) var shadowMap: texture_depth_2d;
653
654
  @group(1) @binding(1) var shadowSampler: sampler_comparison;
654
- @group(2) @binding(0) var reflectionMap: texture_2d<f32>;
655
- @group(2) @binding(1) var reflectionSampler: sampler;
655
+ @group(3) @binding(0) var reflectionMap: texture_2d<f32>;
656
+ @group(3) @binding(1) var reflectionSampler: sampler;
657
+ @group(2) @binding(0) var baseColourMap: texture_2d<f32>;
658
+ @group(2) @binding(1) var normalMap: texture_2d<f32>;
659
+ @group(2) @binding(2) var ormMap: texture_2d<f32>;
660
+ @group(2) @binding(3) var materialSampler: sampler;
661
+ @group(2) @binding(4) var<uniform> materialParameters: vec4f;
662
+ @group(2) @binding(5) var clearcoatMap: texture_2d<f32>;
656
663
  struct Surface {
657
664
  @builtin(position) clip: vec4f,
658
665
  @location(0) world: vec3f, @location(1) normal: vec3f,
659
- @location(2) colour: vec3f, @location(3) material: vec3f,
666
+ @location(2) colour: vec3f, @location(3) material: vec3f, @location(4) uv: vec2f,
660
667
  };
661
668
  fn project(p: vec3f) -> vec4f {
662
669
  let d = p - frame.eye.xyz;
@@ -670,8 +677,8 @@ fn lightProject(p: vec3f) -> vec4f {
670
677
  dot(d, frame.lightUp.xyz) / 34.0, dot(d, frame.lightForward.xyz) / 100.0, 1.0);
671
678
  }
672
679
  @vertex fn surfaceVertex(@location(0) p: vec3f, @location(1) n: vec3f,
673
- @location(2) c: vec3f, @location(3) m: vec3f) -> Surface {
674
- return Surface(project(p), p, n, c, m);
680
+ @location(2) c: vec3f, @location(3) m: vec3f, @location(4) uv: vec2f) -> Surface {
681
+ return Surface(project(p), p, n, c, m, uv);
675
682
  }
676
683
  @vertex fn shadowVertex(@location(0) p: vec3f) -> @builtin(position) vec4f {
677
684
  return lightProject(p);
@@ -724,17 +731,25 @@ fn visibility(p: vec3f, n: vec3f) -> f32 {
724
731
  }
725
732
  return result / 9.0;
726
733
  }
727
- fn brdf(c: vec3f, n: vec3f, v: vec3f, roughness: f32, metal: f32) -> vec3f {
734
+ // Dielectric coating is evaluated independently from the coloured substrate.
735
+ // This returns the microfacet lobe without Fresnel, including the cosine term.
736
+ fn specularLobe(n: vec3f, v: vec3f, roughness: f32) -> f32 {
728
737
  let l = frame.sun.xyz; let h = normalize(l + v);
729
738
  let nl = max(dot(n,l),0.0); let nv = max(dot(n,v),0.001);
730
- let nh = max(dot(n,h),0.0); let vh = max(dot(v,h),0.0);
739
+ let nh = max(dot(n,h),0.0);
731
740
  let a = max(0.035, roughness * roughness); let a2 = a * a;
732
741
  let d = a2 / max(0.00001, 3.14159265 * pow(nh * nh * (a2 - 1.0) + 1.0, 2.0));
733
742
  let k = pow(roughness + 1.0, 2.0) / 8.0;
734
743
  let g = (nl / (nl * (1.0-k) + k)) * (nv / (nv * (1.0-k) + k));
744
+ return d * g / max(0.001, 4.0*nl*nv) * nl;
745
+ }
746
+ fn brdf(c: vec3f, n: vec3f, v: vec3f, roughness: f32, metal: f32) -> vec3f {
747
+ let h = normalize(frame.sun.xyz + v);
748
+ let nl = max(dot(n,frame.sun.xyz),0.0);
749
+ let vh = max(dot(v,h),0.0);
735
750
  let f0 = mix(vec3f(0.04), c, metal);
736
751
  let f = f0 + (1.0-f0) * pow(1.0-vh, 5.0);
737
- return ((1.0-f) * (1.0-metal) * c / 3.14159265 + d * g * f / max(0.001, 4.0*nl*nv)) * nl;
752
+ return (1.0-f) * (1.0-metal) * c / 3.14159265 * nl + specularLobe(n,v,roughness) * f;
738
753
  }
739
754
  fn fog(c: vec3f, p: vec3f) -> vec3f {
740
755
  let amount = 1.0 - exp(-distance(frame.eye.xyz, p) * 0.0018);
@@ -742,23 +757,58 @@ fn fog(c: vec3f, p: vec3f) -> vec3f {
742
757
  }
743
758
  fn surfaceRadiance(s: Surface, front: bool) -> vec3f {
744
759
  let facing = select(select(-1.0,1.0,front),select(1.0,-1.0,front),frame.info.y>0.5);
745
- var n = normalize(s.normal) * facing;
760
+ let coatNormal = normalize(s.normal) * facing;
761
+ var n = coatNormal;
746
762
  let v = normalize(frame.eye.xyz - s.world);
747
- var c = s.colour;
748
- if (s.material.z > 2.5) {
763
+ // Derivatives and samples must be uniform across each fragment quad.
764
+ // Relative degeneracy: raw derivative magnitudes shrink with model scale and
765
+ // zoom. An absolute tangent cutoff silently disabled normal maps in close-ups.
766
+ let dpScale = max(length(dpdx(s.world)), length(dpdy(s.world)));
767
+ let uvScale = max(length(dpdx(s.uv)), length(dpdy(s.uv)));
768
+ let dx = dpdx(s.world) / max(dpScale, 1e-20);
769
+ let dy = dpdy(s.world) / max(dpScale, 1e-20);
770
+ let ux = dpdx(s.uv) / max(uvScale, 1e-20);
771
+ let uy = dpdy(s.uv) / max(uvScale, 1e-20);
772
+ let determinant = ux.x * uy.y - ux.y * uy.x;
773
+ let tangent = dx * uy.y - dy * ux.y;
774
+ let bitangent = dy * ux.x - dx * uy.x;
775
+ let sampledNormal = textureSample(normalMap, materialSampler, s.uv).xyz * 2.0 - 1.0;
776
+ var c = s.colour * textureSample(baseColourMap, materialSampler, s.uv).rgb;
777
+ let orm = textureSample(ormMap, materialSampler, s.uv).rgb;
778
+ let coatSample = textureSample(clearcoatMap, materialSampler, s.uv).rg;
779
+ let roughness = clamp(s.material.x * orm.g, 0.04, 1.0);
780
+ let metalness = clamp(s.material.y * orm.b, 0.0, 1.0);
781
+ let orthogonalTangent = tangent - n * dot(n,tangent);
782
+ let orthogonalBitangent = bitangent - n * dot(n,bitangent);
783
+ if (dpScale > 1e-20 && uvScale > 1e-20 && abs(determinant) > 1e-6 && dot(orthogonalTangent,orthogonalTangent) > 1e-12 && dot(orthogonalBitangent,orthogonalBitangent) > 1e-12) {
784
+ let t = normalize(orthogonalTangent) * sign(determinant);
785
+ let b = normalize(orthogonalBitangent) * sign(determinant);
786
+ n = normalize(t * sampledNormal.x * materialParameters.x + b * sampledNormal.y * materialParameters.x + n * max(0.001,sampledNormal.z));
787
+ }
788
+ if (materialParameters.y < 0.5 && s.material.z > 2.5) {
749
789
  let rock = smoothstep(0.45,0.68,noise(s.world*0.27));
750
790
  c = mix(c,vec3f(0.16,0.145,0.11),rock*0.7);
751
791
  c *= 0.75+0.45*noise(s.world*2.4);
752
792
  n = normalize(n+vec3f(noise(s.world*4.0)-0.5,0,noise(s.world*4.0+vec3f(13))-0.5)*0.28);
753
- } else if (s.material.z > 0.5 && s.material.z < 1.5) {
793
+ } else if (materialParameters.y < 0.5 && s.material.z > 0.5 && s.material.z < 1.5) {
754
794
  let grain = noise(s.world * vec3f(18, 3, 1.6));
755
795
  c *= 0.82 + 0.30 * grain;
756
- } else if (s.material.z > 1.5) { c *= 0.88 + 0.18 * noise(s.world * 12.0); }
796
+ } else if (materialParameters.y < 0.5 && s.material.z > 1.5) { c *= 0.88 + 0.18 * noise(s.world * 12.0); }
757
797
  let hemi = mix(vec3f(0.06,0.05,0.035), vec3f(0.16,0.22,0.29), n.y * 0.5 + 0.5);
758
- let direct = brdf(c, n, v, s.material.x, s.material.y) * vec3f(3.0, 2.65, 2.15) * visibility(s.world, n);
759
- let ambient = c * hemi * (1.0 - s.material.y * 0.7);
760
- let env = sky(reflect(-v,n)) * mix(vec3f(0.04), c, s.material.y) * (1.0-s.material.x) * 0.30;
761
- return fog(direct + ambient + env, s.world);
798
+ let direct = brdf(c, n, v, roughness, metalness) * vec3f(3.0, 2.65, 2.15) * visibility(s.world, n);
799
+ let ambient = c * hemi * (1.0 - metalness * 0.7) * orm.r;
800
+ let env = sky(reflect(-v,n)) * mix(vec3f(0.04), c, metalness) * (1.0-roughness) * orm.r * 0.30;
801
+ let baseRadiance = direct + ambient + env;
802
+ let coatCoverage = materialParameters.z * coatSample.r;
803
+ if (coatCoverage <= 0.0) { return fog(baseRadiance,s.world); }
804
+ // KHR_materials_clearcoat's simple Fresnel layering: varnish has its own
805
+ // smooth normal and roughness. Its reflection never inherits the wood colour.
806
+ let coatFresnel = 0.04 + 0.96 * pow(1.0-clamp(abs(dot(coatNormal,v)),0.0,1.0),5.0);
807
+ let coatWeight = coatCoverage * coatFresnel;
808
+ let coatRoughness = clamp(materialParameters.w * coatSample.g,0.04,1.0);
809
+ let coatDirect = vec3f(3.0,2.65,2.15) * specularLobe(coatNormal,v,coatRoughness) * visibility(s.world,coatNormal);
810
+ let coatEnvironment = sky(reflect(-v,coatNormal)) * (1.0-coatRoughness) * orm.r * 0.30;
811
+ return fog(baseRadiance * (1.0-coatWeight) + (coatDirect+coatEnvironment) * coatWeight, s.world);
762
812
  }
763
813
  @fragment fn surfaceFragment(s: Surface, @builtin(front_facing) front: bool) -> @location(0) vec4f {
764
814
  return vec4f(display(surfaceRadiance(s,front)),1);
@@ -812,12 +862,114 @@ struct WaterResult { @location(0) colour: vec4f, @builtin(frag_depth) depth: f32
812
862
  return vec4f(sky(cameraRay(pixel.xy)),1);
813
863
  }
814
864
  @fragment fn reflectionSurface(s: Surface, @builtin(front_facing) front: bool) -> @location(0) vec4f {
865
+ let radiance = surfaceRadiance(s,front);
815
866
  if (s.world.y < frame.viewport.z) { discard; }
816
- return vec4f(surfaceRadiance(s,front),1);
867
+ return vec4f(radiance,1);
817
868
  }
818
869
  `
819
870
  );
820
871
 
872
+ // src/native-surface-materials.js
873
+ var MAX_BYTES = 64 * 1024 * 1024;
874
+ var MAPS = ["baseColor", "normal", "orm", "clearcoatMap"];
875
+ var defaults = {
876
+ baseColor: [255, 255, 255, 255],
877
+ normal: [128, 128, 255, 255],
878
+ orm: [255, 255, 255, 255],
879
+ clearcoatMap: [255, 255, 255, 255]
880
+ };
881
+ var dimension = (n) => Number.isInteger(n) && n >= 1 && n <= 2048 && (n & n - 1) === 0;
882
+ function validateNativeMaterials(materials) {
883
+ if (!Array.isArray(materials) || materials.length > 16)
884
+ throw new Error("materials must contain at most 16 entries.");
885
+ let bytes = 0;
886
+ for (const material of materials) {
887
+ if (!material || typeof material !== "object")
888
+ throw new Error("Each material must be an object.");
889
+ if (!Number.isFinite(material.normalScale ?? 1) || (material.normalScale ?? 1) < 0 || (material.normalScale ?? 1) > 4) {
890
+ throw new Error("material normalScale must be between 0 and 4.");
891
+ }
892
+ for (const key of ["clearcoat", "clearcoatRoughness"]) {
893
+ const value = material[key] ?? 0;
894
+ if (!Number.isFinite(value) || value < 0 || value > 1) {
895
+ throw new Error(`material ${key} must be between 0 and 1.`);
896
+ }
897
+ }
898
+ for (const key of MAPS) {
899
+ const map = material[key];
900
+ if (map === void 0) continue;
901
+ if (!map || !dimension(map.width) || !dimension(map.height) || !(map.data instanceof Uint8Array || map.data instanceof Uint8ClampedArray) || map.data.length !== map.width * map.height * 4) {
902
+ throw new Error(
903
+ `material ${key} texture must contain RGBA8 data with power-of-two dimensions between 1 and 2048.`
904
+ );
905
+ }
906
+ bytes += map.data.byteLength;
907
+ if (bytes > MAX_BYTES)
908
+ throw new Error("material textures exceed the 64 MiB source budget.");
909
+ }
910
+ }
911
+ }
912
+ var linear = (x) => x <= 0.04045 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4;
913
+ var srgb = (x) => x <= 31308e-7 ? x * 12.92 : 1.055 * x ** (1 / 2.4) - 0.055;
914
+ function nextMip(image, colour) {
915
+ const width = Math.max(1, image.width / 2), height = Math.max(1, image.height / 2);
916
+ const data = new Uint8Array(width * height * 4);
917
+ for (let y = 0; y < height; y++)
918
+ for (let x = 0; x < width; x++)
919
+ for (let c = 0; c < 4; c++) {
920
+ let sum = 0;
921
+ for (let dy = 0; dy < 2; dy++)
922
+ for (let dx = 0; dx < 2; dx++) {
923
+ const offset = (Math.min(image.height - 1, y * 2 + dy) * image.width + Math.min(image.width - 1, x * 2 + dx)) * 4 + c;
924
+ const value = image.data[offset] / 255;
925
+ sum += colour && c < 3 ? linear(value) : value;
926
+ }
927
+ data[(y * width + x) * 4 + c] = Math.round(
928
+ 255 * (colour && c < 3 ? srgb(sum / 4) : sum / 4)
929
+ );
930
+ }
931
+ return { width, height, data };
932
+ }
933
+ function createNativeMaterialTextures(device, materials, own) {
934
+ return [{}, ...materials].map((material, index) => {
935
+ const result = {
936
+ normalScale: material.normal ? material.normalScale ?? 1 : 0,
937
+ authored: ["baseColor", "normal", "orm"].some((key) => material[key]),
938
+ clearcoat: material.clearcoat ?? 0,
939
+ clearcoatRoughness: material.clearcoatRoughness ?? 0
940
+ };
941
+ for (const key of MAPS) {
942
+ let level = material[key] ?? {
943
+ width: 1,
944
+ height: 1,
945
+ data: new Uint8Array(defaults[key])
946
+ };
947
+ const mipLevelCount = 1 + Math.log2(Math.max(level.width, level.height));
948
+ const texture = own(
949
+ device.createTexture({
950
+ label: `native.material.${index}.${key}`,
951
+ size: { width: level.width, height: level.height },
952
+ format: key === "baseColor" ? "rgba8unorm-srgb" : "rgba8unorm",
953
+ mipLevelCount,
954
+ usage: 6
955
+ })
956
+ );
957
+ for (let mipLevel = 0; mipLevel < mipLevelCount; mipLevel++) {
958
+ device.queue.writeTexture(
959
+ { texture, mipLevel },
960
+ level.data,
961
+ { bytesPerRow: level.width * 4, rowsPerImage: level.height },
962
+ { width: level.width, height: level.height, depthOrArrayLayers: 1 }
963
+ );
964
+ if (mipLevel + 1 < mipLevelCount)
965
+ level = nextMip(level, key === "baseColor");
966
+ }
967
+ result[key] = texture.createView();
968
+ }
969
+ return result;
970
+ });
971
+ }
972
+
821
973
  // src/native-scene-renderer.js
822
974
  var STRIDE = 12;
823
975
  var UNIFORM_BYTES = 256;
@@ -833,7 +985,7 @@ var unit = (a) => {
833
985
  return a.map((v) => v / length);
834
986
  };
835
987
  var finiteVector = (a) => Array.isArray(a) && a.length === 3 && a.every(Number.isFinite);
836
- function validateFrame(frame, maxVertices) {
988
+ function validateFrame(frame, maxVertices, materialCount) {
837
989
  const { vertices, camera, time = 0 } = frame ?? {};
838
990
  if (!(vertices instanceof Float32Array) || vertices.length % (STRIDE * 3) !== 0 || vertices.length > maxVertices * STRIDE || !vertices.every(Number.isFinite)) {
839
991
  throw new Error(
@@ -855,6 +1007,30 @@ function validateFrame(frame, maxVertices) {
855
1007
  "wakes must contain at most four finite [x, z, heading] entries."
856
1008
  );
857
1009
  }
1010
+ const count = vertices.length / STRIDE;
1011
+ if (frame.texcoords !== void 0 && (!(frame.texcoords instanceof Float32Array) || frame.texcoords.length !== count * 2 || !frame.texcoords.every(Number.isFinite))) {
1012
+ throw new Error(
1013
+ "texcoords must contain two finite coordinates per vertex."
1014
+ );
1015
+ }
1016
+ if (frame.surfaces !== void 0) {
1017
+ if (!Array.isArray(frame.surfaces) || frame.surfaces.length > 256 || !frame.texcoords) {
1018
+ throw new Error(
1019
+ "surfaces require texcoords and at most 256 contiguous draw ranges."
1020
+ );
1021
+ }
1022
+ let end = 0;
1023
+ for (const range of frame.surfaces) {
1024
+ if (!range || range.firstVertex !== end || !Number.isInteger(range.vertexCount) || range.vertexCount <= 0 || range.vertexCount % 3 !== 0 || !Number.isInteger(range.materialIndex) || range.materialIndex < 0 || range.materialIndex >= materialCount) {
1025
+ throw new Error(
1026
+ "surface ranges must cover triangles in order and reference a valid material."
1027
+ );
1028
+ }
1029
+ end += range.vertexCount;
1030
+ }
1031
+ if (end !== count)
1032
+ throw new Error("surface ranges must cover every vertex exactly once.");
1033
+ }
858
1034
  return { ...frame, time };
859
1035
  }
860
1036
  function frameUniforms(frame, width, height, reflected = false) {
@@ -902,6 +1078,9 @@ function frameUniforms(frame, width, height, reflected = false) {
902
1078
  }
903
1079
  async function createNativeSceneRenderer(options = {}) {
904
1080
  const maxVertices = options.maxVertices ?? 3e5;
1081
+ const materials = options.materials ?? [];
1082
+ validateNativeMaterials(materials);
1083
+ const materialCount = materials.length;
905
1084
  if (!Number.isInteger(maxVertices) || maxVertices < 3 || maxVertices > 6e5) {
906
1085
  throw new Error("maxVertices must be an integer between 3 and 600000.");
907
1086
  }
@@ -910,6 +1089,7 @@ async function createNativeSceneRenderer(options = {}) {
910
1089
  let available = true;
911
1090
  let targets = null;
912
1091
  let vertexBuffer = null;
1092
+ let uvBuffer = null;
913
1093
  let vertexCapacity = 0;
914
1094
  let pendingFrame = null;
915
1095
  let submittedFrames = 0;
@@ -1016,7 +1196,20 @@ async function createNativeSceneRenderer(options = {}) {
1016
1196
  if (p !== shadowPipeline) pass2.setBindGroup(1, shadowGroup);
1017
1197
  if (vertexCount > 0) {
1018
1198
  pass2.setVertexBuffer(0, vertexBuffer);
1019
- pass2.draw(vertexCount);
1199
+ if (p === shadowPipeline) {
1200
+ pass2.draw(vertexCount);
1201
+ } else {
1202
+ pass2.setVertexBuffer(1, uvBuffer);
1203
+ if (pendingFrame.surfaces) {
1204
+ for (const range of pendingFrame.surfaces) {
1205
+ pass2.setBindGroup(2, materialGroups[range.materialIndex + 1]);
1206
+ pass2.draw(range.vertexCount, 1, range.firstVertex);
1207
+ }
1208
+ } else {
1209
+ pass2.setBindGroup(2, materialGroups[0]);
1210
+ pass2.draw(vertexCount);
1211
+ }
1212
+ }
1020
1213
  }
1021
1214
  };
1022
1215
  const shadowPass = encoder.beginRenderPass({
@@ -1062,7 +1255,8 @@ async function createNativeSceneRenderer(options = {}) {
1062
1255
  if (pendingFrame.water !== false) {
1063
1256
  pass.setPipeline(waterPipeline);
1064
1257
  pass.setBindGroup(1, shadowGroup);
1065
- pass.setBindGroup(2, targets.reflectionGroup);
1258
+ pass.setBindGroup(2, materialGroups[0]);
1259
+ pass.setBindGroup(3, targets.reflectionGroup);
1066
1260
  pass.draw(3);
1067
1261
  }
1068
1262
  drawGeometry(pass, surfacePipeline, mainGroup);
@@ -1112,6 +1306,63 @@ async function createNativeSceneRenderer(options = {}) {
1112
1306
  { binding: 1, visibility: 2, sampler: { type: "filtering" } }
1113
1307
  ]
1114
1308
  });
1309
+ const materialLayout = device.createBindGroupLayout({
1310
+ entries: [
1311
+ ...[0, 1, 2, 5].map((binding) => ({
1312
+ binding,
1313
+ visibility: 2,
1314
+ texture: { sampleType: "float" }
1315
+ })),
1316
+ { binding: 3, visibility: 2, sampler: { type: "filtering" } },
1317
+ {
1318
+ binding: 4,
1319
+ visibility: 2,
1320
+ buffer: { type: "uniform", minBindingSize: 16 }
1321
+ }
1322
+ ]
1323
+ });
1324
+ const materialSampler = device.createSampler({
1325
+ addressModeU: "repeat",
1326
+ addressModeV: "repeat",
1327
+ minFilter: "linear",
1328
+ magFilter: "linear",
1329
+ mipmapFilter: "linear",
1330
+ maxAnisotropy: 4
1331
+ });
1332
+ const materialGroups = createNativeMaterialTextures(
1333
+ device,
1334
+ materials,
1335
+ own
1336
+ ).map((material) => {
1337
+ const params = own(
1338
+ device.createBuffer({
1339
+ size: 16,
1340
+ usage: 72,
1341
+ label: "native.material-parameters"
1342
+ })
1343
+ );
1344
+ device.queue.writeBuffer(
1345
+ params,
1346
+ 0,
1347
+ new Float32Array([
1348
+ material.normalScale,
1349
+ material.authored ? 1 : 0,
1350
+ material.clearcoat,
1351
+ material.clearcoatRoughness
1352
+ ])
1353
+ );
1354
+ return device.createBindGroup({
1355
+ layout: materialLayout,
1356
+ entries: [
1357
+ { binding: 0, resource: material.baseColor },
1358
+ { binding: 1, resource: material.normal },
1359
+ { binding: 2, resource: material.orm },
1360
+ { binding: 3, resource: materialSampler },
1361
+ { binding: 4, resource: { buffer: params } },
1362
+ { binding: 5, resource: material.clearcoatMap }
1363
+ ]
1364
+ });
1365
+ });
1115
1366
  const uniformBuffer = own(
1116
1367
  device.createBuffer({
1117
1368
  label: "native.frame",
@@ -1176,7 +1427,19 @@ async function createNativeSceneRenderer(options = {}) {
1176
1427
  {
1177
1428
  arrayStride: STRIDE * 4,
1178
1429
  attributes: vertex === "shadowVertex" ? attributes.slice(0, 1) : attributes
1179
- }
1430
+ },
1431
+ ...vertex === "shadowVertex" ? [] : [
1432
+ {
1433
+ arrayStride: 8,
1434
+ attributes: [
1435
+ {
1436
+ shaderLocation: 4,
1437
+ offset: 0,
1438
+ format: "float32x2"
1439
+ }
1440
+ ]
1441
+ }
1442
+ ]
1180
1443
  ]
1181
1444
  } : {}
1182
1445
  },
@@ -1210,7 +1473,7 @@ async function createNativeSceneRenderer(options = {}) {
1210
1473
  "surfaceFragment",
1211
1474
  format,
1212
1475
  4,
1213
- [uniformLayout, shadowLayout],
1476
+ [uniformLayout, shadowLayout, materialLayout],
1214
1477
  true
1215
1478
  );
1216
1479
  const skyPipeline = await pipeline(
@@ -1227,7 +1490,7 @@ async function createNativeSceneRenderer(options = {}) {
1227
1490
  "waterFragment",
1228
1491
  format,
1229
1492
  4,
1230
- [uniformLayout, shadowLayout, reflectionLayout]
1493
+ [uniformLayout, shadowLayout, materialLayout, reflectionLayout]
1231
1494
  );
1232
1495
  const reflectedSurfacePipeline = await pipeline(
1233
1496
  "reflection-surface",
@@ -1235,7 +1498,7 @@ async function createNativeSceneRenderer(options = {}) {
1235
1498
  "reflectionSurface",
1236
1499
  "rgba16float",
1237
1500
  1,
1238
- [uniformLayout, shadowLayout],
1501
+ [uniformLayout, shadowLayout, materialLayout],
1239
1502
  true
1240
1503
  );
1241
1504
  const reflectedSkyPipeline = await pipeline(
@@ -1250,15 +1513,23 @@ async function createNativeSceneRenderer(options = {}) {
1250
1513
  render(input) {
1251
1514
  if (destroyed) throw new Error("Native scene renderer was destroyed.");
1252
1515
  if (!available) throw new Error("WebGPU device was lost.");
1253
- pendingFrame = validateFrame(input, maxVertices);
1516
+ pendingFrame = validateFrame(input, maxVertices, materialCount);
1254
1517
  const data = pendingFrame.vertices;
1255
1518
  ensureTargets();
1256
1519
  if (data.byteLength > vertexCapacity) {
1257
1520
  if (vertexBuffer) release(vertexBuffer);
1521
+ if (uvBuffer) release(uvBuffer);
1258
1522
  vertexCapacity = Math.min(
1259
1523
  maxVertices * STRIDE * 4,
1260
1524
  Math.max(4096, 2 ** Math.ceil(Math.log2(data.byteLength)))
1261
1525
  );
1526
+ uvBuffer = own(
1527
+ device.createBuffer({
1528
+ label: "native.surface-uvs",
1529
+ size: Math.ceil(vertexCapacity / (STRIDE * 4)) * 8,
1530
+ usage: 40
1531
+ })
1532
+ );
1262
1533
  vertexBuffer = own(
1263
1534
  device.createBuffer({
1264
1535
  label: "native.surface-vertices",
@@ -1267,6 +1538,8 @@ async function createNativeSceneRenderer(options = {}) {
1267
1538
  })
1268
1539
  );
1269
1540
  }
1541
+ if (pendingFrame.texcoords?.byteLength)
1542
+ device.queue.writeBuffer(uvBuffer, 0, pendingFrame.texcoords);
1270
1543
  vertexCount = data.length / STRIDE;
1271
1544
  if (data.byteLength) device.queue.writeBuffer(vertexBuffer, 0, data);
1272
1545
  device.queue.writeBuffer(
@@ -3947,8 +4220,8 @@ function hashUint32(value) {
3947
4220
  x = ((x >>> 16 ^ x) >>> 0) * 73244475 >>> 0;
3948
4221
  return (x >>> 16 ^ x) >>> 0;
3949
4222
  }
3950
- function mixSeed(pixelId, sampleId, bounce, frameIndex, dimension) {
3951
- let x = (pixelId >>> 0) * 747796405 ^ (sampleId >>> 0) * 2891336453 ^ (bounce >>> 0) * 277803737 ^ (frameIndex >>> 0) * 1442695041 ^ (dimension >>> 0) * 1597334677;
4223
+ function mixSeed(pixelId, sampleId, bounce, frameIndex, dimension2) {
4224
+ let x = (pixelId >>> 0) * 747796405 ^ (sampleId >>> 0) * 2891336453 ^ (bounce >>> 0) * 277803737 ^ (frameIndex >>> 0) * 1442695041 ^ (dimension2 >>> 0) * 1597334677;
3952
4225
  x >>>= 0;
3953
4226
  x ^= x >>> 16;
3954
4227
  x = x * 2146121005 >>> 0;
@@ -9827,25 +10100,25 @@ function toWgslConstName(name) {
9827
10100
  return `SAMPLE_DIM_${name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase()}`;
9828
10101
  }
9829
10102
  var usedDimensions = /* @__PURE__ */ new Set();
9830
- for (const [, dimension] of sampleDimensionEntries) {
9831
- if (usedDimensions.has(dimension)) {
9832
- throw new Error(`Duplicate wavefront sample dimension ${dimension}.`);
10103
+ for (const [, dimension2] of sampleDimensionEntries) {
10104
+ if (usedDimensions.has(dimension2)) {
10105
+ throw new Error(`Duplicate wavefront sample dimension ${dimension2}.`);
9833
10106
  }
9834
- usedDimensions.add(dimension);
10107
+ usedDimensions.add(dimension2);
9835
10108
  }
9836
10109
  var WAVEFRONT_SAMPLE_DIMENSIONS = Object.freeze(
9837
10110
  Object.fromEntries(sampleDimensionEntries)
9838
10111
  );
9839
10112
  function listWavefrontSampleDimensions() {
9840
10113
  return sampleDimensionEntries.map(
9841
- ([name, dimension]) => Object.freeze({
10114
+ ([name, dimension2]) => Object.freeze({
9842
10115
  name,
9843
- dimension,
10116
+ dimension: dimension2,
9844
10117
  wgslName: toWgslConstName(name)
9845
10118
  })
9846
10119
  );
9847
10120
  }
9848
- var WAVEFRONT_SAMPLE_DIMENSIONS_WGSL = listWavefrontSampleDimensions().map(({ wgslName, dimension }) => `const ${wgslName}: u32 = ${dimension}u;`).join("\n");
10121
+ var WAVEFRONT_SAMPLE_DIMENSIONS_WGSL = listWavefrontSampleDimensions().map(({ wgslName, dimension: dimension2 }) => `const ${wgslName}: u32 = ${dimension2}u;`).join("\n");
9849
10122
  function fract(value) {
9850
10123
  return value - Math.floor(value);
9851
10124
  }
@@ -9858,19 +10131,19 @@ function radicalInverseVdc2(value) {
9858
10131
  bits = ((bits & 16711935) << 8 | (bits & 4278255360) >>> 8) >>> 0;
9859
10132
  return bits / 4294967296;
9860
10133
  }
9861
- function sampleWavefrontDimension1D(pixelId, sampleId, bounce, frameIndex, dimension) {
9862
- return random01FromSeed(mixSeed(pixelId, sampleId, bounce, frameIndex, dimension));
10134
+ function sampleWavefrontDimension1D(pixelId, sampleId, bounce, frameIndex, dimension2) {
10135
+ return random01FromSeed(mixSeed(pixelId, sampleId, bounce, frameIndex, dimension2));
9863
10136
  }
9864
- function sampleWavefrontDimension2D(pixelId, sampleId, bounce, frameIndex, dimension, strataCount) {
10137
+ function sampleWavefrontDimension2D(pixelId, sampleId, bounce, frameIndex, dimension2, strataCount) {
9865
10138
  const strata = Math.max(1, Number.isFinite(strataCount) ? Math.trunc(strataCount) : 1);
9866
10139
  const jitter = sampleWavefrontDimension1D(
9867
10140
  pixelId,
9868
10141
  sampleId,
9869
10142
  bounce,
9870
10143
  frameIndex,
9871
- dimension
10144
+ dimension2
9872
10145
  );
9873
- const scramble = hashUint32(mixSeed(pixelId, sampleId, bounce, frameIndex, dimension));
10146
+ const scramble = hashUint32(mixSeed(pixelId, sampleId, bounce, frameIndex, dimension2));
9874
10147
  const stratified = fract(((sampleId >>> 0) % strata + jitter) / strata);
9875
10148
  const lowDiscrepancy = fract(radicalInverseVdc2(sampleId >>> 0 ^ scramble) + jitter);
9876
10149
  return Object.freeze([stratified, lowDiscrepancy]);
@@ -14349,6 +14622,7 @@ function createRayTracingRenderPlan(options = {}) {
14349
14622
  supportsWebGpu,
14350
14623
  traceWavefrontReferenceTriangles,
14351
14624
  transitionMediumStack,
14625
+ validateNativeSurfaceMaterials,
14352
14626
  wavefrontMaterialKinds,
14353
14627
  wavefrontPathTracingComputeLimits,
14354
14628
  wavefrontSceneObjectKinds