From c7b8765daeff6fb54cd49880e2816e17acfc3aaa Mon Sep 17 00:00:00 2001 From: nityam Date: Mon, 17 Aug 2026 01:11:28 +0530 Subject: [PATCH 1/7] read the normal map slot as a height map when bump mode is set --- src/core/p5.Renderer3D.js | 12 ++++++++++++ src/webgl/shaders/phong.frag | 18 ++++++++++++++++-- src/webgpu/shaders/material.js | 18 ++++++++++++++++-- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 57fb3f55e3..2876238ef5 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; @@ -1652,6 +1655,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/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); } From 1670e42cbcacaa2a4bb2756e0d6820b42eb439ec Mon Sep 17 00:00:00 2001 From: nityam Date: Mon, 17 Aug 2026 01:11:28 +0530 Subject: [PATCH 2/7] add bumpTexture() and document how it differs from normalTexture() --- src/webgl/material.js | 97 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 2 deletions(-) diff --git a/src/webgl/material.js b/src/webgl/material.js index 5e6053e69a..b5d84234d3 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -2579,6 +2579,13 @@ function material(p5, fn) { * Call `normalTexture(null)` to turn it off, or scope it between * 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 * the `.mtl` file's `map_Bump`. @@ -2647,6 +2654,84 @@ 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 + * 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. + * 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); + * + * // Light the sphere from the upper left. + * ambientLight(60); + * pointLight(255, 255, 255, -80, -80, 150); + * noStroke(); + * fill(200); + * + * // 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 +4220,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 From 6147199848d66ebae845b4b7ab848131846a35b6 Mon Sep 17 00:00:00 2001 From: nityam Date: Mon, 17 Aug 2026 01:11:28 +0530 Subject: [PATCH 3/7] test bump mode and that the two map kinds share one slot --- test/unit/webgl/p5.RendererGL.js | 33 ++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) 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); From 3d218798f13b45519dc1e9cdce90030b95ea2b3a Mon Sep 17 00:00:00 2001 From: nityam Date: Mon, 17 Aug 2026 02:03:07 +0530 Subject: [PATCH 4/7] match the example style of the other map setters --- src/webgl/material.js | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/webgl/material.js b/src/webgl/material.js index b5d84234d3..5557371ef6 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -2577,9 +2577,9 @@ 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 + * `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 @@ -2587,7 +2587,7 @@ function material(p5, fn) { * 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. @@ -2662,14 +2662,14 @@ function material(p5, fn) { * 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 + * `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(). + * also be scoped between `push()` and + * `pop()`. * - * normalTexture() creates a similar effect + * `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 @@ -2685,6 +2685,8 @@ function material(p5, fn) { * @chainable * * @example + * // Click and drag the mouse to view the scene from different angles. + * * let bumpMap; * * function setup() { @@ -2714,6 +2716,12 @@ function material(p5, fn) { * 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); From 660917d8b101ba936b610f246ba2db9935ec8704 Mon Sep 17 00:00:00 2001 From: nityam Date: Mon, 17 Aug 2026 02:44:25 +0530 Subject: [PATCH 5/7] make the map examples tile so they don't seam on a sphere, and add highlights to the bump one --- src/webgl/material.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/webgl/material.js b/src/webgl/material.js index 5557371ef6..3fa865b64a 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -2610,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; @@ -2642,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); @@ -2697,7 +2702,10 @@ function material(p5, fn) { * 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. + * // 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; @@ -2728,6 +2736,13 @@ function material(p5, fn) { * 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); From 91a76fdf90f72efe4ccd2a0e33b931f84d336019 Mon Sep 17 00:00:00 2001 From: nityam Date: Mon, 17 Aug 2026 03:20:31 +0530 Subject: [PATCH 6/7] load map_Bump as a bump map and add norm for normal maps --- src/core/p5.Renderer3D.js | 1 + src/webgl/loading.js | 36 +++++++++++------- src/webgl/p5.GeometryPart.js | 5 ++- .../000.png | Bin .../metadata.json | 0 .../000.png | Bin .../metadata.json | 0 7 files changed, 26 insertions(+), 16 deletions(-) rename test/unit/visual/screenshots/WebGL/3DModel/{a normal-mapped sphere shows surface detail under light => a bump-mapped sphere shows surface detail under light}/000.png (100%) rename test/unit/visual/screenshots/WebGL/3DModel/{a normal-mapped sphere shows surface detail under light => a bump-mapped sphere shows surface detail under light}/metadata.json (100%) rename test/unit/visual/screenshots/WebGPU/3D Materials/{a normal-mapped sphere shows surface detail under light => a bump-mapped sphere shows surface detail under light}/000.png (100%) rename test/unit/visual/screenshots/WebGPU/3D Materials/{a normal-mapped sphere shows surface detail under light => a bump-mapped sphere shows surface detail under light}/metadata.json (100%) diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 2876238ef5..a84aa56679 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -735,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); } } 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/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/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 bump-mapped sphere shows surface detail under light/000.png similarity index 100% rename from test/unit/visual/screenshots/WebGL/3DModel/a normal-mapped sphere shows surface detail under light/000.png rename to test/unit/visual/screenshots/WebGL/3DModel/a bump-mapped sphere shows surface detail under light/000.png 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/WebGPU/3D Materials/a normal-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 similarity index 100% rename from test/unit/visual/screenshots/WebGPU/3D Materials/a normal-mapped sphere shows surface detail under light/000.png rename to test/unit/visual/screenshots/WebGPU/3D Materials/a bump-mapped sphere shows surface detail under light/000.png 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 From a39d144e802e85ac2b0fc3fef6ebf02cdde132f7 Mon Sep 17 00:00:00 2001 From: nityam Date: Mon, 17 Aug 2026 03:20:31 +0530 Subject: [PATCH 7/7] test both mtl map keywords with a real height map fixture --- test/unit/assets/bump_sphere.mtl | 4 ++-- test/unit/assets/bumpmap.png | Bin 0 -> 322 bytes test/unit/assets/normal_mapped.mtl | 2 +- test/unit/io/loadModel.js | 2 ++ test/unit/io/parseMtl.js | 18 ++++++++++++++++-- test/unit/visual/cases/webgl.js | 6 +++--- test/unit/visual/cases/webgpu.js | 4 ++-- .../000.png | Bin 2458 -> 3130 bytes .../000.png | Bin 2459 -> 3144 bytes test/unit/webgl/p5.GeometryPart.js | 3 ++- 10 files changed, 28 insertions(+), 11 deletions(-) create mode 100644 test/unit/assets/bumpmap.png 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 0000000000000000000000000000000000000000..5e90e9d20870e296bc57e258296dea54ce516a42 GIT binary patch literal 322 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1SD0tpLH@YFmigjIEGZ*dOLlgV2gtQ%lV{} zjP86X4GzLgmNk4W4Q>+{EgzgB}^m@IY`NECwe=Yhx+XUKhTK2AceQR6pEViSQr(CONPw=X` zUV8U??<3C>GbhExUJngjSTX6+ulEc&U6Gmg=bM4zH$^sI^3wzh+iz_AaNprau*RQy zg(H*CeGQ&cZhPH7&~J8(y{qN>;=l86IQ+<0_@k-9e}ef_gN*`N)PedB2gDD{T-?Qa SKd>AaHVmGwelF{r5}E)`4vNA6 literal 0 HcmV?d00001 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 index 71e09efd5b76c654f376a75aa2069787d7eb96db..ff3bfe783fba284b8e9f81a6e921e4f4f12e903a 100644 GIT binary patch delta 3126 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 Q;Q#;t07*qoM6N<$f)%(9mjD0& delta 2449 zcmV;C32yef7@8B1BYz1`Nkl_5{PeD&2=@qg*3pT;Mjd=e?84C#WQ zLx;wqMT=S*qSIwvj>w1+BLcBI?zkh~fB*d$IB;OhnKLIAE?ihe7V&~1LxyCLMcAT6 zi#jc!bnLW{kt0WD?+C-@#9&Dg1eQd56P9}WsI)iE zGtWFzs$#~B8L?%{md1Q7iBY3Q1+>(*MIlqCPHjAukg>T%;>?*d9a>hE6Q-2n(4j+d z%PqH57I!FZRp8K4r)EOduU{WamoCl6ti4e>TYuLg($XL;m8Fz&vE?PfA)Mhsdgq;Y zD(V#MmC>U|H|yI>$n@#cb2Lmyy`^DqH0#+;0nsU?d^)9cRa9L2an7}5UcM#Wo^?2A zQ`ui0nh8N%Lz|EQk$~b^ooWR!qRO_`$Np=ru4y?=_uqeixa&2os}f>aEnmJoAG2iw zQGWqTqiN@VGcOv1c#e5EFIlprYWgZ6PAlHm$yGvh9FtW&TF?BWH4dE030ALOU73w4 zAsaVt%=<$$2-vo5TjO)2s@MM@lT!6Qd+4Eus#sJBDQkJ(zJ0NM`}TZtRki+?GEYDK zbUga#qw(siuSR`+eLV5R6YQ0bLPw{Hg#3GhK!|k-+lLm zrPYxA$NzaymPwv`^2z+1u_qsSVFiE_uf*0xIqVc;YKrIg~~hab+w5jM~6-Miz(7hjCK?z$@zc7MP) z^*q<+&6^kV=g-$Q=FXj4^*umgl@M3-lu|z19lbo@()ak|k9X+m9WDIqJ8t%H~$inU=M*lWEzqWs>cpO30UAewjVQJ0in_(rgiC$tiO^FQdD`Dl7TxWr;0g zSz0B{cJJPuX+)I}ga|+id696Pcz^D>=khWj;)fr82qGSM;DKz7^vWx*e~}Pe1*{RG~_U8iTryh>Xp*a^=drXJ*ZsmG_CkMafcgf@nto*vM=V#v(#w zN+~!=ULSMF7szv*G4$ielz&owt~q>g^YO2D@80E`HVk+%n(ZpPqP5ymwtepN!0yYKUobV@0|vm4Mrr%s*9PkUZck0^48 z5+P4Cf%0rhDfi*sl&&rnmp}c%Gs26U{fRUI1&(Cn5$+-B+Jr+np7k$iCPanJi6An< z=oJ-tgQk@7{R=T=+J6_MlyWkql-G7jSFOU75|bU{HQG0Uv17-EJ?PoOQAI$dAOh5Z z*9*5_r(7Pa;!iUng9i`JOUGIiN@3ivVMFd>BIHv_`2a{Mg?s^VtwB^p;>eLB;Skxo zcW)qBNV6gU+99r91PDET{CNJZW)Hpb#v9S}*Gx!hz+*O}6MqB=o+91!CD5aG0 z@vK}skOpaEa0DP-tnn&A1P~$uMA$d%u3fw0#~*(*Z8NkeL{#h*Yu8b$m&punl9W=8 z8WBhWqS5d}yusv1DMPqeX(GUQEJz#I9uS2B>2_p?0(UiC`0a?q3CBQ!N0nc*Uv}sfL2Ho_?Exgz^g@{U1F+7Gm@D_pu zv52xj2&+uJeh7oUoPjj*hYuePmH_Dk2M&b8V9%aCL4V9Izx)#2yLZR8RkSHYucl%u zi6KJ_i@2ziXb@2e^-%h=AVNT#u?X6?Z(k7Un%|>GkH7(!w$QE+oefkO8QwJ^;s_AJ zi=H{at2_^-UK<`~^#$#a<#|g8HgDb>_QUpuS8ewybK}q+Y7$})1Nxs@Y2ZcA1Vj-g zu2^-5B7e@a$V($mu%|fk2p9O!;af0Ah$P3E(_FQnW+4W%PKjY1y@*>5h)RRyV^0u6 zq9`j~?NM|ej>=+_}3IuJpO_h)HjkleX*XCMj6h(J!HKptVx-ZHYR zlsoMRfQ|~l2|6r9Cm@0#koKUmY>;!}RFX%QIDe7Yzkh$;$i{HpKpqGH<()yNg_OyN zAO;pJSP*^s^a*#N`uh6t#%Bq^Yp=Z)Z@u+aXpgw|B7;1X@j7d}ETnQ`B#}ZAX3w6T zy)PQ_1=?s^*|)3d>nfzHWV+J-h7s~#00030|6{LtKmY&$21!IgR09CiakvJL7{)>X P015yANkvXXu0mjf=?}{7 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 index 425c6c732c43d113d96debb0ca97fbdcb408a1b3..d695ef1a1cffe7c735fc69a3f1f70f2f6286f417 100644 GIT binary patch delta 3140 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 Q01E&B07*qoM6N<$f`P}XoB#j- 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 }); });