Skip to content

Commit 5adc648

Browse files
committed
Add image light visual tests
1 parent ecb6bfd commit 5adc648

19 files changed

Lines changed: 186 additions & 46 deletions

File tree

src/core/p5.Renderer3D.js

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1644,6 +1644,9 @@ export class Renderer3D extends Renderer {
16441644
let specularLight = this.getSpecularTexture(this.states.activeImageLight);
16451645

16461646
shader.setUniform("environmentMapSpecular", specularLight);
1647+
} else {
1648+
shader.setUniform("environmentMapDiffused", this._getEmptyTexture());
1649+
shader.setUniform("environmentMapSpecular", this._getEmptyTexture());
16471650
}
16481651
}
16491652

@@ -1830,7 +1833,7 @@ export class Renderer3D extends Renderer {
18301833
* an angle to each pixel. This creates and caches textures for reuse, since
18311834
* creating this texture is somewhat expensive.
18321835
*/
1833-
getDiffusedTexture(input) {
1836+
makeDiffusedTexture(input) {
18341837
// if one already exists for a given input image
18351838
if (this.diffusedTextures.get(input) != null) {
18361839
return this.diffusedTextures.get(input);
@@ -1861,6 +1864,9 @@ export class Renderer3D extends Renderer {
18611864
this.diffusedTextures.set(input, newFramebuffer);
18621865
return newFramebuffer;
18631866
}
1867+
getDiffusedTexture(input) {
1868+
return this.diffusedTextures.get(input);
1869+
}
18641870

18651871
/*
18661872
* used in imageLight,
@@ -1871,7 +1877,7 @@ export class Renderer3D extends Renderer {
18711877
* Storing the texture for input image in map called `specularTextures`
18721878
* maps the input Image to a p5.MipmapTexture
18731879
*/
1874-
getSpecularTexture(input) {
1880+
makeSpecularTexture(input) {
18751881
// check if already exits (there are tex of diff resolution so which one to check)
18761882
// currently doing the whole array
18771883
if (this.specularTextures.get(input) != null) {
@@ -1931,6 +1937,9 @@ export class Renderer3D extends Renderer {
19311937
this.specularTextures.set(input, tex);
19321938
return tex;
19331939
}
1940+
getSpecularTexture(input) {
1941+
return this.specularTextures.get(input);
1942+
}
19341943

19351944
_getSphereMapping(img) {
19361945
if (!this.sphereMapping) {
@@ -1949,6 +1958,7 @@ export class Renderer3D extends Renderer {
19491958
const angleX = p5.mix(uFovX/2.0, -uFovX/2.0, inputs.texCoord.x);
19501959
let rotatedNormal = p5.normalize([angleX, angleY, 1]);
19511960
rotatedNormal = [
1961+
// Don't mind me, just doing matrix vector multiplication...
19521962
p5.dot(rotatedNormal, uN1),
19531963
p5.dot(rotatedNormal, uN2),
19541964
p5.dot(rotatedNormal, uN3),
@@ -1957,8 +1967,8 @@ export class Renderer3D extends Renderer {
19571967
rotatedNormal.z = rotatedNormal.x;
19581968
rotatedNormal.x = -temp;
19591969
const suv = [
1960-
0.5 + 0.5 * (-rotatedNormal.y),
1961-
p5.atan(rotatedNormal.z, rotatedNormal.x) / (2.0 * p5.PI) + 0.5
1970+
p5.atan(rotatedNormal.z, rotatedNormal.x) / (2.0 * p5.PI) + 0.5,
1971+
0.5 + 0.5 * (-rotatedNormal.y)
19621972
];
19631973
return p5.getTexture(uEnvMap, suv);
19641974
})
@@ -1968,10 +1978,12 @@ export class Renderer3D extends Renderer {
19681978
this.scratchMat3.invert(this.scratchMat3); // uNMMatrix is 3x3
19691979
this.sphereMapping.setUniform("uFovY", this.states.curCamera.cameraFOV);
19701980
this.sphereMapping.setUniform("uAspect", this.states.curCamera.aspectRatio);
1971-
// this.sphereMapping.setUniform("uNewNormalMatrix", this.scratchMat3.mat3);
1972-
this.sphereMapping.setUniform("uN1", this.scratchMat3.mat3.slice(0, 3));
1973-
this.sphereMapping.setUniform("uN2", this.scratchMat3.mat3.slice(3, 6));
1974-
this.sphereMapping.setUniform("uN3", this.scratchMat3.mat3.slice(6));
1981+
// Pass in the normal matrix as three vectors. TODO replace this with
1982+
// an actual matrix uniform once we have those again.
1983+
const m = this.scratchMat3.mat3;
1984+
this.sphereMapping.setUniform("uN1", [m[0], m[3], m[6]]);
1985+
this.sphereMapping.setUniform("uN2", [m[1], m[4], m[7]]);
1986+
this.sphereMapping.setUniform("uN3", [m[2], m[5], m[8]]);
19751987
this.sphereMapping.setUniform("uEnvMap", img);
19761988
return this.sphereMapping;
19771989
}

src/strands/p5.strands.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ function strands(p5, fn) {
2525
//////////////////////////////////////////////
2626
// Global Runtime
2727
//////////////////////////////////////////////
28-
function initStrandsContext(ctx, backend, { active = false } = {}) {
28+
function initStrandsContext(ctx, backend, { active = false, renderer = null, baseShader = null } = {}) {
2929
ctx.dag = createDirectedAcyclicGraph();
3030
ctx.cfg = createControlFlowGraph();
3131
ctx.uniforms = [];
@@ -35,6 +35,8 @@ function strands(p5, fn) {
3535
ctx.globalAssignments = [];
3636
ctx.backend = backend;
3737
ctx.active = active;
38+
ctx.renderer = renderer;
39+
ctx.baseShader = baseShader;
3840
ctx.previousFES = p5.disableFriendlyErrors;
3941
ctx.windowOverrides = {};
4042
ctx.fnOverrides = {};
@@ -77,6 +79,8 @@ function strands(p5, fn) {
7779
// const backend = glslBackend;
7880
initStrandsContext(strandsContext, this._renderer.strandsBackend, {
7981
active: true,
82+
renderer: this._renderer,
83+
baseShader: this,
8084
});
8185
createShaderHooksFunctions(strandsContext, fn, this);
8286
// TODO: expose this, is internal for debugging for now.

src/strands/strands_codegen.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,14 @@ export function generateShaderCode(strandsContext) {
1616

1717
for (const {name, typeInfo, defaultValue} of strandsContext.uniforms) {
1818
const key = backend.generateHookUniformKey(name, typeInfo);
19-
hooksObj.uniforms[key] = defaultValue;
19+
if (key !== null) {
20+
hooksObj.uniforms[key] = defaultValue;
21+
}
22+
}
23+
24+
// Add texture bindings to declarations for WebGPU backend
25+
if (backend.addTextureBindingsToDeclarations) {
26+
backend.addTextureBindingsToDeclarations(strandsContext);
2027
}
2128

2229
for (const { hookType, rootNodeID, entryBlockID, shaderContext } of strandsContext.hooks) {

src/webgl/light.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1558,6 +1558,9 @@ function light(p5, fn){
15581558
// for sending uniforms to the fillshader
15591559
this.states.setValue('activeImageLight', img);
15601560
this.states.setValue('enableLighting', true);
1561+
// Make sure textures are cached
1562+
this.makeDiffusedTexture(img);
1563+
this.makeSpecularTexture(img);
15611564
};
15621565

15631566
Renderer3D.prototype.lights = function() {

src/webgl/p5.RendererGL.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import webgl2CompatibilityShader from "./shaders/webgl2Compatibility.glsl";
2626
import normalVert from "./shaders/normal.vert";
2727
import normalFrag from "./shaders/normal.frag";
2828
import basicFrag from "./shaders/basic.frag";
29-
import sphereMappingFrag from "./shaders/sphereMapping.frag";
3029
import lightVert from "./shaders/light.vert";
3130
import lightTextureFrag from "./shaders/light_texture.frag";
3231
import phongVert from "./shaders/phong.vert";
@@ -46,7 +45,6 @@ const defaultShaders = {
4645
normalVert,
4746
normalFrag,
4847
basicFrag,
49-
sphereMappingFrag,
5048
lightVert: lightingShader + lightVert,
5149
lightTextureFrag,
5250
phongVert,
@@ -61,7 +59,6 @@ const defaultShaders = {
6159
filterBaseVert,
6260
filterBaseFrag,
6361
};
64-
let sphereMapping = defaultShaders.sphereMappingFrag;
6562
for (const key in defaultShaders) {
6663
defaultShaders[key] = webgl2CompatibilityShader + defaultShaders[key];
6764
}

src/webgl/shaders/sphereMapping.frag

Lines changed: 0 additions & 26 deletions
This file was deleted.

src/webgpu/p5.RendererWebGPU.js

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -997,6 +997,8 @@ class RendererWebGPU extends Renderer3D {
997997

998998
// this._pInst.background('red');
999999
this._pInst.push();
1000+
this.states.setValue('enableLighting', false);
1001+
this.states.setValue('activeImageLight', null);
10001002
this._pInst.setCamera(this.finalCamera);
10011003
this._pInst.resetShader();
10021004
this._pInst.imageMode(this._pInst.CENTER);
@@ -1344,7 +1346,7 @@ class RendererWebGPU extends Renderer3D {
13441346

13451347
// Extract the struct name from the uniform variable declaration
13461348
const uniformVarRegex = /@group\(0\)\s+@binding\(0\)\s+var<uniform>\s+(\w+)\s*:\s*(\w+);/;
1347-
const uniformVarMatch = uniformVarRegex.exec(shader._vertSrc);
1349+
const uniformVarMatch = uniformVarRegex.exec(shader.vertSrc());
13481350
if (!uniformVarMatch) {
13491351
throw new Error('Expected a uniform struct bound to @group(0) @binding(0)');
13501352
}
@@ -1355,8 +1357,8 @@ class RendererWebGPU extends Renderer3D {
13551357
// TODO: support other texture types
13561358
const samplerRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var\s+(\w+)\s*:\s*(texture_2d<f32>|sampler);/g;
13571359
for (const [src, visibility] of [
1358-
[shader._vertSrc, GPUShaderStage.VERTEX],
1359-
[shader._fragSrc, GPUShaderStage.FRAGMENT]
1360+
[shader.vertSrc(), GPUShaderStage.VERTEX],
1361+
[shader.fragSrc(), GPUShaderStage.FRAGMENT]
13601362
]) {
13611363
let match;
13621364
while ((match = samplerRegex.exec(src)) !== null) {
@@ -1398,6 +1400,27 @@ class RendererWebGPU extends Renderer3D {
13981400
return [...Object.values(uniforms).sort((a, b) => a.index - b.index), ...Object.values(samplers)];
13991401
}
14001402

1403+
getNextBindingIndex(shader, group = 0) {
1404+
// Get the highest binding index in the specified group and return the next available
1405+
const samplerRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var\s+(\w+)\s*:\s*(texture_2d<f32>|sampler|uniform)/g;
1406+
let maxBindingIndex = -1;
1407+
1408+
for (const [src, visibility] of [
1409+
[shader.vertSrc(), GPUShaderStage.VERTEX],
1410+
[shader.fragSrc(), GPUShaderStage.FRAGMENT]
1411+
]) {
1412+
let match;
1413+
while ((match = samplerRegex.exec(src)) !== null) {
1414+
const [_, groupIndex, bindingIndex] = match;
1415+
if (parseInt(groupIndex) === group) {
1416+
maxBindingIndex = Math.max(maxBindingIndex, parseInt(bindingIndex));
1417+
}
1418+
}
1419+
}
1420+
1421+
return maxBindingIndex + 1;
1422+
}
1423+
14011424
updateUniformValue(_shader, uniform, data) {
14021425
if (uniform.isSampler) {
14031426
uniform.texture =

src/webgpu/strands_wgslBackend.js

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,27 @@ export const wgslBackend = {
191191

192192
return mutableCopies ? firstLine + '\n' + mutableCopies : firstLine;
193193
},
194+
addTextureBindingsToDeclarations(strandsContext) {
195+
// Add texture and sampler bindings for sampler2D uniforms to both vertex and fragment declarations
196+
if (!strandsContext.renderer || !strandsContext.baseShader) return;
197+
198+
// Get the next available binding index from the renderer
199+
let bindingIndex = strandsContext.renderer.getNextBindingIndex(strandsContext.baseShader);
200+
201+
for (const {name, typeInfo} of strandsContext.uniforms) {
202+
if (typeInfo.baseType === 'sampler2D') {
203+
const textureBinding = `@group(0) @binding(${bindingIndex}) var ${name}: texture_2d<f32>;`;
204+
const samplerBinding = `@group(0) @binding(${bindingIndex + 1}) var ${name}_sampler: sampler;`;
205+
206+
strandsContext.vertexDeclarations.add(textureBinding);
207+
strandsContext.vertexDeclarations.add(samplerBinding);
208+
strandsContext.fragmentDeclarations.add(textureBinding);
209+
strandsContext.fragmentDeclarations.add(samplerBinding);
210+
211+
bindingIndex += 2;
212+
}
213+
}
214+
},
194215
getTypeName(baseType, dimension) {
195216
const primitiveTypeName = TypeNames[baseType + dimension]
196217
if (!primitiveTypeName) {
@@ -199,6 +220,11 @@ export const wgslBackend = {
199220
return primitiveTypeName;
200221
},
201222
generateHookUniformKey(name, typeInfo) {
223+
// For sampler2D types, we don't add them to the uniform struct
224+
// Instead, they become separate texture and sampler bindings
225+
if (typeInfo.baseType === 'sampler2D') {
226+
return null; // Signal that this should not be added to uniform struct
227+
}
202228
return `${name}: ${this.getTypeName(typeInfo.baseType, typeInfo.dimension)}`;
203229
},
204230
generateVaryingVariable(varName, typeInfo) {
@@ -332,9 +358,9 @@ export const wgslBackend = {
332358
}
333359
}
334360

335-
// Check if this is a uniform variable
336-
const isUniform = generationContext.strandsContext?.uniforms?.some(uniform => uniform.name === node.identifier);
337-
if (isUniform) {
361+
// Check if this is a uniform variable (but not a texture)
362+
const uniform = generationContext.strandsContext?.uniforms?.find(uniform => uniform.name === node.identifier);
363+
if (uniform && uniform.typeInfo.baseType !== 'sampler2D') {
338364
return `uniforms.${node.identifier}`;
339365
}
340366

@@ -366,7 +392,13 @@ export const wgslBackend = {
366392
return `${left} % ${right}`;
367393
}
368394
}
369-
395+
396+
// Convert atan(y, x) to atan2(y, x) in WGSL
397+
if (node.identifier === 'atan' && node.dependsOn.length === 2) {
398+
const functionArgs = node.dependsOn.map(arg => this.generateExpression(generationContext, dag, arg));
399+
return `atan2(${functionArgs.join(', ')})`;
400+
}
401+
370402
const functionArgs = node.dependsOn.map(arg =>this.generateExpression(generationContext, dag, arg));
371403
return `${node.identifier}(${functionArgs.join(', ')})`;
372404
}

test/unit/assets/spheremap.jpg

533 KB
Loading

test/unit/visual/cases/webgl.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,4 +790,42 @@ visualSuite('WebGL', function() {
790790
screenshot();
791791
});
792792
});
793+
794+
visualSuite("Image Based Lighting", function () {
795+
const shinesses = [50, 150];
796+
for (const shininess of shinesses) {
797+
visualTest(
798+
`imageLight with panorama and shininess ${shininess}`,
799+
async function (p5, screenshot) {
800+
p5.createCanvas(100, 100, p5.WEBGL);
801+
802+
// Load the environment map
803+
const env = await p5.loadImage('/unit/assets/spheremap.jpg');
804+
p5.clear();
805+
806+
// Set up panorama background
807+
p5.panorama(env);
808+
809+
// Set up image-based lighting
810+
p5.push();
811+
p5.imageLight(env);
812+
p5.ambientLight(10);
813+
814+
// Configure materials
815+
p5.specularMaterial(255);
816+
p5.shininess(shininess);
817+
p5.metalness(100);
818+
p5.noStroke();
819+
820+
// Draw a sphere in the center
821+
p5.fill('white');
822+
p5.sphere(25);
823+
824+
p5.pop();
825+
826+
screenshot();
827+
},
828+
);
829+
}
830+
});
793831
});

0 commit comments

Comments
 (0)