p5
Version:
[](https://www.npmjs.com/package/p5)
229 lines (191 loc) • 7.3 kB
JavaScript
const uniforms = `
struct Uniforms {
uModelViewMatrix: mat4x4<f32>,
uProjectionMatrix: mat4x4<f32>,
uNormalMatrix: mat3x3<f32>,
roughness: f32,
};
`;
// Shared WGSL functions
const sharedFunctions = `
const PI = 3.14159265359;
fn nTOE(v: vec3<f32>) -> vec2<f32> {
// x = r sin(phi) cos(theta)
// y = r cos(phi)
// z = r sin(phi) sin(theta)
let phi = acos(v.y);
// if phi is 0, then there are no x, z components
var theta = 0.0;
// else
theta = acos(v.x / sin(phi));
let sinTheta = v.z / sin(phi);
if (sinTheta < 0.0) {
// Turn it into -theta, but in the 0-2PI range
theta = 2.0 * PI - theta;
}
theta = theta / (2.0 * PI);
let phiNorm = phi / PI;
return vec2<f32>(phiNorm, theta);
}
fn random(p: vec2<f32>) -> f32 {
let p3 = fract(vec3<f32>(p.x, p.y, p.x) * 0.1031);
let dotP3 = dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
`;
const imageLightVertexShader = `
struct VertexInput {
aPosition: vec3<f32>,
aNormal: vec3<f32>,
aTexCoord: vec2<f32>,
}
struct VertexOutput {
Position: vec4<f32>,
localPos: vec3<f32>,
vWorldNormal: vec3<f32>,
vWorldPosition: vec3<f32>,
vTexCoord: vec2<f32>,
}
${uniforms}
var<uniform> uniforms: Uniforms;
fn main(input: VertexInput) -> VertexOutput {
var output: VertexOutput;
// Multiply the position by the matrix
let viewModelPosition = uniforms.uModelViewMatrix * vec4<f32>(input.aPosition, 1.0);
output.Position = uniforms.uProjectionMatrix * viewModelPosition;
// Orient the normals and pass to the fragment shader
output.vWorldNormal = uniforms.uNormalMatrix * input.aNormal;
// Send the view position to the fragment shader
output.vWorldPosition = viewModelPosition.xyz;
output.localPos = output.vWorldPosition;
output.vTexCoord = input.aTexCoord;
return output;
}
`;
const imageLightDiffusedFragmentShader = `
struct FragmentInput {
localPos: vec3<f32>,
vTexCoord: vec2<f32>,
}
${uniforms}
var<uniform> uniforms: Uniforms;
var environmentMap: texture_2d<f32>;
var environmentMap_sampler: sampler;
${sharedFunctions}
fn main(input: FragmentInput) -> vec4<f32> {
// The sample direction equals the hemisphere's orientation
let phi = input.vTexCoord.x * 2.0 * PI;
let theta = input.vTexCoord.y * PI;
let x = sin(theta) * cos(phi);
let y = sin(theta) * sin(phi);
let z = cos(theta);
let normal = vec3<f32>(x, y, z);
// Discretely sampling the hemisphere given the integral's
// spherical coordinates translates to the following fragment code:
var irradiance = vec3<f32>(0.0);
let up = vec3<f32>(0.0, 1.0, 0.0);
let right = normalize(cross(up, normal));
let upNorm = normalize(cross(normal, right));
// We specify a fixed sampleDelta delta value to traverse
// the hemisphere; decreasing or increasing the sample delta
// will increase or decrease the accuracy respectively.
let sampleDelta = 0.100;
var nrSamples = 0.0;
let randomOffset = random(input.vTexCoord) * sampleDelta;
for (var rawPhi = 0.0; rawPhi < 2.0 * PI; rawPhi += sampleDelta) {
let phiSample = rawPhi + randomOffset;
for (var rawTheta = 0.0; rawTheta < 0.5 * PI; rawTheta += sampleDelta) {
let thetaSample = rawTheta + randomOffset;
// spherical to cartesian (in tangent space) // tangent space to world // add each sample result to irradiance
let xSample = sin(thetaSample) * cos(phiSample);
let ySample = sin(thetaSample) * sin(phiSample);
let zSample = cos(thetaSample);
let tangentSample = vec3<f32>(xSample, ySample, zSample);
let sampleVec = tangentSample.x * right + tangentSample.y * upNorm + tangentSample.z * normal;
let envSample = textureSample(environmentMap, environmentMap_sampler, nTOE(sampleVec));
irradiance += envSample.xyz * cos(thetaSample) * sin(thetaSample);
nrSamples += 1.0;
}
}
// divide by the total number of samples taken, giving us the average sampled irradiance.
irradiance = PI * irradiance * (1.0 / nrSamples);
return vec4<f32>(irradiance, 1.0);
}
`;
const imageLightSpecularFragmentShader = `
struct FragmentInput {
localPos: vec3<f32>,
vTexCoord: vec2<f32>,
}
${uniforms}
var<uniform> uniforms: Uniforms;
var environmentMap: texture_2d<f32>;
var environmentMap_sampler: sampler;
${sharedFunctions}
fn VanDerCorput(nIn: i32, base: i32) -> f32 {
// Use the bit manipulation version for WebGPU (equivalent to WEBGL2 version)
var n = u32(nIn);
n = (n << 16u) | (n >> 16u);
n = ((n & 0x55555555u) << 1u) | ((n & 0xAAAAAAAAu) >> 1u);
n = ((n & 0x33333333u) << 2u) | ((n & 0xCCCCCCCCu) >> 2u);
n = ((n & 0x0F0F0F0Fu) << 4u) | ((n & 0xF0F0F0F0u) >> 4u);
n = ((n & 0x00FF00FFu) << 8u) | ((n & 0xFF00FF00u) >> 8u);
return f32(n) * 2.3283064365386963e-10; // / 0x100000000
}
fn HammersleyNoBitOps(i: i32, N: i32) -> vec2<f32> {
return vec2<f32>(f32(i) / f32(N), VanDerCorput(i, 2));
}
fn ImportanceSampleGGX(Xi: vec2<f32>, N: vec3<f32>, roughness: f32) -> vec3<f32> {
let a = roughness * roughness;
let phi = 2.0 * PI * Xi.x;
let cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a * a - 1.0) * Xi.y));
let sinTheta = sqrt(1.0 - cosTheta * cosTheta);
// from spherical coordinates to cartesian coordinates
var H: vec3<f32>;
H.x = cos(phi) * sinTheta;
H.y = sin(phi) * sinTheta;
H.z = cosTheta;
// from tangent-space vector to world-space sample vector
let up = select(vec3<f32>(0.0, 0.0, 1.0), vec3<f32>(1.0, 0.0, 0.0), abs(N.z) < 0.999);
let tangent = normalize(cross(up, N));
let bitangent = cross(N, tangent);
let sampleVec = tangent * H.x + bitangent * H.y + N * H.z;
return normalize(sampleVec);
}
fn main(input: FragmentInput) -> vec4<f32> {
let SAMPLE_COUNT = 400i; // 4096
let lowRoughnessLimit = i32(pow(2.0, (uniforms.roughness + 0.1) * 20.0));
var totalWeight = 0.0;
var prefilteredColor = vec3<f32>(0.0);
let phi = input.vTexCoord.x * 2.0 * PI;
let theta = input.vTexCoord.y * PI;
let x = sin(theta) * cos(phi);
let y = sin(theta) * sin(phi);
let z = cos(theta);
let N = vec3<f32>(x, y, z);
let V = N;
for (var i = 0i; i < SAMPLE_COUNT; i++) {
// break at smaller sample numbers for low roughness levels
if (i == lowRoughnessLimit) {
break;
}
let Xi = HammersleyNoBitOps(i, SAMPLE_COUNT);
let H = ImportanceSampleGGX(Xi, N, uniforms.roughness);
let L = normalize(2.0 * dot(V, H) * H - V);
let NdotL = max(dot(N, L), 0.0);
// Always sample the texture to maintain uniform control flow
let envSample = textureSample(environmentMap, environmentMap_sampler, nTOE(L));
// Only add to accumulators if NdotL > 0
if (NdotL > 0.0) {
prefilteredColor += envSample.xyz * NdotL;
totalWeight += NdotL;
}
}
prefilteredColor = prefilteredColor / totalWeight;
return vec4<f32>(prefilteredColor, 1.0);
}
`;
export { imageLightDiffusedFragmentShader, imageLightSpecularFragmentShader, imageLightVertexShader };