diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 57fb3f55e3..a84aa56679 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -153,6 +153,9 @@ export class Renderer3D extends Renderer { this.states._shininessTex = null; this.states._normalTex = null; this.states._normalScale = 1; + // how to read _normalTex: 0 = tangent-space normal map (rgb is the normal), + // 1 = bump map (brightness is height, the normal comes from its slope) + this.states._normalMapMode = 0; this.states.textureMode = constants.IMAGE; this.states.textureWrapX = constants.CLAMP; this.states.textureWrapY = constants.CLAMP; @@ -732,6 +735,7 @@ export class Renderer3D extends Renderer { if (partState.normalScale != null) { this.states.setValue('_normalScale', partState.normalScale); } + this.states.setValue('_normalMapMode', partState.normalMapMode ?? 0); } } @@ -1652,6 +1656,15 @@ export class Renderer3D extends Renderer { fillShader.setUniform('uHasNormalMap', !!this.states._normalTex); fillShader.setUniform('uNormalSampler', this.states._normalTex || empty); fillShader.setUniform('uNormalScale', this.states._normalScale); + fillShader.setUniform('uNormalMapMode', this.states._normalMapMode); + // a bump map reads its neighbours to find the slope, so it needs to know + // how far apart texels are. falls back to a sane size for sources that + // don't report their dimensions. + const normalTex = this.states._normalTex; + fillShader.setUniform('uNormalTexelSize', [ + 1 / (normalTex && normalTex.width ? normalTex.width : 256), + 1 / (normalTex && normalTex.height ? normalTex.height : 256) + ]); } fillShader.setUniform( 'uTint', diff --git a/src/webgl/loading.js b/src/webgl/loading.js index fb3ed933f2..17f26b2604 100755 --- a/src/webgl/loading.js +++ b/src/webgl/loading.js @@ -78,15 +78,21 @@ function parseMtlData(data) { //shininess texture materials[currentMaterial].shininessTexturePath = tokens[1]; } else if (tokens[0] === 'map_Bump' || tokens[0] === 'bump') { - //bump map. the path is the last token; a `-bm ` option can precede - //it to scale the bump strength (maps often use the full range for precision - //and get scaled down here). + //bump map, brightness is height. `-bm ` can precede the path materials[currentMaterial].bumpTexturePath = tokens[tokens.length - 1]; const bmIndex = tokens.indexOf('-bm'); if (bmIndex !== -1 && tokens[bmIndex + 1] !== undefined) { const bm = parseFloat(tokens[bmIndex + 1]); if (!isNaN(bm)) materials[currentMaterial].bumpScale = bm; } + } else if (tokens[0] === 'norm') { + //normal map. not in the original spec, but what most exporters use + materials[currentMaterial].normalTexturePath = tokens[tokens.length - 1]; + const bmIndex = tokens.indexOf('-bm'); + if (bmIndex !== -1 && tokens[bmIndex + 1] !== undefined) { + const bm = parseFloat(tokens[bmIndex + 1]); + if (!isNaN(bm)) materials[currentMaterial].bumpScale = bm; + } } } @@ -123,9 +129,10 @@ function mtlToPartState(material) { // the map scales the base shininess; default the base to 1 when no Ns if (state.shininess == null) state.shininess = 1; } - if (material.normalTexture) { - state.normalTexture = material.normalTexture; - // a -bm multiplier scales the bump strength; defaults to 1 when omitted + // one slot, mode says how to read it. norm wins if both are set + if (material.bumpTexture || material.normalTexture) { + state.normalTexture = material.normalTexture || material.bumpTexture; + state.normalMapMode = material.normalTexture ? 0 : 1; if (material.bumpScale != null) state.normalScale = material.bumpScale; } return state; @@ -138,7 +145,8 @@ const MATERIAL_TEXTURE_MAPS = [ ['specularTexturePath', 'specularTexture'], // map_Ks (specular) ['ambientTexturePath', 'ambientTexture'], // map_Ka (ambient) ['shininessTexturePath', 'shininessTexture'], // map_Ns (shininess) - ['bumpTexturePath', 'normalTexture'] // map_Bump (normal) + ['bumpTexturePath', 'bumpTexture'], // map_Bump (height) + ['normalTexturePath', 'normalTexture'] // norm (tangent-space normal) ]; // load each material's texture maps and hang them on the material so they land @@ -242,10 +250,11 @@ function loading(p5, fn) { * Note: When a `.obj` file references materials stored in a `.mtl` file, * p5.js loads and applies them, so a model with several materials appears the * way it was exported. Each material can use diffuse (`map_Kd`), specular - * (`map_Ks`), ambient (`map_Ka`), shininess (`map_Ns`), and normal - * (`map_Bump`) texture maps. Keep the `.mtl` file and its images alongside - * the `.obj` file so their paths resolve. A texture that fails to load is - * skipped with a warning instead of failing the whole model. + * (`map_Ks`), ambient (`map_Ka`), shininess (`map_Ns`), bump (`map_Bump`), + * and normal (`norm`) texture maps. A bump map is read as a height map, while + * `norm` is read as a tangent-space normal map. Keep the `.mtl` file and its + * images alongside the `.obj` file so their paths resolve. A texture that + * fails to load is skipped with a warning instead of failing the whole model. * * The first way to call `loadModel()` has three optional parameters after the * file path. The first optional parameter, `successCallback`, is a function @@ -839,10 +848,9 @@ function loading(p5, fn) { // normal maps need per-vertex tangents; compute them once on the aggregate // (normals are ready above) so buildMaterialParts hands each part its slice. - // only done when a material actually uses a normal map, so plain models pay - // nothing extra. + // only done when a material actually uses one, so plain models pay nothing const needsTangents = Object.values(materials).some( - m => m && m.normalTexture + m => m && (m.normalTexture || m.bumpTexture) ); if (needsTangents) { model.computeTangents(); diff --git a/src/webgl/material.js b/src/webgl/material.js index 5e6053e69a..3fa865b64a 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -2577,10 +2577,17 @@ function material(p5, fn) { * of map glTF models use. Pass an optional `scale` to tune the strength. * * Call `normalTexture(null)` to turn it off, or scope it between - * push() and pop(). + * `push()` and `pop()`. + * + * `bumpTexture()` creates a similar effect from + * a grayscale height map instead. The two are easy to mix up because they + * produce similar results, but they expect different images: a normal map is + * the blue-tinted kind that stores directions, while a bump map is grayscale + * and stores height. Only one can be active at a time, so setting one + * replaces the other. * * A light source is needed to see the effect. Models loaded with - * loadModel() apply their own normal map from + * `loadModel()` apply their own normal map from * the `.mtl` file's `map_Bump`. * * Note: `normalTexture()` can only be used in WebGL mode. @@ -2603,7 +2610,9 @@ function material(p5, fn) { * normalMap.loadPixels(); * for (let y = 0; y < normalMap.height; y += 1) { * for (let x = 0; x < normalMap.width; x += 1) { - * // Slope of the ridge at this point. + * // Slope of the ridge at this point. The pattern repeats a whole + * // number of times across the image so it tiles, which keeps it from + * // showing a seam where the sphere's texture coordinates wrap around. * let s = sin(((x + y) / normalMap.width) * TWO_PI * 3) * 0.8; * let inv = 1 / sqrt(s * s + s * s + 1); * let i = (x + y * normalMap.width) * 4; @@ -2635,6 +2644,9 @@ function material(p5, fn) { * noStroke(); * fill(200); * + * // Tile the map so it wraps around the sphere without a seam. + * textureWrap(REPEAT); + * * // Add the ridges without changing the geometry. * normalTexture(normalMap); * sphere(40); @@ -2647,6 +2659,102 @@ function material(p5, fn) { return this; }; + /** + * Sets a grayscale image that adds bumps and dents to a shape's surface. + * + * A bump map is a height map: the brightness of the image at each point is + * read as how high the surface is there, and p5.js works out which way the + * surface tilts from how quickly that height changes. Bright areas rise and + * dark areas sink, so lights react to detail that isn't in the geometry. + * + * `bumpTexture()` works like `texture()`, but sets + * the bump map instead of the base color. The parameter, `tex`, is the image + * to use. Passing `null` clears it, as in `bumpTexture(null)`. The optional + * second parameter, `scale`, tunes how pronounced the bumps are. The map can + * also be scoped between `push()` and + * `pop()`. + * + * `normalTexture()` creates a similar effect + * from a tangent-space normal map, the blue-tinted kind that stores + * directions rather than height. A bump map is usually easier to make by + * hand, since it's just a grayscale picture of where the surface is high and + * low. Only one can be active at a time, so setting one replaces the other. + * + * A light source is needed to see the effect. + * + * Note: `bumpTexture()` can only be used in WebGL mode. + * + * @method bumpTexture + * @param {p5.Image|p5.MediaElement|p5.Graphics|p5.Texture|p5.Framebuffer|p5.FramebufferTexture} tex grayscale image to use as the bump map, or `null` to clear it. + * @param {Number} [scale=1] strength multiplier for the bumps. + * @chainable + * + * @example + * // Click and drag the mouse to view the scene from different angles. + * + * let bumpMap; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Build a grayscale height map with a grid of round bumps. + * bumpMap = createImage(64, 64); + * bumpMap.loadPixels(); + * for (let y = 0; y < bumpMap.height; y += 1) { + * for (let x = 0; x < bumpMap.width; x += 1) { + * // Bright where the surface is high, dark where it's low. The pattern + * // repeats a whole number of times across the image so it tiles, + * // which keeps it from showing a seam where the sphere's texture + * // coordinates wrap around. + * let h = sin((x / bumpMap.width) * TWO_PI * 4); + * h *= sin((y / bumpMap.height) * TWO_PI * 4); + * let v = (h * 0.5 + 0.5) * 255; + * let i = (x + y * bumpMap.width) * 4; + * bumpMap.pixels[i] = v; + * bumpMap.pixels[i + 1] = v; + * bumpMap.pixels[i + 2] = v; + * bumpMap.pixels[i + 3] = 255; + * } + * } + * bumpMap.updatePixels(); + * + * describe('A gray sphere lit from the upper left. A grid of round bumps covers its surface.'); + * } + * + * function draw() { + * background(0); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Rock the shape so the lighting shifts across it. + * rotateY(sin(millis() * 0.002) * PI * 0.1); + * + * // Light the sphere from the upper left. + * ambientLight(60); + * pointLight(255, 255, 255, -80, -80, 150); + * noStroke(); + * fill(200); + * + * // Tile the map so it wraps around the sphere without a seam. + * textureWrap(REPEAT); + * + * // Highlights make the raised areas easier to pick out. + * specularMaterial(255); + * shininess(40); + * + * // Raise the bumps without changing the geometry. + * bumpTexture(bumpMap, 4); + * sphere(40); + * } + */ + fn.bumpTexture = function (tex, scale) { + this._assert3d('bumpTexture'); + this._renderer.bumpTexture(tex || null, scale); + + return this; + }; + /** * Sets an image that controls where a shape looks glossy. * @@ -4135,11 +4243,19 @@ function material(p5, fn) { this.states.setValue('fillColor', new Color([1, 1, 1])); }; + // normal maps and bump maps share one texture slot and differ only by the mode + // flag the shader reads, so only one can be active at a time and setting either + // replaces the other. null clears the map, back to the plain shader variant. Renderer3D.prototype.normalTexture = function (tex, scale = 1) { - // null clears the map (back to the plain shader variant); a value sets the - // normal map + its strength. push()/pop() scopes it like any other state. this.states.setValue('_normalTex', tex || null); this.states.setValue('_normalScale', tex ? scale : 1); + this.states.setValue('_normalMapMode', 0); + }; + + Renderer3D.prototype.bumpTexture = function (tex, scale = 1) { + this.states.setValue('_normalTex', tex || null); + this.states.setValue('_normalScale', tex ? scale : 1); + this.states.setValue('_normalMapMode', tex ? 1 : 0); }; // the remaining map setters mirror _applyPartState: setting a map also turns diff --git a/src/webgl/p5.GeometryPart.js b/src/webgl/p5.GeometryPart.js index 2edd05e478..678b97c020 100644 --- a/src/webgl/p5.GeometryPart.js +++ b/src/webgl/p5.GeometryPart.js @@ -17,8 +17,9 @@ function createPartState() { specularTexture: null, // map_Ks -> p5.Image | null ambientTexture: null, // map_Ka -> p5.Image | null shininessTexture: null, // map_Ns -> p5.Image | null - normalTexture: null, // map_Bump -> p5.Image | null - normalScale: 1 // map_Bump -bm -> bump strength multiplier + normalTexture: null, // map_Bump or norm -> p5.Image | null + normalScale: 1, // -bm -> strength multiplier + normalMapMode: 0 // 0 = normal map (norm), 1 = bump map (map_Bump) }; } diff --git a/src/webgl/shaders/phong.frag b/src/webgl/shaders/phong.frag index b58221ba11..527826f460 100644 --- a/src/webgl/shaders/phong.frag +++ b/src/webgl/shaders/phong.frag @@ -25,6 +25,8 @@ uniform bool uHasShininessTex; uniform sampler2D uNormalSampler; uniform bool uHasNormalMap; uniform float uNormalScale; +uniform int uNormalMapMode; +uniform vec2 uNormalTexelSize; #endif IN vec3 vNormal; @@ -71,8 +73,20 @@ void main(void) { vec3 T = normalize(vTangent.xyz); T = normalize(T - N * dot(N, T)); vec3 B = cross(N, T) * vTangent.w; - vec3 mapN = TEXTURE(uNormalSampler, vTexCoord).rgb * 2.0 - 1.0; - // scale the tangent-space slope so the bump strength can be tuned (-bm) + vec3 mapN; + if (uNormalMapMode == 1) { + // bump map: brightness is height, so the tangent-space normal comes from + // how fast that height changes between neighbouring texels. + float h = TEXTURE(uNormalSampler, vTexCoord).r; + float hu = TEXTURE(uNormalSampler, vTexCoord + vec2(uNormalTexelSize.x, 0.0)).r; + float hv = TEXTURE(uNormalSampler, vTexCoord + vec2(0.0, uNormalTexelSize.y)).r; + // the surface leans away from the direction height increases in + mapN = normalize(vec3(h - hu, h - hv, 1.0)); + } else { + // normal map: rgb already holds the tangent-space normal + mapN = TEXTURE(uNormalSampler, vTexCoord).rgb * 2.0 - 1.0; + } + // scale the tangent-space slope so the strength can be tuned (-bm) mapN.xy *= uNormalScale; N = normalize(mat3(T, B, N) * mapN); } diff --git a/src/webgpu/shaders/material.js b/src/webgpu/shaders/material.js index 9df038a87f..8541967642 100644 --- a/src/webgpu/shaders/material.js +++ b/src/webgpu/shaders/material.js @@ -14,6 +14,8 @@ struct MaterialUniforms { uMetallic: f32, uHasNormalMap: u32, uNormalScale: f32, + uNormalMapMode: u32, + uNormalTexelSize: vec2, } // Group 0: Lighting @@ -396,8 +398,20 @@ ${useTextureMaps ? ` if (material.uHasNormalMap == 1) { var T = normalize(input.vTangent.xyz); T = normalize(T - N * dot(N, T)); let B = cross(N, T) * input.vTangent.w; - var mapN = textureSample(uNormalSampler, uNormalSampler_sampler, input.vTexCoord).rgb * 2.0 - 1.0; - // scale the tangent-space slope so the bump strength can be tuned (-bm) + var mapN: vec3; + if (material.uNormalMapMode == 1u) { + // bump map: brightness is height, so the tangent-space normal comes from + // how fast that height changes between neighbouring texels. + let h = textureSample(uNormalSampler, uNormalSampler_sampler, input.vTexCoord).r; + let hu = textureSample(uNormalSampler, uNormalSampler_sampler, input.vTexCoord + vec2(material.uNormalTexelSize.x, 0.0)).r; + let hv = textureSample(uNormalSampler, uNormalSampler_sampler, input.vTexCoord + vec2(0.0, material.uNormalTexelSize.y)).r; + // the surface leans away from the direction height increases in + mapN = normalize(vec3(h - hu, h - hv, 1.0)); + } else { + // normal map: rgb already holds the tangent-space normal + mapN = textureSample(uNormalSampler, uNormalSampler_sampler, input.vTexCoord).rgb * 2.0 - 1.0; + } + // scale the tangent-space slope so the strength can be tuned (-bm) mapN = vec3(mapN.xy * material.uNormalScale, mapN.z); N = normalize(mat3x3(T, B, N) * mapN); } diff --git a/test/unit/assets/bump_sphere.mtl b/test/unit/assets/bump_sphere.mtl index 24f93d425e..b8933eace4 100644 --- a/test/unit/assets/bump_sphere.mtl +++ b/test/unit/assets/bump_sphere.mtl @@ -2,10 +2,10 @@ newmtl m0 Kd 0.8 0.8 0.8 Ks 0.5 0.5 0.5 Ns 60 -map_Bump spheremap.jpg +map_Bump -bm 8 bumpmap.png newmtl m1 Kd 0.8 0.8 0.8 Ks 0.5 0.5 0.5 Ns 60 -map_Bump spheremap.jpg +map_Bump -bm 8 bumpmap.png diff --git a/test/unit/assets/bumpmap.png b/test/unit/assets/bumpmap.png new file mode 100644 index 0000000000..5e90e9d208 Binary files /dev/null and b/test/unit/assets/bumpmap.png differ diff --git a/test/unit/assets/normal_mapped.mtl b/test/unit/assets/normal_mapped.mtl index 75391a4a70..a57a3f12f4 100644 --- a/test/unit/assets/normal_mapped.mtl +++ b/test/unit/assets/normal_mapped.mtl @@ -1,6 +1,6 @@ newmtl m0 Kd 0.8 0.8 0.8 -map_Bump spheremap.jpg +norm spheremap.jpg newmtl m1 Kd 0.5 0.5 0.5 diff --git a/test/unit/io/loadModel.js b/test/unit/io/loadModel.js index 3d143fa013..af6fe6f48a 100644 --- a/test/unit/io/loadModel.js +++ b/test/unit/io/loadModel.js @@ -130,6 +130,8 @@ suite('loadModel', function () { const normalMapped = model.parts.find(p => p.partState.normalTexture); assert.ok(normalMapped, 'a part has the normal map'); assert.equal(normalMapped.partState.normalTexture, fakeImage); + // norm means read it as a normal map, not as heights + assert.equal(normalMapped.partState.normalMapMode, 0); } finally { delete mockP5Prototype.loadImage; } diff --git a/test/unit/io/parseMtl.js b/test/unit/io/parseMtl.js index 3880bcb7a2..3f867255ab 100644 --- a/test/unit/io/parseMtl.js +++ b/test/unit/io/parseMtl.js @@ -36,14 +36,28 @@ suite('parseMtlData', function () { expect(m.bumpScale).toEqual(0.5); }); - test('a normal map carries its -bm strength onto the part state', function () { + test('norm is read as a normal map', function () { + const materials = parseMtlData('newmtl m\nnorm normal.png'); + expect(materials.m.normalTexturePath).toEqual('normal.png'); + expect(materials.m.bumpTexturePath).toBeUndefined(); + }); + + test('map_Bump lands in bump mode and norm in normal mode', function () { + const img = { width: 1, height: 1 }; + expect(mtlToPartState({ bumpTexture: img }).normalMapMode).toEqual(1); + expect(mtlToPartState({ normalTexture: img }).normalMapMode).toEqual(0); + // both end up in the same slot, so only one can be active + expect(mtlToPartState({ bumpTexture: img }).normalTexture).toBe(img); + }); + + test('a map carries its -bm strength onto the part state', function () { const img = { width: 1, height: 1 }; const state = mtlToPartState({ normalTexture: img, bumpScale: 2.5 }); expect(state.normalTexture).toBe(img); expect(state.normalScale).toEqual(2.5); }); - test('a normal map with no -bm defaults the strength to 1', function () { + test('a map with no -bm defaults the strength to 1', function () { const img = { width: 1, height: 1 }; const state = mtlToPartState({ normalTexture: img }); expect(state.normalScale).toEqual(1); diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index e8b64c40bc..8aa084fd49 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -412,11 +412,11 @@ visualSuite('WebGL', function () { } ); visualTest( - 'a normal-mapped sphere shows surface detail under light', + 'a bump-mapped sphere shows surface detail under light', async function (p5, screenshot) { p5.createCanvas(50, 50, p5.WEBGL); - // bump_sphere.obj is a 2-material sphere with a normal map on both halves, - // so under a light the whole surface shows bump detail (baked tangents) + // bump_sphere.obj is a 2-material sphere using map_Bump on both halves, so + // under a light the whole surface shows bump detail const model = await new Promise(resolve => p5.loadModel('test/unit/assets/bump_sphere.obj', resolve) ); diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index dd7ba1b742..87dbf1a8c8 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -2064,10 +2064,10 @@ visualSuite('WebGPU', function () { visualSuite('3D Materials', function () { visualTest( - 'a normal-mapped sphere shows surface detail under light', + 'a bump-mapped sphere shows surface detail under light', async function (p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); - // bump_sphere.obj carries a normal map on both halves, so the maps shader + // bump_sphere.obj uses map_Bump on both halves, so the maps shader // variant (tangent attribute + normal sampling) is exercised end to end const model = await p5.loadModel('test/unit/assets/bump_sphere.obj'); p5.background(255); diff --git a/test/unit/visual/screenshots/WebGL/3DModel/a bump-mapped sphere shows surface detail under light/000.png b/test/unit/visual/screenshots/WebGL/3DModel/a bump-mapped sphere shows surface detail under light/000.png new file mode 100644 index 0000000000..ff3bfe783f Binary files /dev/null and b/test/unit/visual/screenshots/WebGL/3DModel/a bump-mapped sphere shows surface detail under light/000.png differ diff --git a/test/unit/visual/screenshots/WebGL/3DModel/a normal-mapped sphere shows surface detail under light/metadata.json b/test/unit/visual/screenshots/WebGL/3DModel/a bump-mapped sphere shows surface detail under light/metadata.json similarity index 100% rename from test/unit/visual/screenshots/WebGL/3DModel/a normal-mapped sphere shows surface detail under light/metadata.json rename to test/unit/visual/screenshots/WebGL/3DModel/a bump-mapped sphere shows surface detail under light/metadata.json diff --git a/test/unit/visual/screenshots/WebGL/3DModel/a normal-mapped sphere shows surface detail under light/000.png b/test/unit/visual/screenshots/WebGL/3DModel/a normal-mapped sphere shows surface detail under light/000.png deleted file mode 100644 index 71e09efd5b..0000000000 Binary files a/test/unit/visual/screenshots/WebGL/3DModel/a normal-mapped sphere shows surface detail under light/000.png and /dev/null differ diff --git a/test/unit/visual/screenshots/WebGPU/3D Materials/a bump-mapped sphere shows surface detail under light/000.png b/test/unit/visual/screenshots/WebGPU/3D Materials/a bump-mapped sphere shows surface detail under light/000.png new file mode 100644 index 0000000000..d695ef1a1c Binary files /dev/null and b/test/unit/visual/screenshots/WebGPU/3D Materials/a bump-mapped sphere shows surface detail under light/000.png differ diff --git a/test/unit/visual/screenshots/WebGPU/3D Materials/a normal-mapped sphere shows surface detail under light/metadata.json b/test/unit/visual/screenshots/WebGPU/3D Materials/a bump-mapped sphere shows surface detail under light/metadata.json similarity index 100% rename from test/unit/visual/screenshots/WebGPU/3D Materials/a normal-mapped sphere shows surface detail under light/metadata.json rename to test/unit/visual/screenshots/WebGPU/3D Materials/a bump-mapped sphere shows surface detail under light/metadata.json diff --git a/test/unit/visual/screenshots/WebGPU/3D Materials/a normal-mapped sphere shows surface detail under light/000.png b/test/unit/visual/screenshots/WebGPU/3D Materials/a normal-mapped sphere shows surface detail under light/000.png deleted file mode 100644 index 425c6c732c..0000000000 Binary files a/test/unit/visual/screenshots/WebGPU/3D Materials/a normal-mapped sphere shows surface detail under light/000.png and /dev/null differ diff --git a/test/unit/webgl/p5.GeometryPart.js b/test/unit/webgl/p5.GeometryPart.js index c7fbe074df..1d7a3a1bf1 100644 --- a/test/unit/webgl/p5.GeometryPart.js +++ b/test/unit/webgl/p5.GeometryPart.js @@ -45,7 +45,8 @@ suite('p5.GeometryPart', function () { ambientTexture: null, shininessTexture: null, normalTexture: null, - normalScale: 1 + normalScale: 1, + normalMapMode: 0 }); }); diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index e0616b4adb..6d50708c7b 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -3258,6 +3258,39 @@ void main() { } ); + test('bumpTexture() sets the map in height mode and null clears it', + function () { + myp5.createCanvas(50, 50, myp5.WEBGL); + myp5.bumpTexture(img, 3); + expect(myp5._renderer.states._normalTex).toBe(img); + expect(myp5._renderer.states._normalScale).toEqual(3); + // mode 1 tells the shader to read the map as heights + expect(myp5._renderer.states._normalMapMode).toEqual(1); + myp5.bumpTexture(null); + expect(myp5._renderer.states._normalTex).toBeNull(); + expect(myp5._renderer.states._normalMapMode).toEqual(0); + } + ); + + test('bump and normal maps share one slot, so setting one replaces the other', + function () { + myp5.createCanvas(50, 50, myp5.WEBGL); + const other = { width: 1, height: 1 }; + + myp5.bumpTexture(img); + expect(myp5._renderer.states._normalMapMode).toEqual(1); + + // switching to a normal map keeps a single active map, in mode 0 + myp5.normalTexture(other); + expect(myp5._renderer.states._normalTex).toBe(other); + expect(myp5._renderer.states._normalMapMode).toEqual(0); + + myp5.bumpTexture(img); + expect(myp5._renderer.states._normalTex).toBe(img); + expect(myp5._renderer.states._normalMapMode).toEqual(1); + } + ); + test('specularTexture() sets the map and turns on the specular term', function () { myp5.createCanvas(50, 50, myp5.WEBGL);