@plasius/gpu-renderer 0.2.46 → 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/CHANGELOG.md +53 -0
- package/README.md +230 -0
- package/dist/index.cjs +512 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +510 -40
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/canonical-wavefront-meshes.js +226 -0
- package/src/index.d.ts +55 -0
- package/src/index.js +3 -0
- package/src/native-scene-renderer.js +152 -7
- package/src/native-scene-shader.js +68 -18
- package/src/native-surface-materials.js +124 -0
|
@@ -11,12 +11,18 @@ struct Frame {
|
|
|
11
11
|
@group(0) @binding(0) var<uniform> frame: Frame;
|
|
12
12
|
@group(1) @binding(0) var shadowMap: texture_depth_2d;
|
|
13
13
|
@group(1) @binding(1) var shadowSampler: sampler_comparison;
|
|
14
|
-
@group(
|
|
15
|
-
@group(
|
|
14
|
+
@group(3) @binding(0) var reflectionMap: texture_2d<f32>;
|
|
15
|
+
@group(3) @binding(1) var reflectionSampler: sampler;
|
|
16
|
+
@group(2) @binding(0) var baseColourMap: texture_2d<f32>;
|
|
17
|
+
@group(2) @binding(1) var normalMap: texture_2d<f32>;
|
|
18
|
+
@group(2) @binding(2) var ormMap: texture_2d<f32>;
|
|
19
|
+
@group(2) @binding(3) var materialSampler: sampler;
|
|
20
|
+
@group(2) @binding(4) var<uniform> materialParameters: vec4f;
|
|
21
|
+
@group(2) @binding(5) var clearcoatMap: texture_2d<f32>;
|
|
16
22
|
struct Surface {
|
|
17
23
|
@builtin(position) clip: vec4f,
|
|
18
24
|
@location(0) world: vec3f, @location(1) normal: vec3f,
|
|
19
|
-
@location(2) colour: vec3f, @location(3) material: vec3f,
|
|
25
|
+
@location(2) colour: vec3f, @location(3) material: vec3f, @location(4) uv: vec2f,
|
|
20
26
|
};
|
|
21
27
|
fn project(p: vec3f) -> vec4f {
|
|
22
28
|
let d = p - frame.eye.xyz;
|
|
@@ -30,8 +36,8 @@ fn lightProject(p: vec3f) -> vec4f {
|
|
|
30
36
|
dot(d, frame.lightUp.xyz) / 34.0, dot(d, frame.lightForward.xyz) / 100.0, 1.0);
|
|
31
37
|
}
|
|
32
38
|
@vertex fn surfaceVertex(@location(0) p: vec3f, @location(1) n: vec3f,
|
|
33
|
-
@location(2) c: vec3f, @location(3) m: vec3f) -> Surface {
|
|
34
|
-
return Surface(project(p), p, n, c, m);
|
|
39
|
+
@location(2) c: vec3f, @location(3) m: vec3f, @location(4) uv: vec2f) -> Surface {
|
|
40
|
+
return Surface(project(p), p, n, c, m, uv);
|
|
35
41
|
}
|
|
36
42
|
@vertex fn shadowVertex(@location(0) p: vec3f) -> @builtin(position) vec4f {
|
|
37
43
|
return lightProject(p);
|
|
@@ -84,17 +90,25 @@ fn visibility(p: vec3f, n: vec3f) -> f32 {
|
|
|
84
90
|
}
|
|
85
91
|
return result / 9.0;
|
|
86
92
|
}
|
|
87
|
-
|
|
93
|
+
// Dielectric coating is evaluated independently from the coloured substrate.
|
|
94
|
+
// This returns the microfacet lobe without Fresnel, including the cosine term.
|
|
95
|
+
fn specularLobe(n: vec3f, v: vec3f, roughness: f32) -> f32 {
|
|
88
96
|
let l = frame.sun.xyz; let h = normalize(l + v);
|
|
89
97
|
let nl = max(dot(n,l),0.0); let nv = max(dot(n,v),0.001);
|
|
90
|
-
let nh = max(dot(n,h),0.0);
|
|
98
|
+
let nh = max(dot(n,h),0.0);
|
|
91
99
|
let a = max(0.035, roughness * roughness); let a2 = a * a;
|
|
92
100
|
let d = a2 / max(0.00001, 3.14159265 * pow(nh * nh * (a2 - 1.0) + 1.0, 2.0));
|
|
93
101
|
let k = pow(roughness + 1.0, 2.0) / 8.0;
|
|
94
102
|
let g = (nl / (nl * (1.0-k) + k)) * (nv / (nv * (1.0-k) + k));
|
|
103
|
+
return d * g / max(0.001, 4.0*nl*nv) * nl;
|
|
104
|
+
}
|
|
105
|
+
fn brdf(c: vec3f, n: vec3f, v: vec3f, roughness: f32, metal: f32) -> vec3f {
|
|
106
|
+
let h = normalize(frame.sun.xyz + v);
|
|
107
|
+
let nl = max(dot(n,frame.sun.xyz),0.0);
|
|
108
|
+
let vh = max(dot(v,h),0.0);
|
|
95
109
|
let f0 = mix(vec3f(0.04), c, metal);
|
|
96
110
|
let f = f0 + (1.0-f0) * pow(1.0-vh, 5.0);
|
|
97
|
-
return (
|
|
111
|
+
return (1.0-f) * (1.0-metal) * c / 3.14159265 * nl + specularLobe(n,v,roughness) * f;
|
|
98
112
|
}
|
|
99
113
|
fn fog(c: vec3f, p: vec3f) -> vec3f {
|
|
100
114
|
let amount = 1.0 - exp(-distance(frame.eye.xyz, p) * 0.0018);
|
|
@@ -102,23 +116,58 @@ fn fog(c: vec3f, p: vec3f) -> vec3f {
|
|
|
102
116
|
}
|
|
103
117
|
fn surfaceRadiance(s: Surface, front: bool) -> vec3f {
|
|
104
118
|
let facing = select(select(-1.0,1.0,front),select(1.0,-1.0,front),frame.info.y>0.5);
|
|
105
|
-
|
|
119
|
+
let coatNormal = normalize(s.normal) * facing;
|
|
120
|
+
var n = coatNormal;
|
|
106
121
|
let v = normalize(frame.eye.xyz - s.world);
|
|
107
|
-
|
|
108
|
-
|
|
122
|
+
// Derivatives and samples must be uniform across each fragment quad.
|
|
123
|
+
// Relative degeneracy: raw derivative magnitudes shrink with model scale and
|
|
124
|
+
// zoom. An absolute tangent cutoff silently disabled normal maps in close-ups.
|
|
125
|
+
let dpScale = max(length(dpdx(s.world)), length(dpdy(s.world)));
|
|
126
|
+
let uvScale = max(length(dpdx(s.uv)), length(dpdy(s.uv)));
|
|
127
|
+
let dx = dpdx(s.world) / max(dpScale, 1e-20);
|
|
128
|
+
let dy = dpdy(s.world) / max(dpScale, 1e-20);
|
|
129
|
+
let ux = dpdx(s.uv) / max(uvScale, 1e-20);
|
|
130
|
+
let uy = dpdy(s.uv) / max(uvScale, 1e-20);
|
|
131
|
+
let determinant = ux.x * uy.y - ux.y * uy.x;
|
|
132
|
+
let tangent = dx * uy.y - dy * ux.y;
|
|
133
|
+
let bitangent = dy * ux.x - dx * uy.x;
|
|
134
|
+
let sampledNormal = textureSample(normalMap, materialSampler, s.uv).xyz * 2.0 - 1.0;
|
|
135
|
+
var c = s.colour * textureSample(baseColourMap, materialSampler, s.uv).rgb;
|
|
136
|
+
let orm = textureSample(ormMap, materialSampler, s.uv).rgb;
|
|
137
|
+
let coatSample = textureSample(clearcoatMap, materialSampler, s.uv).rg;
|
|
138
|
+
let roughness = clamp(s.material.x * orm.g, 0.04, 1.0);
|
|
139
|
+
let metalness = clamp(s.material.y * orm.b, 0.0, 1.0);
|
|
140
|
+
let orthogonalTangent = tangent - n * dot(n,tangent);
|
|
141
|
+
let orthogonalBitangent = bitangent - n * dot(n,bitangent);
|
|
142
|
+
if (dpScale > 1e-20 && uvScale > 1e-20 && abs(determinant) > 1e-6 && dot(orthogonalTangent,orthogonalTangent) > 1e-12 && dot(orthogonalBitangent,orthogonalBitangent) > 1e-12) {
|
|
143
|
+
let t = normalize(orthogonalTangent) * sign(determinant);
|
|
144
|
+
let b = normalize(orthogonalBitangent) * sign(determinant);
|
|
145
|
+
n = normalize(t * sampledNormal.x * materialParameters.x + b * sampledNormal.y * materialParameters.x + n * max(0.001,sampledNormal.z));
|
|
146
|
+
}
|
|
147
|
+
if (materialParameters.y < 0.5 && s.material.z > 2.5) {
|
|
109
148
|
let rock = smoothstep(0.45,0.68,noise(s.world*0.27));
|
|
110
149
|
c = mix(c,vec3f(0.16,0.145,0.11),rock*0.7);
|
|
111
150
|
c *= 0.75+0.45*noise(s.world*2.4);
|
|
112
151
|
n = normalize(n+vec3f(noise(s.world*4.0)-0.5,0,noise(s.world*4.0+vec3f(13))-0.5)*0.28);
|
|
113
|
-
} else if (s.material.z > 0.5 && s.material.z < 1.5) {
|
|
152
|
+
} else if (materialParameters.y < 0.5 && s.material.z > 0.5 && s.material.z < 1.5) {
|
|
114
153
|
let grain = noise(s.world * vec3f(18, 3, 1.6));
|
|
115
154
|
c *= 0.82 + 0.30 * grain;
|
|
116
|
-
} else if (s.material.z > 1.5) { c *= 0.88 + 0.18 * noise(s.world * 12.0); }
|
|
155
|
+
} else if (materialParameters.y < 0.5 && s.material.z > 1.5) { c *= 0.88 + 0.18 * noise(s.world * 12.0); }
|
|
117
156
|
let hemi = mix(vec3f(0.06,0.05,0.035), vec3f(0.16,0.22,0.29), n.y * 0.5 + 0.5);
|
|
118
|
-
let direct = brdf(c, n, v,
|
|
119
|
-
let ambient = c * hemi * (1.0 -
|
|
120
|
-
let env = sky(reflect(-v,n)) * mix(vec3f(0.04), c,
|
|
121
|
-
|
|
157
|
+
let direct = brdf(c, n, v, roughness, metalness) * vec3f(3.0, 2.65, 2.15) * visibility(s.world, n);
|
|
158
|
+
let ambient = c * hemi * (1.0 - metalness * 0.7) * orm.r;
|
|
159
|
+
let env = sky(reflect(-v,n)) * mix(vec3f(0.04), c, metalness) * (1.0-roughness) * orm.r * 0.30;
|
|
160
|
+
let baseRadiance = direct + ambient + env;
|
|
161
|
+
let coatCoverage = materialParameters.z * coatSample.r;
|
|
162
|
+
if (coatCoverage <= 0.0) { return fog(baseRadiance,s.world); }
|
|
163
|
+
// KHR_materials_clearcoat's simple Fresnel layering: varnish has its own
|
|
164
|
+
// smooth normal and roughness. Its reflection never inherits the wood colour.
|
|
165
|
+
let coatFresnel = 0.04 + 0.96 * pow(1.0-clamp(abs(dot(coatNormal,v)),0.0,1.0),5.0);
|
|
166
|
+
let coatWeight = coatCoverage * coatFresnel;
|
|
167
|
+
let coatRoughness = clamp(materialParameters.w * coatSample.g,0.04,1.0);
|
|
168
|
+
let coatDirect = vec3f(3.0,2.65,2.15) * specularLobe(coatNormal,v,coatRoughness) * visibility(s.world,coatNormal);
|
|
169
|
+
let coatEnvironment = sky(reflect(-v,coatNormal)) * (1.0-coatRoughness) * orm.r * 0.30;
|
|
170
|
+
return fog(baseRadiance * (1.0-coatWeight) + (coatDirect+coatEnvironment) * coatWeight, s.world);
|
|
122
171
|
}
|
|
123
172
|
@fragment fn surfaceFragment(s: Surface, @builtin(front_facing) front: bool) -> @location(0) vec4f {
|
|
124
173
|
return vec4f(display(surfaceRadiance(s,front)),1);
|
|
@@ -172,7 +221,8 @@ struct WaterResult { @location(0) colour: vec4f, @builtin(frag_depth) depth: f32
|
|
|
172
221
|
return vec4f(sky(cameraRay(pixel.xy)),1);
|
|
173
222
|
}
|
|
174
223
|
@fragment fn reflectionSurface(s: Surface, @builtin(front_facing) front: bool) -> @location(0) vec4f {
|
|
224
|
+
let radiance = surfaceRadiance(s,front);
|
|
175
225
|
if (s.world.y < frame.viewport.z) { discard; }
|
|
176
|
-
return vec4f(
|
|
226
|
+
return vec4f(radiance,1);
|
|
177
227
|
}
|
|
178
228
|
`;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Decoded RGBA contract matches gpu-shared's glTF loader. No network or DOM work.
|
|
2
|
+
const MAX_BYTES = 64 * 1024 * 1024;
|
|
3
|
+
const MAPS = ["baseColor", "normal", "orm", "clearcoatMap"];
|
|
4
|
+
const defaults = {
|
|
5
|
+
baseColor: [255, 255, 255, 255],
|
|
6
|
+
normal: [128, 128, 255, 255],
|
|
7
|
+
orm: [255, 255, 255, 255],
|
|
8
|
+
clearcoatMap: [255, 255, 255, 255],
|
|
9
|
+
};
|
|
10
|
+
const dimension = (n) =>
|
|
11
|
+
Number.isInteger(n) && n >= 1 && n <= 2048 && (n & (n - 1)) === 0;
|
|
12
|
+
|
|
13
|
+
export function validateNativeMaterials(materials) {
|
|
14
|
+
if (!Array.isArray(materials) || materials.length > 16)
|
|
15
|
+
throw new Error("materials must contain at most 16 entries.");
|
|
16
|
+
let bytes = 0;
|
|
17
|
+
for (const material of materials) {
|
|
18
|
+
if (!material || typeof material !== "object")
|
|
19
|
+
throw new Error("Each material must be an object.");
|
|
20
|
+
if (
|
|
21
|
+
!Number.isFinite(material.normalScale ?? 1) ||
|
|
22
|
+
(material.normalScale ?? 1) < 0 ||
|
|
23
|
+
(material.normalScale ?? 1) > 4
|
|
24
|
+
) {
|
|
25
|
+
throw new Error("material normalScale must be between 0 and 4.");
|
|
26
|
+
}
|
|
27
|
+
for (const key of ["clearcoat", "clearcoatRoughness"]) {
|
|
28
|
+
const value = material[key] ?? 0;
|
|
29
|
+
if (!Number.isFinite(value) || value < 0 || value > 1) {
|
|
30
|
+
throw new Error(`material ${key} must be between 0 and 1.`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
for (const key of MAPS) {
|
|
34
|
+
const map = material[key];
|
|
35
|
+
if (map === undefined) continue;
|
|
36
|
+
if (
|
|
37
|
+
!map ||
|
|
38
|
+
!dimension(map.width) ||
|
|
39
|
+
!dimension(map.height) ||
|
|
40
|
+
!(
|
|
41
|
+
map.data instanceof Uint8Array ||
|
|
42
|
+
map.data instanceof Uint8ClampedArray
|
|
43
|
+
) ||
|
|
44
|
+
map.data.length !== map.width * map.height * 4
|
|
45
|
+
) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`material ${key} texture must contain RGBA8 data with power-of-two dimensions between 1 and 2048.`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
bytes += map.data.byteLength;
|
|
51
|
+
if (bytes > MAX_BYTES)
|
|
52
|
+
throw new Error("material textures exceed the 64 MiB source budget.");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const linear = (x) => (x <= 0.04045 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4);
|
|
57
|
+
const srgb = (x) =>
|
|
58
|
+
x <= 0.0031308 ? x * 12.92 : 1.055 * x ** (1 / 2.4) - 0.055;
|
|
59
|
+
|
|
60
|
+
function nextMip(image, colour) {
|
|
61
|
+
const width = Math.max(1, image.width / 2),
|
|
62
|
+
height = Math.max(1, image.height / 2);
|
|
63
|
+
const data = new Uint8Array(width * height * 4);
|
|
64
|
+
for (let y = 0; y < height; y++)
|
|
65
|
+
for (let x = 0; x < width; x++)
|
|
66
|
+
for (let c = 0; c < 4; c++) {
|
|
67
|
+
let sum = 0;
|
|
68
|
+
for (let dy = 0; dy < 2; dy++)
|
|
69
|
+
for (let dx = 0; dx < 2; dx++) {
|
|
70
|
+
const offset =
|
|
71
|
+
(Math.min(image.height - 1, y * 2 + dy) * image.width +
|
|
72
|
+
Math.min(image.width - 1, x * 2 + dx)) *
|
|
73
|
+
4 +
|
|
74
|
+
c;
|
|
75
|
+
const value = image.data[offset] / 255;
|
|
76
|
+
sum += colour && c < 3 ? linear(value) : value;
|
|
77
|
+
}
|
|
78
|
+
data[(y * width + x) * 4 + c] = Math.round(
|
|
79
|
+
255 * (colour && c < 3 ? srgb(sum / 4) : sum / 4),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return { width, height, data };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function createNativeMaterialTextures(device, materials, own) {
|
|
86
|
+
// Index zero is the backwards-compatible material; caller materials start at one.
|
|
87
|
+
return [{}, ...materials].map((material, index) => {
|
|
88
|
+
const result = {
|
|
89
|
+
normalScale: material.normal ? (material.normalScale ?? 1) : 0,
|
|
90
|
+
authored: ["baseColor", "normal", "orm"].some((key) => material[key]),
|
|
91
|
+
clearcoat: material.clearcoat ?? 0,
|
|
92
|
+
clearcoatRoughness: material.clearcoatRoughness ?? 0,
|
|
93
|
+
};
|
|
94
|
+
for (const key of MAPS) {
|
|
95
|
+
let level = material[key] ?? {
|
|
96
|
+
width: 1,
|
|
97
|
+
height: 1,
|
|
98
|
+
data: new Uint8Array(defaults[key]),
|
|
99
|
+
};
|
|
100
|
+
const mipLevelCount = 1 + Math.log2(Math.max(level.width, level.height));
|
|
101
|
+
const texture = own(
|
|
102
|
+
device.createTexture({
|
|
103
|
+
label: `native.material.${index}.${key}`,
|
|
104
|
+
size: { width: level.width, height: level.height },
|
|
105
|
+
format: key === "baseColor" ? "rgba8unorm-srgb" : "rgba8unorm",
|
|
106
|
+
mipLevelCount,
|
|
107
|
+
usage: 0x06,
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
for (let mipLevel = 0; mipLevel < mipLevelCount; mipLevel++) {
|
|
111
|
+
device.queue.writeTexture(
|
|
112
|
+
{ texture, mipLevel },
|
|
113
|
+
level.data,
|
|
114
|
+
{ bytesPerRow: level.width * 4, rowsPerImage: level.height },
|
|
115
|
+
{ width: level.width, height: level.height, depthOrArrayLayers: 1 },
|
|
116
|
+
);
|
|
117
|
+
if (mipLevel + 1 < mipLevelCount)
|
|
118
|
+
level = nextMip(level, key === "baseColor");
|
|
119
|
+
}
|
|
120
|
+
result[key] = texture.createView();
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
});
|
|
124
|
+
}
|