From 06c30cff5aab2ab0f143ff425626164ed533a9c8 Mon Sep 17 00:00:00 2001 From: nityam Date: Tue, 11 Aug 2026 10:17:40 +0530 Subject: [PATCH 1/8] render map_Bump normal maps with baked per-vertex tangents --- src/core/p5.Renderer3D.js | 29 ++++++++++--- src/webgl/loading.js | 24 ++++++++++- src/webgl/p5.Geometry.js | 79 ++++++++++++++++++++++++++++++++++++ src/webgl/p5.GeometryPart.js | 5 ++- src/webgl/p5.RendererGL.js | 1 + src/webgl/shaders/phong.frag | 20 ++++++++- src/webgl/shaders/phong.vert | 23 +++++++++++ 7 files changed, 173 insertions(+), 8 deletions(-) diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 96768d08ea..21c261beab 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -151,6 +151,7 @@ export class Renderer3D extends Renderer { this.states._specularTex = null; this.states._ambientTex = null; this.states._shininessTex = null; + this.states._normalTex = null; this.states.textureMode = constants.IMAGE; this.states.textureWrapX = constants.CLAMP; this.states.textureWrapY = constants.CLAMP; @@ -295,7 +296,17 @@ export class Renderer3D extends Renderer { ), new RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, arr => arr.flat() - ) + ), + // surface tangents for normal mapping. [x, y, z, handedness] per vertex. + // only computed for models with a normal map, and only read by the map + // shader variant; defaults to a dummy so the attribute stays valid. + new RenderBuffer( + 4, + 'vertexTangents', + 'tangentBuffer', + 'aTangent', + this + ).default(geometry => geometry.vertices.flatMap(() => [0, 0, 0, 1])) ], stroke: [ new RenderBuffer( @@ -702,6 +713,9 @@ export class Renderer3D extends Renderer { if (partState.shininessTexture) { this.states.setValue('_shininessTex', partState.shininessTexture); } + if (partState.normalTexture) { + this.states.setValue('_normalTex', partState.normalTexture); + } } _drawStrokes(geometry, { count } = {}) { @@ -1483,13 +1497,14 @@ export class Renderer3D extends Renderer { } // true when the current material has any mtl texture map bound (specular, - // ambient or shininess). drives both shader-variant selection and whether - // the map uniforms are worth binding at all. + // ambient, shininess or normal). drives both shader-variant selection and + // whether the map uniforms are worth binding at all. _hasActiveTextureMap() { return !!( this.states._specularTex || this.states._ambientTex || - this.states._shininessTex + this.states._shininessTex || + this.states._normalTex ); } @@ -1601,7 +1616,8 @@ export class Renderer3D extends Renderer { // mtl texture maps only exist in the USE_TEXTURE_MAPS shader variant, which // is only selected when a part actually binds one. so for the common case // (lit scene, no maps) we skip all of these setUniform calls entirely and - // the plain phong shader carries none of the map uniforms. + // the plain phong shader carries none of the map uniforms. bind all four + // pairs together since the variant declares all of them. if (this._hasActiveTextureMap()) { // specular map (map_Ks) fillShader.setUniform('uHasSpecularTex', !!this.states._specularTex); @@ -1615,6 +1631,9 @@ export class Renderer3D extends Renderer { 'uShininessSampler', this.states._shininessTex || empty ); + // normal map (map_Bump): perturbs the surface normal in tangent space + fillShader.setUniform('uHasNormalMap', !!this.states._normalTex); + fillShader.setUniform('uNormalSampler', this.states._normalTex || empty); } fillShader.setUniform( 'uTint', diff --git a/src/webgl/loading.js b/src/webgl/loading.js index 603a75afaa..0013b11446 100755 --- a/src/webgl/loading.js +++ b/src/webgl/loading.js @@ -117,6 +117,7 @@ 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; return state; } @@ -126,7 +127,8 @@ const MATERIAL_TEXTURE_MAPS = [ ['texturePath', 'texture'], // map_Kd (diffuse) ['specularTexturePath', 'specularTexture'], // map_Ks (specular) ['ambientTexturePath', 'ambientTexture'], // map_Ka (ambient) - ['shininessTexturePath', 'shininessTexture'] // map_Ns (shininess) + ['shininessTexturePath', 'shininessTexture'], // map_Ns (shininess) + ['bumpTexturePath', 'normalTexture'] // map_Bump (normal) ]; // load each material's texture maps and hang them on the material so they land @@ -173,6 +175,7 @@ function buildMaterialParts(model, faceMaterials, materials) { const hasUvs = model.uvs.length > 0; const hasNormals = model.vertexNormals.length > 0; + const hasTangents = model.vertexTangents.length > 0; const parts = []; for (const name of names) { @@ -190,6 +193,14 @@ function buildMaterialParts(model, faceMaterials, materials) { part.vertices.push(model.vertices[vi]); if (hasUvs) part.uvs.push(model.uvs[vi]); if (hasNormals) part.vertexNormals.push(model.vertexNormals[vi]); + if (hasTangents) { + part.vertexTangents.push( + model.vertexTangents[vi * 4], + model.vertexTangents[vi * 4 + 1], + model.vertexTangents[vi * 4 + 2], + model.vertexTangents[vi * 4 + 3] + ); + } } return localIndex.get(vi); }); @@ -813,6 +824,17 @@ function loading(p5, fn) { model.vertexColors = []; } + // 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. + const needsTangents = Object.values(materials).some( + m => m && m.normalTexture + ); + if (needsTangents) { + model.computeTangents(); + } + // bucket faces into per-material parts (aggregate arrays above stay as-is) buildMaterialParts(model, faceMaterials, materials); diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index cd842eaa67..783381b682 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -38,6 +38,11 @@ class Geometry { this.vertexNormals = []; + // per-vertex surface tangents for normal mapping, stored flat as + // [x, y, z, w] where w is the bitangent handedness. computeTangents() fills + // this; empty until a normal-mapped model needs it. + this.vertexTangents = []; + this.faces = []; this.uvs = []; @@ -250,6 +255,7 @@ class Geometry { this.vertexStrokeColors.length = 0; this.lineVertexColors.clear(); this.vertexNormals.length = 0; + this.vertexTangents.length = 0; this.uvs.length = 0; for (const propName in this.userVertexProperties) { @@ -1256,6 +1262,79 @@ class Geometry { return this; } + /** + * computes a per-vertex surface tangent from the uvs, needed for normal + * (bump) mapping. the tangent points along the +u texture direction; its w + * component stores the bitangent handedness so the shader can rebuild the + * bitangent as cross(normal, tangent) * w. results are stored flat as + * [x, y, z, w] per vertex on this.vertexTangents. needs uvs and vertex + * normals, so run computeNormals() first if the model has none. + * @private + * @chainable + */ + computeTangents() { + const vertices = this.vertices; + const faces = this.faces; + const uvs = this.uvs.flat(); + const normals = this.vertexNormals; + + // nothing to build a tangent basis from without uvs and normals + if (uvs.length === 0 || normals.length === 0) { + this.vertexTangents = []; + return this; + } + + // accumulate the +u direction (tan) and +v direction (bitan) per vertex + const tan = []; + const bitan = []; + for (let i = 0; i < vertices.length; i++) { + tan.push(new Vector(0, 0, 0)); + bitan.push(new Vector(0, 0, 0)); + } + const uvAt = i => ({ x: uvs[i * 2] || 0, y: uvs[i * 2 + 1] || 0 }); + + for (const face of faces) { + const [i0, i1, i2] = face; + const e1 = Vector.sub(vertices[i1], vertices[i0]); + const e2 = Vector.sub(vertices[i2], vertices[i0]); + const w0 = uvAt(i0); + const w1 = uvAt(i1); + const w2 = uvAt(i2); + const du1 = w1.x - w0.x; + const dv1 = w1.y - w0.y; + const du2 = w2.x - w0.x; + const dv2 = w2.y - w0.y; + + const denom = du1 * dv2 - du2 * dv1; + const r = denom === 0 ? 0 : 1 / denom; + const sdir = Vector.sub(Vector.mult(e1, dv2), Vector.mult(e2, dv1)).mult(r); + const tdir = Vector.sub(Vector.mult(e2, du1), Vector.mult(e1, du2)).mult(r); + + for (const idx of face) { + tan[idx].add(sdir); + bitan[idx].add(tdir); + } + } + + // orthonormalise each tangent against its normal and record handedness + const tangents = []; + for (let i = 0; i < vertices.length; i++) { + const n = normals[i] || new Vector(0, 0, 1); + let t = Vector.sub(tan[i], Vector.mult(n, n.dot(tan[i]))); + if (t.magSq() === 0) { + // degenerate uvs: pick any direction perpendicular to the normal + const seed = Math.abs(n.x) < 0.9 ? new Vector(1, 0, 0) : new Vector(0, 1, 0); + t = Vector.sub(seed, Vector.mult(n, n.dot(seed))); + } + t.normalize(); + const handedness = Vector.cross(n, t).dot(bitan[i]) < 0 ? -1 : 1; + tangents.push(t.x, t.y, t.z, handedness); + } + + this.vertexTangents = tangents; + return this; + } + /** * Averages the vertex normals. Used in curved * surfaces diff --git a/src/webgl/p5.GeometryPart.js b/src/webgl/p5.GeometryPart.js index 5873ac8959..b10209cbdf 100644 --- a/src/webgl/p5.GeometryPart.js +++ b/src/webgl/p5.GeometryPart.js @@ -16,7 +16,8 @@ function createPartState() { texture: null, // map_Kd -> p5.Image | null specularTexture: null, // map_Ks -> p5.Image | null ambientTexture: null, // map_Ka -> p5.Image | null - shininessTexture: null // map_Ns -> p5.Image | null + shininessTexture: null, // map_Ns -> p5.Image | null + normalTexture: null // map_Bump -> p5.Image | null }; } @@ -30,6 +31,8 @@ class GeometryPart { this.vertices = []; this.vertexNormals = []; + // surface tangents for normal mapping, flat [x, y, z, w] per vertex + this.vertexTangents = []; this.faces = []; this.uvs = []; this.vertexColors = []; diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index cffb0f5f83..e344a4a467 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -744,6 +744,7 @@ class RendererGL extends Renderer3D { this[cacheKey] = new Shader( this, this._webGL2CompatibilityPrefix('vert', 'highp') + + mapsDefine + defaultShaders.phongVert, this._webGL2CompatibilityPrefix('frag', 'highp') + mapsDefine + diff --git a/src/webgl/shaders/phong.frag b/src/webgl/shaders/phong.frag index 894f13cc78..9bb6e8b0b8 100644 --- a/src/webgl/shaders/phong.frag +++ b/src/webgl/shaders/phong.frag @@ -22,12 +22,17 @@ uniform sampler2D uAmbientSampler; uniform bool uHasAmbientTex; uniform sampler2D uShininessSampler; uniform bool uHasShininessTex; +uniform sampler2D uNormalSampler; +uniform bool uHasNormalMap; #endif IN vec3 vNormal; IN vec2 vTexCoord; IN vec3 vViewPosition; IN vec4 vColor; +#ifdef USE_TEXTURE_MAPS +IN vec4 vTangent; +#endif struct ColorComponents { vec3 baseColor; @@ -56,7 +61,20 @@ void main(void) { HOOK_beforeFragment(); Inputs inputs; - inputs.normal = normalize(vNormal); + vec3 N = normalize(vNormal); +#ifdef USE_TEXTURE_MAPS + if (uHasNormalMap) { + // rebuild the tangent basis (TBN) from the smooth per-vertex tangent and + // perturb the normal by the map. gram-schmidt keeps T perpendicular to the + // interpolated normal so the frame stays smooth across triangles (no facets). + 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; + N = normalize(mat3(T, B, N) * mapN); + } +#endif + inputs.normal = N; inputs.texCoord = vTexCoord; inputs.ambientLight = uAmbientColor; inputs.color = isTexture diff --git a/src/webgl/shaders/phong.vert b/src/webgl/shaders/phong.vert index 49a10933fc..28f549c7b8 100644 --- a/src/webgl/shaders/phong.vert +++ b/src/webgl/shaders/phong.vert @@ -6,6 +6,9 @@ IN vec3 aPosition; IN vec3 aNormal; IN vec2 aTexCoord; IN vec4 aVertexColor; +#ifdef USE_TEXTURE_MAPS +IN vec4 aTangent; +#endif #ifdef AUGMENTED_HOOK_getWorldInputs uniform mat4 uModelMatrix; @@ -26,6 +29,9 @@ OUT vec2 vTexCoord; OUT vec3 vViewPosition; OUT vec3 vAmbientColor; OUT vec4 vColor; +#ifdef USE_TEXTURE_MAPS +OUT vec4 vTangent; +#endif struct Vertex { vec3 position; @@ -42,6 +48,11 @@ void main(void) { inputs.normal = aNormal; inputs.texCoord = aTexCoord; inputs.color = (uUseVertexColor && aVertexColor.x >= 0.0) ? aVertexColor : uMaterialColor; +#ifdef USE_TEXTURE_MAPS + // transform the surface tangent alongside the normal so it ends up in the + // same space; handedness (w) is passed through for rebuilding the bitangent. + vec3 tangent = aTangent.xyz; +#endif #ifdef AUGMENTED_HOOK_getObjectInputs inputs = HOOK_getObjectInputs(inputs); #endif @@ -49,6 +60,9 @@ void main(void) { #ifdef AUGMENTED_HOOK_getWorldInputs inputs.position = (uModelMatrix * vec4(inputs.position, 1.)).xyz; inputs.normal = uModelNormalMatrix * inputs.normal; +#ifdef USE_TEXTURE_MAPS + tangent = uModelNormalMatrix * tangent; +#endif inputs = HOOK_getWorldInputs(inputs); #endif @@ -56,10 +70,16 @@ void main(void) { // Already multiplied by the model matrix, just apply view inputs.position = (uViewMatrix * vec4(inputs.position, 1.)).xyz; inputs.normal = uCameraNormalMatrix * inputs.normal; +#ifdef USE_TEXTURE_MAPS + tangent = uCameraNormalMatrix * tangent; +#endif #else // Apply both at once inputs.position = (uModelViewMatrix * vec4(inputs.position, 1.)).xyz; inputs.normal = uNormalMatrix * inputs.normal; +#ifdef USE_TEXTURE_MAPS + tangent = uNormalMatrix * tangent; +#endif #endif #ifdef AUGMENTED_HOOK_getCameraInputs inputs = HOOK_getCameraInputs(inputs); @@ -70,6 +90,9 @@ void main(void) { vTexCoord = inputs.texCoord; vNormal = inputs.normal; vColor = inputs.color; +#ifdef USE_TEXTURE_MAPS + vTangent = vec4(tangent, aTangent.w); +#endif gl_Position = uProjectionMatrix * vec4(inputs.position, 1.); HOOK_afterVertex(); From 8887a80528bbad8673b7a2bc3f36b77f0d3cfa9f Mon Sep 17 00:00:00 2001 From: nityam Date: Tue, 11 Aug 2026 10:17:40 +0530 Subject: [PATCH 2/8] port normal mapping to webgpu with a per-flag shader variant --- src/webgpu/p5.RendererWebGPU.js | 18 ++++++++++----- src/webgpu/shaders/material.js | 40 ++++++++++++++++++++++----------- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index acf096bd96..072ae2f34c 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -2663,12 +2663,18 @@ function rendererWebGPU(p5, fn) { this._postSubmitCallbacks.push(() => gpuTexture.destroy()); } - _getLightShader() { - if (!this._defaultLightShader) { - this._defaultLightShader = new Shader( + // useTextureMaps selects a shader variant that carries the tangent attribute + // and normal-map sampling. plain lit materials get the variant without any of + // it (webgpu has no #ifdef, so the shader source is built per-flag). + _getLightShader(useTextureMaps = false) { + const cacheKey = useTextureMaps + ? '_defaultLightShaderWithMaps' + : '_defaultLightShader'; + if (!this[cacheKey]) { + this[cacheKey] = new Shader( this, - materialVertexShader, - materialFragmentShader, + materialVertexShader({ useTextureMaps }), + materialFragmentShader({ useTextureMaps }), { vertex: { 'void beforeVertex': '() {}', @@ -2695,7 +2701,7 @@ function rendererWebGPU(p5, fn) { } ); } - return this._defaultLightShader; + return this[cacheKey]; } _getColorShader() { diff --git a/src/webgpu/shaders/material.js b/src/webgpu/shaders/material.js index 751ec0ad3a..4949627f9a 100644 --- a/src/webgpu/shaders/material.js +++ b/src/webgpu/shaders/material.js @@ -12,6 +12,7 @@ struct MaterialUniforms { uSpecular: u32, uShininess: f32, uMetallic: f32, + uHasNormalMap: u32, } // Group 0: Lighting @@ -59,12 +60,15 @@ struct CameraUniforms { } `; -export const materialVertexShader = ` +// webgpu has no preprocessor, so the shaders are functions of a flag. with a +// normal map we compile a variant that carries the tangent attribute + varying; +// without one nothing extra is declared, matching the webgl #define variant. +export const materialVertexShader = ({ useTextureMaps = false } = {}) => ` struct VertexInput { @location(0) aPosition: vec3, @location(1) aNormal: vec3, @location(2) aTexCoord: vec2, - @location(3) aVertexColor: vec4, + @location(3) aVertexColor: vec4,${useTextureMaps ? '\n @location(4) aTangent: vec4,' : ''} }; struct VertexOutput { @@ -72,7 +76,7 @@ struct VertexOutput { @location(0) vNormal: vec3, @location(1) vTexCoord: vec2, @location(2) vViewPosition: vec3, - @location(4) vColor: vec4, + @location(4) vColor: vec4,${useTextureMaps ? '\n @location(5) vTangent: vec4,' : ''} }; ${uniforms} @@ -100,7 +104,7 @@ fn main(input: VertexInput) -> VertexOutput { input.aTexCoord, select(model.uMaterialColor, input.aVertexColor, useVertexColor) ); - +${useTextureMaps ? ' var tangent = input.aTangent.xyz;\n' : ''} // @p5 ifdef Vertex getObjectInputs inputs = HOOK_getObjectInputs(inputs); // @p5 endif @@ -108,19 +112,19 @@ fn main(input: VertexInput) -> VertexOutput { // @p5 ifdef Vertex getWorldInputs inputs.position = (model.uModelMatrix * vec4(inputs.position, 1.0)).xyz; inputs.normal = model.uModelNormalMatrix * inputs.normal; - inputs = HOOK_getWorldInputs(inputs); +${useTextureMaps ? ' tangent = model.uModelNormalMatrix * tangent;\n' : ''} inputs = HOOK_getWorldInputs(inputs); // @p5 endif // @p5 ifdef Vertex getWorldInputs // Already multiplied by the model matrix, just apply view inputs.position = (camera.uViewMatrix * vec4(inputs.position, 1.0)).xyz; inputs.normal = camera.uCameraNormalMatrix * inputs.normal; -// @p5 endif +${useTextureMaps ? ' tangent = camera.uCameraNormalMatrix * tangent;\n' : ''}// @p5 endif // @p5 ifndef Vertex getWorldInputs // Apply both at once inputs.position = (model.uModelViewMatrix * vec4(inputs.position, 1.0)).xyz; inputs.normal = model.uNormalMatrix * inputs.normal; -// @p5 endif +${useTextureMaps ? ' tangent = model.uNormalMatrix * tangent;\n' : ''}// @p5 endif // @p5 ifdef Vertex getCameraInputs inputs = HOOK_getCameraInputs(inputs); @@ -130,7 +134,7 @@ fn main(input: VertexInput) -> VertexOutput { output.vTexCoord = inputs.texCoord; output.vNormal = normalize(inputs.normal); output.vColor = inputs.color; - +${useTextureMaps ? ' output.vTangent = vec4(tangent, input.aTangent.w);\n' : ''} output.Position = camera.uProjectionMatrix * vec4(inputs.position, 1.0); HOOK_afterVertex(); @@ -138,12 +142,12 @@ fn main(input: VertexInput) -> VertexOutput { } `; -export const materialFragmentShader = ` +export const materialFragmentShader = ({ useTextureMaps = false } = {}) => ` struct FragmentInput { @location(0) vNormal: vec3, @location(1) vTexCoord: vec2, @location(2) vViewPosition: vec3, - @location(4) vColor: vec4, + @location(4) vColor: vec4,${useTextureMaps ? '\n @location(5) vTangent: vec4,' : ''} }; ${uniforms} @@ -155,7 +159,7 @@ ${uniforms} @group(0) @binding(5) var environmentMapDiffused_sampler: sampler; @group(0) @binding(6) var environmentMapSpecular: texture_2d; @group(0) @binding(7) var environmentMapSpecular_sampler: sampler; -@group(1) @binding(0) var model: ModelUniforms; +${useTextureMaps ? '@group(0) @binding(8) var uNormalSampler: texture_2d;\n@group(0) @binding(9) var uNormalSampler_sampler: sampler;\n' : ''}@group(1) @binding(0) var model: ModelUniforms; @group(2) @binding(0) var camera: CameraUniforms; struct ColorComponents { @@ -384,8 +388,18 @@ fn main(input: FragmentInput) -> @location(0) vec4 { textureSample(uSampler, uSampler_sampler, input.vTexCoord) * (material.uTint/255.0), material.isTexture == 1 ); // TODO: check isTexture and apply tint - var inputs = Inputs( - normalize(input.vNormal), + var N = normalize(input.vNormal); +${useTextureMaps ? ` if (material.uHasNormalMap == 1) { + // rebuild the tangent basis from the smooth per-vertex tangent (gram-schmidt + // keeps it perpendicular to the interpolated normal) and perturb by the map. + var T = normalize(input.vTangent.xyz); + T = normalize(T - N * dot(N, T)); + let B = cross(N, T) * input.vTangent.w; + let mapN = textureSample(uNormalSampler, uNormalSampler_sampler, input.vTexCoord).rgb * 2.0 - 1.0; + N = normalize(mat3x3(T, B, N) * mapN); + } +` : ''} var inputs = Inputs( + N, input.vTexCoord, material.uAmbientColor, select(color.rgb, material.uAmbientMatColor.rgb, material.uHasSetAmbient == 1), From 92559703906766dc10eb12fa43933b8d13adb471 Mon Sep 17 00:00:00 2001 From: nityam Date: Tue, 11 Aug 2026 10:17:40 +0530 Subject: [PATCH 3/8] test normal mapping across parse, load and lit renders in both renderers --- test/unit/assets/bump_sphere.mtl | 11 + test/unit/assets/bump_sphere.obj | 1382 +++++++++++++++++ test/unit/assets/normal_mapped.mtl | 6 + test/unit/assets/normal_mapped.obj | 17 + test/unit/io/loadModel.js | 18 + test/unit/io/parseMtl.js | 6 + test/unit/visual/cases/webgl.js | 17 + test/unit/visual/cases/webgpu.js | 18 + .../000.png | Bin 0 -> 2458 bytes .../metadata.json | 3 + .../000.png | Bin 0 -> 2459 bytes .../metadata.json | 3 + test/unit/webgl/p5.Geometry.js | 50 + test/unit/webgl/p5.GeometryPart.js | 3 +- 14 files changed, 1533 insertions(+), 1 deletion(-) create mode 100644 test/unit/assets/bump_sphere.mtl create mode 100644 test/unit/assets/bump_sphere.obj create mode 100644 test/unit/assets/normal_mapped.mtl create mode 100644 test/unit/assets/normal_mapped.obj create mode 100644 test/unit/visual/screenshots/WebGL/3DModel/a normal-mapped sphere shows surface detail under light/000.png create mode 100644 test/unit/visual/screenshots/WebGL/3DModel/a normal-mapped sphere shows surface detail under light/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/3D Materials/a normal-mapped sphere shows surface detail under light/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/3D Materials/a normal-mapped sphere shows surface detail under light/metadata.json diff --git a/test/unit/assets/bump_sphere.mtl b/test/unit/assets/bump_sphere.mtl new file mode 100644 index 0000000000..24f93d425e --- /dev/null +++ b/test/unit/assets/bump_sphere.mtl @@ -0,0 +1,11 @@ +newmtl m0 +Kd 0.8 0.8 0.8 +Ks 0.5 0.5 0.5 +Ns 60 +map_Bump spheremap.jpg + +newmtl m1 +Kd 0.8 0.8 0.8 +Ks 0.5 0.5 0.5 +Ns 60 +map_Bump spheremap.jpg diff --git a/test/unit/assets/bump_sphere.obj b/test/unit/assets/bump_sphere.obj new file mode 100644 index 0000000000..71701f7fd6 --- /dev/null +++ b/test/unit/assets/bump_sphere.obj @@ -0,0 +1,1382 @@ +mtllib bump_sphere.mtl +v 0.0000 1.0000 0.0000 +v 0.0000 1.0000 0.0000 +v 0.0000 1.0000 0.0000 +v 0.0000 1.0000 0.0000 +v 0.0000 1.0000 0.0000 +v 0.0000 1.0000 0.0000 +v 0.0000 1.0000 0.0000 +v 0.0000 1.0000 0.0000 +v 0.0000 1.0000 0.0000 +v 0.0000 1.0000 0.0000 +v 0.0000 1.0000 0.0000 +v 0.0000 1.0000 0.0000 +v 0.0000 1.0000 0.0000 +v 0.0000 1.0000 0.0000 +v 0.0000 1.0000 0.0000 +v 0.0000 1.0000 0.0000 +v 0.0000 1.0000 0.0000 +v 0.1951 0.9808 0.0000 +v 0.1802 0.9808 0.0747 +v 0.1379 0.9808 0.1379 +v 0.0747 0.9808 0.1802 +v 0.0000 0.9808 0.1951 +v -0.0747 0.9808 0.1802 +v -0.1379 0.9808 0.1379 +v -0.1802 0.9808 0.0747 +v -0.1951 0.9808 0.0000 +v -0.1802 0.9808 -0.0747 +v -0.1379 0.9808 -0.1379 +v -0.0747 0.9808 -0.1802 +v -0.0000 0.9808 -0.1951 +v 0.0747 0.9808 -0.1802 +v 0.1379 0.9808 -0.1379 +v 0.1802 0.9808 -0.0747 +v 0.1951 0.9808 -0.0000 +v 0.3827 0.9239 0.0000 +v 0.3536 0.9239 0.1464 +v 0.2706 0.9239 0.2706 +v 0.1464 0.9239 0.3536 +v 0.0000 0.9239 0.3827 +v -0.1464 0.9239 0.3536 +v -0.2706 0.9239 0.2706 +v -0.3536 0.9239 0.1464 +v -0.3827 0.9239 0.0000 +v -0.3536 0.9239 -0.1464 +v -0.2706 0.9239 -0.2706 +v -0.1464 0.9239 -0.3536 +v -0.0000 0.9239 -0.3827 +v 0.1464 0.9239 -0.3536 +v 0.2706 0.9239 -0.2706 +v 0.3536 0.9239 -0.1464 +v 0.3827 0.9239 -0.0000 +v 0.5556 0.8315 0.0000 +v 0.5133 0.8315 0.2126 +v 0.3928 0.8315 0.3928 +v 0.2126 0.8315 0.5133 +v 0.0000 0.8315 0.5556 +v -0.2126 0.8315 0.5133 +v -0.3928 0.8315 0.3928 +v -0.5133 0.8315 0.2126 +v -0.5556 0.8315 0.0000 +v -0.5133 0.8315 -0.2126 +v -0.3928 0.8315 -0.3928 +v -0.2126 0.8315 -0.5133 +v -0.0000 0.8315 -0.5556 +v 0.2126 0.8315 -0.5133 +v 0.3928 0.8315 -0.3928 +v 0.5133 0.8315 -0.2126 +v 0.5556 0.8315 -0.0000 +v 0.7071 0.7071 0.0000 +v 0.6533 0.7071 0.2706 +v 0.5000 0.7071 0.5000 +v 0.2706 0.7071 0.6533 +v 0.0000 0.7071 0.7071 +v -0.2706 0.7071 0.6533 +v -0.5000 0.7071 0.5000 +v -0.6533 0.7071 0.2706 +v -0.7071 0.7071 0.0000 +v -0.6533 0.7071 -0.2706 +v -0.5000 0.7071 -0.5000 +v -0.2706 0.7071 -0.6533 +v -0.0000 0.7071 -0.7071 +v 0.2706 0.7071 -0.6533 +v 0.5000 0.7071 -0.5000 +v 0.6533 0.7071 -0.2706 +v 0.7071 0.7071 -0.0000 +v 0.8315 0.5556 0.0000 +v 0.7682 0.5556 0.3182 +v 0.5879 0.5556 0.5879 +v 0.3182 0.5556 0.7682 +v 0.0000 0.5556 0.8315 +v -0.3182 0.5556 0.7682 +v -0.5879 0.5556 0.5879 +v -0.7682 0.5556 0.3182 +v -0.8315 0.5556 0.0000 +v -0.7682 0.5556 -0.3182 +v -0.5879 0.5556 -0.5879 +v -0.3182 0.5556 -0.7682 +v -0.0000 0.5556 -0.8315 +v 0.3182 0.5556 -0.7682 +v 0.5879 0.5556 -0.5879 +v 0.7682 0.5556 -0.3182 +v 0.8315 0.5556 -0.0000 +v 0.9239 0.3827 0.0000 +v 0.8536 0.3827 0.3536 +v 0.6533 0.3827 0.6533 +v 0.3536 0.3827 0.8536 +v 0.0000 0.3827 0.9239 +v -0.3536 0.3827 0.8536 +v -0.6533 0.3827 0.6533 +v -0.8536 0.3827 0.3536 +v -0.9239 0.3827 0.0000 +v -0.8536 0.3827 -0.3536 +v -0.6533 0.3827 -0.6533 +v -0.3536 0.3827 -0.8536 +v -0.0000 0.3827 -0.9239 +v 0.3536 0.3827 -0.8536 +v 0.6533 0.3827 -0.6533 +v 0.8536 0.3827 -0.3536 +v 0.9239 0.3827 -0.0000 +v 0.9808 0.1951 0.0000 +v 0.9061 0.1951 0.3753 +v 0.6935 0.1951 0.6935 +v 0.3753 0.1951 0.9061 +v 0.0000 0.1951 0.9808 +v -0.3753 0.1951 0.9061 +v -0.6935 0.1951 0.6935 +v -0.9061 0.1951 0.3753 +v -0.9808 0.1951 0.0000 +v -0.9061 0.1951 -0.3753 +v -0.6935 0.1951 -0.6935 +v -0.3753 0.1951 -0.9061 +v -0.0000 0.1951 -0.9808 +v 0.3753 0.1951 -0.9061 +v 0.6935 0.1951 -0.6935 +v 0.9061 0.1951 -0.3753 +v 0.9808 0.1951 -0.0000 +v 1.0000 0.0000 0.0000 +v 0.9239 0.0000 0.3827 +v 0.7071 0.0000 0.7071 +v 0.3827 0.0000 0.9239 +v 0.0000 0.0000 1.0000 +v -0.3827 0.0000 0.9239 +v -0.7071 0.0000 0.7071 +v -0.9239 0.0000 0.3827 +v -1.0000 0.0000 0.0000 +v -0.9239 0.0000 -0.3827 +v -0.7071 0.0000 -0.7071 +v -0.3827 0.0000 -0.9239 +v -0.0000 0.0000 -1.0000 +v 0.3827 0.0000 -0.9239 +v 0.7071 0.0000 -0.7071 +v 0.9239 0.0000 -0.3827 +v 1.0000 0.0000 -0.0000 +v 0.9808 -0.1951 0.0000 +v 0.9061 -0.1951 0.3753 +v 0.6935 -0.1951 0.6935 +v 0.3753 -0.1951 0.9061 +v 0.0000 -0.1951 0.9808 +v -0.3753 -0.1951 0.9061 +v -0.6935 -0.1951 0.6935 +v -0.9061 -0.1951 0.3753 +v -0.9808 -0.1951 0.0000 +v -0.9061 -0.1951 -0.3753 +v -0.6935 -0.1951 -0.6935 +v -0.3753 -0.1951 -0.9061 +v -0.0000 -0.1951 -0.9808 +v 0.3753 -0.1951 -0.9061 +v 0.6935 -0.1951 -0.6935 +v 0.9061 -0.1951 -0.3753 +v 0.9808 -0.1951 -0.0000 +v 0.9239 -0.3827 0.0000 +v 0.8536 -0.3827 0.3536 +v 0.6533 -0.3827 0.6533 +v 0.3536 -0.3827 0.8536 +v 0.0000 -0.3827 0.9239 +v -0.3536 -0.3827 0.8536 +v -0.6533 -0.3827 0.6533 +v -0.8536 -0.3827 0.3536 +v -0.9239 -0.3827 0.0000 +v -0.8536 -0.3827 -0.3536 +v -0.6533 -0.3827 -0.6533 +v -0.3536 -0.3827 -0.8536 +v -0.0000 -0.3827 -0.9239 +v 0.3536 -0.3827 -0.8536 +v 0.6533 -0.3827 -0.6533 +v 0.8536 -0.3827 -0.3536 +v 0.9239 -0.3827 -0.0000 +v 0.8315 -0.5556 0.0000 +v 0.7682 -0.5556 0.3182 +v 0.5879 -0.5556 0.5879 +v 0.3182 -0.5556 0.7682 +v 0.0000 -0.5556 0.8315 +v -0.3182 -0.5556 0.7682 +v -0.5879 -0.5556 0.5879 +v -0.7682 -0.5556 0.3182 +v -0.8315 -0.5556 0.0000 +v -0.7682 -0.5556 -0.3182 +v -0.5879 -0.5556 -0.5879 +v -0.3182 -0.5556 -0.7682 +v -0.0000 -0.5556 -0.8315 +v 0.3182 -0.5556 -0.7682 +v 0.5879 -0.5556 -0.5879 +v 0.7682 -0.5556 -0.3182 +v 0.8315 -0.5556 -0.0000 +v 0.7071 -0.7071 0.0000 +v 0.6533 -0.7071 0.2706 +v 0.5000 -0.7071 0.5000 +v 0.2706 -0.7071 0.6533 +v 0.0000 -0.7071 0.7071 +v -0.2706 -0.7071 0.6533 +v -0.5000 -0.7071 0.5000 +v -0.6533 -0.7071 0.2706 +v -0.7071 -0.7071 0.0000 +v -0.6533 -0.7071 -0.2706 +v -0.5000 -0.7071 -0.5000 +v -0.2706 -0.7071 -0.6533 +v -0.0000 -0.7071 -0.7071 +v 0.2706 -0.7071 -0.6533 +v 0.5000 -0.7071 -0.5000 +v 0.6533 -0.7071 -0.2706 +v 0.7071 -0.7071 -0.0000 +v 0.5556 -0.8315 0.0000 +v 0.5133 -0.8315 0.2126 +v 0.3928 -0.8315 0.3928 +v 0.2126 -0.8315 0.5133 +v 0.0000 -0.8315 0.5556 +v -0.2126 -0.8315 0.5133 +v -0.3928 -0.8315 0.3928 +v -0.5133 -0.8315 0.2126 +v -0.5556 -0.8315 0.0000 +v -0.5133 -0.8315 -0.2126 +v -0.3928 -0.8315 -0.3928 +v -0.2126 -0.8315 -0.5133 +v -0.0000 -0.8315 -0.5556 +v 0.2126 -0.8315 -0.5133 +v 0.3928 -0.8315 -0.3928 +v 0.5133 -0.8315 -0.2126 +v 0.5556 -0.8315 -0.0000 +v 0.3827 -0.9239 0.0000 +v 0.3536 -0.9239 0.1464 +v 0.2706 -0.9239 0.2706 +v 0.1464 -0.9239 0.3536 +v 0.0000 -0.9239 0.3827 +v -0.1464 -0.9239 0.3536 +v -0.2706 -0.9239 0.2706 +v -0.3536 -0.9239 0.1464 +v -0.3827 -0.9239 0.0000 +v -0.3536 -0.9239 -0.1464 +v -0.2706 -0.9239 -0.2706 +v -0.1464 -0.9239 -0.3536 +v -0.0000 -0.9239 -0.3827 +v 0.1464 -0.9239 -0.3536 +v 0.2706 -0.9239 -0.2706 +v 0.3536 -0.9239 -0.1464 +v 0.3827 -0.9239 -0.0000 +v 0.1951 -0.9808 0.0000 +v 0.1802 -0.9808 0.0747 +v 0.1379 -0.9808 0.1379 +v 0.0747 -0.9808 0.1802 +v 0.0000 -0.9808 0.1951 +v -0.0747 -0.9808 0.1802 +v -0.1379 -0.9808 0.1379 +v -0.1802 -0.9808 0.0747 +v -0.1951 -0.9808 0.0000 +v -0.1802 -0.9808 -0.0747 +v -0.1379 -0.9808 -0.1379 +v -0.0747 -0.9808 -0.1802 +v -0.0000 -0.9808 -0.1951 +v 0.0747 -0.9808 -0.1802 +v 0.1379 -0.9808 -0.1379 +v 0.1802 -0.9808 -0.0747 +v 0.1951 -0.9808 -0.0000 +v 0.0000 -1.0000 0.0000 +v 0.0000 -1.0000 0.0000 +v 0.0000 -1.0000 0.0000 +v 0.0000 -1.0000 0.0000 +v 0.0000 -1.0000 0.0000 +v -0.0000 -1.0000 0.0000 +v -0.0000 -1.0000 0.0000 +v -0.0000 -1.0000 0.0000 +v -0.0000 -1.0000 0.0000 +v -0.0000 -1.0000 -0.0000 +v -0.0000 -1.0000 -0.0000 +v -0.0000 -1.0000 -0.0000 +v -0.0000 -1.0000 -0.0000 +v 0.0000 -1.0000 -0.0000 +v 0.0000 -1.0000 -0.0000 +v 0.0000 -1.0000 -0.0000 +v 0.0000 -1.0000 -0.0000 +vt 0.0000 1.0000 +vt 0.0625 1.0000 +vt 0.1250 1.0000 +vt 0.1875 1.0000 +vt 0.2500 1.0000 +vt 0.3125 1.0000 +vt 0.3750 1.0000 +vt 0.4375 1.0000 +vt 0.5000 1.0000 +vt 0.5625 1.0000 +vt 0.6250 1.0000 +vt 0.6875 1.0000 +vt 0.7500 1.0000 +vt 0.8125 1.0000 +vt 0.8750 1.0000 +vt 0.9375 1.0000 +vt 1.0000 1.0000 +vt 0.0000 0.9375 +vt 0.0625 0.9375 +vt 0.1250 0.9375 +vt 0.1875 0.9375 +vt 0.2500 0.9375 +vt 0.3125 0.9375 +vt 0.3750 0.9375 +vt 0.4375 0.9375 +vt 0.5000 0.9375 +vt 0.5625 0.9375 +vt 0.6250 0.9375 +vt 0.6875 0.9375 +vt 0.7500 0.9375 +vt 0.8125 0.9375 +vt 0.8750 0.9375 +vt 0.9375 0.9375 +vt 1.0000 0.9375 +vt 0.0000 0.8750 +vt 0.0625 0.8750 +vt 0.1250 0.8750 +vt 0.1875 0.8750 +vt 0.2500 0.8750 +vt 0.3125 0.8750 +vt 0.3750 0.8750 +vt 0.4375 0.8750 +vt 0.5000 0.8750 +vt 0.5625 0.8750 +vt 0.6250 0.8750 +vt 0.6875 0.8750 +vt 0.7500 0.8750 +vt 0.8125 0.8750 +vt 0.8750 0.8750 +vt 0.9375 0.8750 +vt 1.0000 0.8750 +vt 0.0000 0.8125 +vt 0.0625 0.8125 +vt 0.1250 0.8125 +vt 0.1875 0.8125 +vt 0.2500 0.8125 +vt 0.3125 0.8125 +vt 0.3750 0.8125 +vt 0.4375 0.8125 +vt 0.5000 0.8125 +vt 0.5625 0.8125 +vt 0.6250 0.8125 +vt 0.6875 0.8125 +vt 0.7500 0.8125 +vt 0.8125 0.8125 +vt 0.8750 0.8125 +vt 0.9375 0.8125 +vt 1.0000 0.8125 +vt 0.0000 0.7500 +vt 0.0625 0.7500 +vt 0.1250 0.7500 +vt 0.1875 0.7500 +vt 0.2500 0.7500 +vt 0.3125 0.7500 +vt 0.3750 0.7500 +vt 0.4375 0.7500 +vt 0.5000 0.7500 +vt 0.5625 0.7500 +vt 0.6250 0.7500 +vt 0.6875 0.7500 +vt 0.7500 0.7500 +vt 0.8125 0.7500 +vt 0.8750 0.7500 +vt 0.9375 0.7500 +vt 1.0000 0.7500 +vt 0.0000 0.6875 +vt 0.0625 0.6875 +vt 0.1250 0.6875 +vt 0.1875 0.6875 +vt 0.2500 0.6875 +vt 0.3125 0.6875 +vt 0.3750 0.6875 +vt 0.4375 0.6875 +vt 0.5000 0.6875 +vt 0.5625 0.6875 +vt 0.6250 0.6875 +vt 0.6875 0.6875 +vt 0.7500 0.6875 +vt 0.8125 0.6875 +vt 0.8750 0.6875 +vt 0.9375 0.6875 +vt 1.0000 0.6875 +vt 0.0000 0.6250 +vt 0.0625 0.6250 +vt 0.1250 0.6250 +vt 0.1875 0.6250 +vt 0.2500 0.6250 +vt 0.3125 0.6250 +vt 0.3750 0.6250 +vt 0.4375 0.6250 +vt 0.5000 0.6250 +vt 0.5625 0.6250 +vt 0.6250 0.6250 +vt 0.6875 0.6250 +vt 0.7500 0.6250 +vt 0.8125 0.6250 +vt 0.8750 0.6250 +vt 0.9375 0.6250 +vt 1.0000 0.6250 +vt 0.0000 0.5625 +vt 0.0625 0.5625 +vt 0.1250 0.5625 +vt 0.1875 0.5625 +vt 0.2500 0.5625 +vt 0.3125 0.5625 +vt 0.3750 0.5625 +vt 0.4375 0.5625 +vt 0.5000 0.5625 +vt 0.5625 0.5625 +vt 0.6250 0.5625 +vt 0.6875 0.5625 +vt 0.7500 0.5625 +vt 0.8125 0.5625 +vt 0.8750 0.5625 +vt 0.9375 0.5625 +vt 1.0000 0.5625 +vt 0.0000 0.5000 +vt 0.0625 0.5000 +vt 0.1250 0.5000 +vt 0.1875 0.5000 +vt 0.2500 0.5000 +vt 0.3125 0.5000 +vt 0.3750 0.5000 +vt 0.4375 0.5000 +vt 0.5000 0.5000 +vt 0.5625 0.5000 +vt 0.6250 0.5000 +vt 0.6875 0.5000 +vt 0.7500 0.5000 +vt 0.8125 0.5000 +vt 0.8750 0.5000 +vt 0.9375 0.5000 +vt 1.0000 0.5000 +vt 0.0000 0.4375 +vt 0.0625 0.4375 +vt 0.1250 0.4375 +vt 0.1875 0.4375 +vt 0.2500 0.4375 +vt 0.3125 0.4375 +vt 0.3750 0.4375 +vt 0.4375 0.4375 +vt 0.5000 0.4375 +vt 0.5625 0.4375 +vt 0.6250 0.4375 +vt 0.6875 0.4375 +vt 0.7500 0.4375 +vt 0.8125 0.4375 +vt 0.8750 0.4375 +vt 0.9375 0.4375 +vt 1.0000 0.4375 +vt 0.0000 0.3750 +vt 0.0625 0.3750 +vt 0.1250 0.3750 +vt 0.1875 0.3750 +vt 0.2500 0.3750 +vt 0.3125 0.3750 +vt 0.3750 0.3750 +vt 0.4375 0.3750 +vt 0.5000 0.3750 +vt 0.5625 0.3750 +vt 0.6250 0.3750 +vt 0.6875 0.3750 +vt 0.7500 0.3750 +vt 0.8125 0.3750 +vt 0.8750 0.3750 +vt 0.9375 0.3750 +vt 1.0000 0.3750 +vt 0.0000 0.3125 +vt 0.0625 0.3125 +vt 0.1250 0.3125 +vt 0.1875 0.3125 +vt 0.2500 0.3125 +vt 0.3125 0.3125 +vt 0.3750 0.3125 +vt 0.4375 0.3125 +vt 0.5000 0.3125 +vt 0.5625 0.3125 +vt 0.6250 0.3125 +vt 0.6875 0.3125 +vt 0.7500 0.3125 +vt 0.8125 0.3125 +vt 0.8750 0.3125 +vt 0.9375 0.3125 +vt 1.0000 0.3125 +vt 0.0000 0.2500 +vt 0.0625 0.2500 +vt 0.1250 0.2500 +vt 0.1875 0.2500 +vt 0.2500 0.2500 +vt 0.3125 0.2500 +vt 0.3750 0.2500 +vt 0.4375 0.2500 +vt 0.5000 0.2500 +vt 0.5625 0.2500 +vt 0.6250 0.2500 +vt 0.6875 0.2500 +vt 0.7500 0.2500 +vt 0.8125 0.2500 +vt 0.8750 0.2500 +vt 0.9375 0.2500 +vt 1.0000 0.2500 +vt 0.0000 0.1875 +vt 0.0625 0.1875 +vt 0.1250 0.1875 +vt 0.1875 0.1875 +vt 0.2500 0.1875 +vt 0.3125 0.1875 +vt 0.3750 0.1875 +vt 0.4375 0.1875 +vt 0.5000 0.1875 +vt 0.5625 0.1875 +vt 0.6250 0.1875 +vt 0.6875 0.1875 +vt 0.7500 0.1875 +vt 0.8125 0.1875 +vt 0.8750 0.1875 +vt 0.9375 0.1875 +vt 1.0000 0.1875 +vt 0.0000 0.1250 +vt 0.0625 0.1250 +vt 0.1250 0.1250 +vt 0.1875 0.1250 +vt 0.2500 0.1250 +vt 0.3125 0.1250 +vt 0.3750 0.1250 +vt 0.4375 0.1250 +vt 0.5000 0.1250 +vt 0.5625 0.1250 +vt 0.6250 0.1250 +vt 0.6875 0.1250 +vt 0.7500 0.1250 +vt 0.8125 0.1250 +vt 0.8750 0.1250 +vt 0.9375 0.1250 +vt 1.0000 0.1250 +vt 0.0000 0.0625 +vt 0.0625 0.0625 +vt 0.1250 0.0625 +vt 0.1875 0.0625 +vt 0.2500 0.0625 +vt 0.3125 0.0625 +vt 0.3750 0.0625 +vt 0.4375 0.0625 +vt 0.5000 0.0625 +vt 0.5625 0.0625 +vt 0.6250 0.0625 +vt 0.6875 0.0625 +vt 0.7500 0.0625 +vt 0.8125 0.0625 +vt 0.8750 0.0625 +vt 0.9375 0.0625 +vt 1.0000 0.0625 +vt 0.0000 0.0000 +vt 0.0625 0.0000 +vt 0.1250 0.0000 +vt 0.1875 0.0000 +vt 0.2500 0.0000 +vt 0.3125 0.0000 +vt 0.3750 0.0000 +vt 0.4375 0.0000 +vt 0.5000 0.0000 +vt 0.5625 0.0000 +vt 0.6250 0.0000 +vt 0.6875 0.0000 +vt 0.7500 0.0000 +vt 0.8125 0.0000 +vt 0.8750 0.0000 +vt 0.9375 0.0000 +vt 1.0000 0.0000 +vn 0.0000 1.0000 0.0000 +vn 0.0000 1.0000 0.0000 +vn 0.0000 1.0000 0.0000 +vn 0.0000 1.0000 0.0000 +vn 0.0000 1.0000 0.0000 +vn 0.0000 1.0000 0.0000 +vn 0.0000 1.0000 0.0000 +vn 0.0000 1.0000 0.0000 +vn 0.0000 1.0000 0.0000 +vn 0.0000 1.0000 0.0000 +vn 0.0000 1.0000 0.0000 +vn 0.0000 1.0000 0.0000 +vn 0.0000 1.0000 0.0000 +vn 0.0000 1.0000 0.0000 +vn 0.0000 1.0000 0.0000 +vn 0.0000 1.0000 0.0000 +vn 0.0000 1.0000 0.0000 +vn 0.1951 0.9808 0.0000 +vn 0.1802 0.9808 0.0747 +vn 0.1379 0.9808 0.1379 +vn 0.0747 0.9808 0.1802 +vn 0.0000 0.9808 0.1951 +vn -0.0747 0.9808 0.1802 +vn -0.1379 0.9808 0.1379 +vn -0.1802 0.9808 0.0747 +vn -0.1951 0.9808 0.0000 +vn -0.1802 0.9808 -0.0747 +vn -0.1379 0.9808 -0.1379 +vn -0.0747 0.9808 -0.1802 +vn -0.0000 0.9808 -0.1951 +vn 0.0747 0.9808 -0.1802 +vn 0.1379 0.9808 -0.1379 +vn 0.1802 0.9808 -0.0747 +vn 0.1951 0.9808 -0.0000 +vn 0.3827 0.9239 0.0000 +vn 0.3536 0.9239 0.1464 +vn 0.2706 0.9239 0.2706 +vn 0.1464 0.9239 0.3536 +vn 0.0000 0.9239 0.3827 +vn -0.1464 0.9239 0.3536 +vn -0.2706 0.9239 0.2706 +vn -0.3536 0.9239 0.1464 +vn -0.3827 0.9239 0.0000 +vn -0.3536 0.9239 -0.1464 +vn -0.2706 0.9239 -0.2706 +vn -0.1464 0.9239 -0.3536 +vn -0.0000 0.9239 -0.3827 +vn 0.1464 0.9239 -0.3536 +vn 0.2706 0.9239 -0.2706 +vn 0.3536 0.9239 -0.1464 +vn 0.3827 0.9239 -0.0000 +vn 0.5556 0.8315 0.0000 +vn 0.5133 0.8315 0.2126 +vn 0.3928 0.8315 0.3928 +vn 0.2126 0.8315 0.5133 +vn 0.0000 0.8315 0.5556 +vn -0.2126 0.8315 0.5133 +vn -0.3928 0.8315 0.3928 +vn -0.5133 0.8315 0.2126 +vn -0.5556 0.8315 0.0000 +vn -0.5133 0.8315 -0.2126 +vn -0.3928 0.8315 -0.3928 +vn -0.2126 0.8315 -0.5133 +vn -0.0000 0.8315 -0.5556 +vn 0.2126 0.8315 -0.5133 +vn 0.3928 0.8315 -0.3928 +vn 0.5133 0.8315 -0.2126 +vn 0.5556 0.8315 -0.0000 +vn 0.7071 0.7071 0.0000 +vn 0.6533 0.7071 0.2706 +vn 0.5000 0.7071 0.5000 +vn 0.2706 0.7071 0.6533 +vn 0.0000 0.7071 0.7071 +vn -0.2706 0.7071 0.6533 +vn -0.5000 0.7071 0.5000 +vn -0.6533 0.7071 0.2706 +vn -0.7071 0.7071 0.0000 +vn -0.6533 0.7071 -0.2706 +vn -0.5000 0.7071 -0.5000 +vn -0.2706 0.7071 -0.6533 +vn -0.0000 0.7071 -0.7071 +vn 0.2706 0.7071 -0.6533 +vn 0.5000 0.7071 -0.5000 +vn 0.6533 0.7071 -0.2706 +vn 0.7071 0.7071 -0.0000 +vn 0.8315 0.5556 0.0000 +vn 0.7682 0.5556 0.3182 +vn 0.5879 0.5556 0.5879 +vn 0.3182 0.5556 0.7682 +vn 0.0000 0.5556 0.8315 +vn -0.3182 0.5556 0.7682 +vn -0.5879 0.5556 0.5879 +vn -0.7682 0.5556 0.3182 +vn -0.8315 0.5556 0.0000 +vn -0.7682 0.5556 -0.3182 +vn -0.5879 0.5556 -0.5879 +vn -0.3182 0.5556 -0.7682 +vn -0.0000 0.5556 -0.8315 +vn 0.3182 0.5556 -0.7682 +vn 0.5879 0.5556 -0.5879 +vn 0.7682 0.5556 -0.3182 +vn 0.8315 0.5556 -0.0000 +vn 0.9239 0.3827 0.0000 +vn 0.8536 0.3827 0.3536 +vn 0.6533 0.3827 0.6533 +vn 0.3536 0.3827 0.8536 +vn 0.0000 0.3827 0.9239 +vn -0.3536 0.3827 0.8536 +vn -0.6533 0.3827 0.6533 +vn -0.8536 0.3827 0.3536 +vn -0.9239 0.3827 0.0000 +vn -0.8536 0.3827 -0.3536 +vn -0.6533 0.3827 -0.6533 +vn -0.3536 0.3827 -0.8536 +vn -0.0000 0.3827 -0.9239 +vn 0.3536 0.3827 -0.8536 +vn 0.6533 0.3827 -0.6533 +vn 0.8536 0.3827 -0.3536 +vn 0.9239 0.3827 -0.0000 +vn 0.9808 0.1951 0.0000 +vn 0.9061 0.1951 0.3753 +vn 0.6935 0.1951 0.6935 +vn 0.3753 0.1951 0.9061 +vn 0.0000 0.1951 0.9808 +vn -0.3753 0.1951 0.9061 +vn -0.6935 0.1951 0.6935 +vn -0.9061 0.1951 0.3753 +vn -0.9808 0.1951 0.0000 +vn -0.9061 0.1951 -0.3753 +vn -0.6935 0.1951 -0.6935 +vn -0.3753 0.1951 -0.9061 +vn -0.0000 0.1951 -0.9808 +vn 0.3753 0.1951 -0.9061 +vn 0.6935 0.1951 -0.6935 +vn 0.9061 0.1951 -0.3753 +vn 0.9808 0.1951 -0.0000 +vn 1.0000 0.0000 0.0000 +vn 0.9239 0.0000 0.3827 +vn 0.7071 0.0000 0.7071 +vn 0.3827 0.0000 0.9239 +vn 0.0000 0.0000 1.0000 +vn -0.3827 0.0000 0.9239 +vn -0.7071 0.0000 0.7071 +vn -0.9239 0.0000 0.3827 +vn -1.0000 0.0000 0.0000 +vn -0.9239 0.0000 -0.3827 +vn -0.7071 0.0000 -0.7071 +vn -0.3827 0.0000 -0.9239 +vn -0.0000 0.0000 -1.0000 +vn 0.3827 0.0000 -0.9239 +vn 0.7071 0.0000 -0.7071 +vn 0.9239 0.0000 -0.3827 +vn 1.0000 0.0000 -0.0000 +vn 0.9808 -0.1951 0.0000 +vn 0.9061 -0.1951 0.3753 +vn 0.6935 -0.1951 0.6935 +vn 0.3753 -0.1951 0.9061 +vn 0.0000 -0.1951 0.9808 +vn -0.3753 -0.1951 0.9061 +vn -0.6935 -0.1951 0.6935 +vn -0.9061 -0.1951 0.3753 +vn -0.9808 -0.1951 0.0000 +vn -0.9061 -0.1951 -0.3753 +vn -0.6935 -0.1951 -0.6935 +vn -0.3753 -0.1951 -0.9061 +vn -0.0000 -0.1951 -0.9808 +vn 0.3753 -0.1951 -0.9061 +vn 0.6935 -0.1951 -0.6935 +vn 0.9061 -0.1951 -0.3753 +vn 0.9808 -0.1951 -0.0000 +vn 0.9239 -0.3827 0.0000 +vn 0.8536 -0.3827 0.3536 +vn 0.6533 -0.3827 0.6533 +vn 0.3536 -0.3827 0.8536 +vn 0.0000 -0.3827 0.9239 +vn -0.3536 -0.3827 0.8536 +vn -0.6533 -0.3827 0.6533 +vn -0.8536 -0.3827 0.3536 +vn -0.9239 -0.3827 0.0000 +vn -0.8536 -0.3827 -0.3536 +vn -0.6533 -0.3827 -0.6533 +vn -0.3536 -0.3827 -0.8536 +vn -0.0000 -0.3827 -0.9239 +vn 0.3536 -0.3827 -0.8536 +vn 0.6533 -0.3827 -0.6533 +vn 0.8536 -0.3827 -0.3536 +vn 0.9239 -0.3827 -0.0000 +vn 0.8315 -0.5556 0.0000 +vn 0.7682 -0.5556 0.3182 +vn 0.5879 -0.5556 0.5879 +vn 0.3182 -0.5556 0.7682 +vn 0.0000 -0.5556 0.8315 +vn -0.3182 -0.5556 0.7682 +vn -0.5879 -0.5556 0.5879 +vn -0.7682 -0.5556 0.3182 +vn -0.8315 -0.5556 0.0000 +vn -0.7682 -0.5556 -0.3182 +vn -0.5879 -0.5556 -0.5879 +vn -0.3182 -0.5556 -0.7682 +vn -0.0000 -0.5556 -0.8315 +vn 0.3182 -0.5556 -0.7682 +vn 0.5879 -0.5556 -0.5879 +vn 0.7682 -0.5556 -0.3182 +vn 0.8315 -0.5556 -0.0000 +vn 0.7071 -0.7071 0.0000 +vn 0.6533 -0.7071 0.2706 +vn 0.5000 -0.7071 0.5000 +vn 0.2706 -0.7071 0.6533 +vn 0.0000 -0.7071 0.7071 +vn -0.2706 -0.7071 0.6533 +vn -0.5000 -0.7071 0.5000 +vn -0.6533 -0.7071 0.2706 +vn -0.7071 -0.7071 0.0000 +vn -0.6533 -0.7071 -0.2706 +vn -0.5000 -0.7071 -0.5000 +vn -0.2706 -0.7071 -0.6533 +vn -0.0000 -0.7071 -0.7071 +vn 0.2706 -0.7071 -0.6533 +vn 0.5000 -0.7071 -0.5000 +vn 0.6533 -0.7071 -0.2706 +vn 0.7071 -0.7071 -0.0000 +vn 0.5556 -0.8315 0.0000 +vn 0.5133 -0.8315 0.2126 +vn 0.3928 -0.8315 0.3928 +vn 0.2126 -0.8315 0.5133 +vn 0.0000 -0.8315 0.5556 +vn -0.2126 -0.8315 0.5133 +vn -0.3928 -0.8315 0.3928 +vn -0.5133 -0.8315 0.2126 +vn -0.5556 -0.8315 0.0000 +vn -0.5133 -0.8315 -0.2126 +vn -0.3928 -0.8315 -0.3928 +vn -0.2126 -0.8315 -0.5133 +vn -0.0000 -0.8315 -0.5556 +vn 0.2126 -0.8315 -0.5133 +vn 0.3928 -0.8315 -0.3928 +vn 0.5133 -0.8315 -0.2126 +vn 0.5556 -0.8315 -0.0000 +vn 0.3827 -0.9239 0.0000 +vn 0.3536 -0.9239 0.1464 +vn 0.2706 -0.9239 0.2706 +vn 0.1464 -0.9239 0.3536 +vn 0.0000 -0.9239 0.3827 +vn -0.1464 -0.9239 0.3536 +vn -0.2706 -0.9239 0.2706 +vn -0.3536 -0.9239 0.1464 +vn -0.3827 -0.9239 0.0000 +vn -0.3536 -0.9239 -0.1464 +vn -0.2706 -0.9239 -0.2706 +vn -0.1464 -0.9239 -0.3536 +vn -0.0000 -0.9239 -0.3827 +vn 0.1464 -0.9239 -0.3536 +vn 0.2706 -0.9239 -0.2706 +vn 0.3536 -0.9239 -0.1464 +vn 0.3827 -0.9239 -0.0000 +vn 0.1951 -0.9808 0.0000 +vn 0.1802 -0.9808 0.0747 +vn 0.1379 -0.9808 0.1379 +vn 0.0747 -0.9808 0.1802 +vn 0.0000 -0.9808 0.1951 +vn -0.0747 -0.9808 0.1802 +vn -0.1379 -0.9808 0.1379 +vn -0.1802 -0.9808 0.0747 +vn -0.1951 -0.9808 0.0000 +vn -0.1802 -0.9808 -0.0747 +vn -0.1379 -0.9808 -0.1379 +vn -0.0747 -0.9808 -0.1802 +vn -0.0000 -0.9808 -0.1951 +vn 0.0747 -0.9808 -0.1802 +vn 0.1379 -0.9808 -0.1379 +vn 0.1802 -0.9808 -0.0747 +vn 0.1951 -0.9808 -0.0000 +vn 0.0000 -1.0000 0.0000 +vn 0.0000 -1.0000 0.0000 +vn 0.0000 -1.0000 0.0000 +vn 0.0000 -1.0000 0.0000 +vn 0.0000 -1.0000 0.0000 +vn -0.0000 -1.0000 0.0000 +vn -0.0000 -1.0000 0.0000 +vn -0.0000 -1.0000 0.0000 +vn -0.0000 -1.0000 0.0000 +vn -0.0000 -1.0000 -0.0000 +vn -0.0000 -1.0000 -0.0000 +vn -0.0000 -1.0000 -0.0000 +vn -0.0000 -1.0000 -0.0000 +vn 0.0000 -1.0000 -0.0000 +vn 0.0000 -1.0000 -0.0000 +vn 0.0000 -1.0000 -0.0000 +vn 0.0000 -1.0000 -0.0000 +usemtl m0 +f 1/1/1 18/18/18 19/19/19 +f 1/1/1 19/19/19 2/2/2 +f 2/2/2 19/19/19 20/20/20 +f 2/2/2 20/20/20 3/3/3 +f 3/3/3 20/20/20 21/21/21 +f 3/3/3 21/21/21 4/4/4 +f 4/4/4 21/21/21 22/22/22 +f 4/4/4 22/22/22 5/5/5 +f 5/5/5 22/22/22 23/23/23 +f 5/5/5 23/23/23 6/6/6 +f 6/6/6 23/23/23 24/24/24 +f 6/6/6 24/24/24 7/7/7 +f 7/7/7 24/24/24 25/25/25 +f 7/7/7 25/25/25 8/8/8 +f 8/8/8 25/25/25 26/26/26 +f 8/8/8 26/26/26 9/9/9 +f 9/9/9 26/26/26 27/27/27 +f 9/9/9 27/27/27 10/10/10 +f 10/10/10 27/27/27 28/28/28 +f 10/10/10 28/28/28 11/11/11 +f 11/11/11 28/28/28 29/29/29 +f 11/11/11 29/29/29 12/12/12 +f 12/12/12 29/29/29 30/30/30 +f 12/12/12 30/30/30 13/13/13 +f 13/13/13 30/30/30 31/31/31 +f 13/13/13 31/31/31 14/14/14 +f 14/14/14 31/31/31 32/32/32 +f 14/14/14 32/32/32 15/15/15 +f 15/15/15 32/32/32 33/33/33 +f 15/15/15 33/33/33 16/16/16 +f 16/16/16 33/33/33 34/34/34 +f 16/16/16 34/34/34 17/17/17 +f 18/18/18 35/35/35 36/36/36 +f 18/18/18 36/36/36 19/19/19 +f 19/19/19 36/36/36 37/37/37 +f 19/19/19 37/37/37 20/20/20 +f 20/20/20 37/37/37 38/38/38 +f 20/20/20 38/38/38 21/21/21 +f 21/21/21 38/38/38 39/39/39 +f 21/21/21 39/39/39 22/22/22 +f 22/22/22 39/39/39 40/40/40 +f 22/22/22 40/40/40 23/23/23 +f 23/23/23 40/40/40 41/41/41 +f 23/23/23 41/41/41 24/24/24 +f 24/24/24 41/41/41 42/42/42 +f 24/24/24 42/42/42 25/25/25 +f 25/25/25 42/42/42 43/43/43 +f 25/25/25 43/43/43 26/26/26 +f 26/26/26 43/43/43 44/44/44 +f 26/26/26 44/44/44 27/27/27 +f 27/27/27 44/44/44 45/45/45 +f 27/27/27 45/45/45 28/28/28 +f 28/28/28 45/45/45 46/46/46 +f 28/28/28 46/46/46 29/29/29 +f 29/29/29 46/46/46 47/47/47 +f 29/29/29 47/47/47 30/30/30 +f 30/30/30 47/47/47 48/48/48 +f 30/30/30 48/48/48 31/31/31 +f 31/31/31 48/48/48 49/49/49 +f 31/31/31 49/49/49 32/32/32 +f 32/32/32 49/49/49 50/50/50 +f 32/32/32 50/50/50 33/33/33 +f 33/33/33 50/50/50 51/51/51 +f 33/33/33 51/51/51 34/34/34 +f 35/35/35 52/52/52 53/53/53 +f 35/35/35 53/53/53 36/36/36 +f 36/36/36 53/53/53 54/54/54 +f 36/36/36 54/54/54 37/37/37 +f 37/37/37 54/54/54 55/55/55 +f 37/37/37 55/55/55 38/38/38 +f 38/38/38 55/55/55 56/56/56 +f 38/38/38 56/56/56 39/39/39 +f 39/39/39 56/56/56 57/57/57 +f 39/39/39 57/57/57 40/40/40 +f 40/40/40 57/57/57 58/58/58 +f 40/40/40 58/58/58 41/41/41 +f 41/41/41 58/58/58 59/59/59 +f 41/41/41 59/59/59 42/42/42 +f 42/42/42 59/59/59 60/60/60 +f 42/42/42 60/60/60 43/43/43 +f 43/43/43 60/60/60 61/61/61 +f 43/43/43 61/61/61 44/44/44 +f 44/44/44 61/61/61 62/62/62 +f 44/44/44 62/62/62 45/45/45 +f 45/45/45 62/62/62 63/63/63 +f 45/45/45 63/63/63 46/46/46 +f 46/46/46 63/63/63 64/64/64 +f 46/46/46 64/64/64 47/47/47 +f 47/47/47 64/64/64 65/65/65 +f 47/47/47 65/65/65 48/48/48 +f 48/48/48 65/65/65 66/66/66 +f 48/48/48 66/66/66 49/49/49 +f 49/49/49 66/66/66 67/67/67 +f 49/49/49 67/67/67 50/50/50 +f 50/50/50 67/67/67 68/68/68 +f 50/50/50 68/68/68 51/51/51 +f 52/52/52 69/69/69 70/70/70 +f 52/52/52 70/70/70 53/53/53 +f 53/53/53 70/70/70 71/71/71 +f 53/53/53 71/71/71 54/54/54 +f 54/54/54 71/71/71 72/72/72 +f 54/54/54 72/72/72 55/55/55 +f 55/55/55 72/72/72 73/73/73 +f 55/55/55 73/73/73 56/56/56 +f 56/56/56 73/73/73 74/74/74 +f 56/56/56 74/74/74 57/57/57 +f 57/57/57 74/74/74 75/75/75 +f 57/57/57 75/75/75 58/58/58 +f 58/58/58 75/75/75 76/76/76 +f 58/58/58 76/76/76 59/59/59 +f 59/59/59 76/76/76 77/77/77 +f 59/59/59 77/77/77 60/60/60 +f 60/60/60 77/77/77 78/78/78 +f 60/60/60 78/78/78 61/61/61 +f 61/61/61 78/78/78 79/79/79 +f 61/61/61 79/79/79 62/62/62 +f 62/62/62 79/79/79 80/80/80 +f 62/62/62 80/80/80 63/63/63 +f 63/63/63 80/80/80 81/81/81 +f 63/63/63 81/81/81 64/64/64 +f 64/64/64 81/81/81 82/82/82 +f 64/64/64 82/82/82 65/65/65 +f 65/65/65 82/82/82 83/83/83 +f 65/65/65 83/83/83 66/66/66 +f 66/66/66 83/83/83 84/84/84 +f 66/66/66 84/84/84 67/67/67 +f 67/67/67 84/84/84 85/85/85 +f 67/67/67 85/85/85 68/68/68 +f 69/69/69 86/86/86 87/87/87 +f 69/69/69 87/87/87 70/70/70 +f 70/70/70 87/87/87 88/88/88 +f 70/70/70 88/88/88 71/71/71 +f 71/71/71 88/88/88 89/89/89 +f 71/71/71 89/89/89 72/72/72 +f 72/72/72 89/89/89 90/90/90 +f 72/72/72 90/90/90 73/73/73 +f 73/73/73 90/90/90 91/91/91 +f 73/73/73 91/91/91 74/74/74 +f 74/74/74 91/91/91 92/92/92 +f 74/74/74 92/92/92 75/75/75 +f 75/75/75 92/92/92 93/93/93 +f 75/75/75 93/93/93 76/76/76 +f 76/76/76 93/93/93 94/94/94 +f 76/76/76 94/94/94 77/77/77 +f 77/77/77 94/94/94 95/95/95 +f 77/77/77 95/95/95 78/78/78 +f 78/78/78 95/95/95 96/96/96 +f 78/78/78 96/96/96 79/79/79 +f 79/79/79 96/96/96 97/97/97 +f 79/79/79 97/97/97 80/80/80 +f 80/80/80 97/97/97 98/98/98 +f 80/80/80 98/98/98 81/81/81 +f 81/81/81 98/98/98 99/99/99 +f 81/81/81 99/99/99 82/82/82 +f 82/82/82 99/99/99 100/100/100 +f 82/82/82 100/100/100 83/83/83 +f 83/83/83 100/100/100 101/101/101 +f 83/83/83 101/101/101 84/84/84 +f 84/84/84 101/101/101 102/102/102 +f 84/84/84 102/102/102 85/85/85 +f 86/86/86 103/103/103 104/104/104 +f 86/86/86 104/104/104 87/87/87 +f 87/87/87 104/104/104 105/105/105 +f 87/87/87 105/105/105 88/88/88 +f 88/88/88 105/105/105 106/106/106 +f 88/88/88 106/106/106 89/89/89 +f 89/89/89 106/106/106 107/107/107 +f 89/89/89 107/107/107 90/90/90 +f 90/90/90 107/107/107 108/108/108 +f 90/90/90 108/108/108 91/91/91 +f 91/91/91 108/108/108 109/109/109 +f 91/91/91 109/109/109 92/92/92 +f 92/92/92 109/109/109 110/110/110 +f 92/92/92 110/110/110 93/93/93 +f 93/93/93 110/110/110 111/111/111 +f 93/93/93 111/111/111 94/94/94 +f 94/94/94 111/111/111 112/112/112 +f 94/94/94 112/112/112 95/95/95 +f 95/95/95 112/112/112 113/113/113 +f 95/95/95 113/113/113 96/96/96 +f 96/96/96 113/113/113 114/114/114 +f 96/96/96 114/114/114 97/97/97 +f 97/97/97 114/114/114 115/115/115 +f 97/97/97 115/115/115 98/98/98 +f 98/98/98 115/115/115 116/116/116 +f 98/98/98 116/116/116 99/99/99 +f 99/99/99 116/116/116 117/117/117 +f 99/99/99 117/117/117 100/100/100 +f 100/100/100 117/117/117 118/118/118 +f 100/100/100 118/118/118 101/101/101 +f 101/101/101 118/118/118 119/119/119 +f 101/101/101 119/119/119 102/102/102 +f 103/103/103 120/120/120 121/121/121 +f 103/103/103 121/121/121 104/104/104 +f 104/104/104 121/121/121 122/122/122 +f 104/104/104 122/122/122 105/105/105 +f 105/105/105 122/122/122 123/123/123 +f 105/105/105 123/123/123 106/106/106 +f 106/106/106 123/123/123 124/124/124 +f 106/106/106 124/124/124 107/107/107 +f 107/107/107 124/124/124 125/125/125 +f 107/107/107 125/125/125 108/108/108 +f 108/108/108 125/125/125 126/126/126 +f 108/108/108 126/126/126 109/109/109 +f 109/109/109 126/126/126 127/127/127 +f 109/109/109 127/127/127 110/110/110 +f 110/110/110 127/127/127 128/128/128 +f 110/110/110 128/128/128 111/111/111 +f 111/111/111 128/128/128 129/129/129 +f 111/111/111 129/129/129 112/112/112 +f 112/112/112 129/129/129 130/130/130 +f 112/112/112 130/130/130 113/113/113 +f 113/113/113 130/130/130 131/131/131 +f 113/113/113 131/131/131 114/114/114 +f 114/114/114 131/131/131 132/132/132 +f 114/114/114 132/132/132 115/115/115 +f 115/115/115 132/132/132 133/133/133 +f 115/115/115 133/133/133 116/116/116 +f 116/116/116 133/133/133 134/134/134 +f 116/116/116 134/134/134 117/117/117 +f 117/117/117 134/134/134 135/135/135 +f 117/117/117 135/135/135 118/118/118 +f 118/118/118 135/135/135 136/136/136 +f 118/118/118 136/136/136 119/119/119 +f 120/120/120 137/137/137 138/138/138 +f 120/120/120 138/138/138 121/121/121 +f 121/121/121 138/138/138 139/139/139 +f 121/121/121 139/139/139 122/122/122 +f 122/122/122 139/139/139 140/140/140 +f 122/122/122 140/140/140 123/123/123 +f 123/123/123 140/140/140 141/141/141 +f 123/123/123 141/141/141 124/124/124 +f 124/124/124 141/141/141 142/142/142 +f 124/124/124 142/142/142 125/125/125 +f 125/125/125 142/142/142 143/143/143 +f 125/125/125 143/143/143 126/126/126 +f 126/126/126 143/143/143 144/144/144 +f 126/126/126 144/144/144 127/127/127 +f 127/127/127 144/144/144 145/145/145 +f 127/127/127 145/145/145 128/128/128 +f 128/128/128 145/145/145 146/146/146 +f 128/128/128 146/146/146 129/129/129 +f 129/129/129 146/146/146 147/147/147 +f 129/129/129 147/147/147 130/130/130 +f 130/130/130 147/147/147 148/148/148 +f 130/130/130 148/148/148 131/131/131 +f 131/131/131 148/148/148 149/149/149 +f 131/131/131 149/149/149 132/132/132 +f 132/132/132 149/149/149 150/150/150 +f 132/132/132 150/150/150 133/133/133 +f 133/133/133 150/150/150 151/151/151 +f 133/133/133 151/151/151 134/134/134 +f 134/134/134 151/151/151 152/152/152 +f 134/134/134 152/152/152 135/135/135 +f 135/135/135 152/152/152 153/153/153 +f 135/135/135 153/153/153 136/136/136 +usemtl m1 +f 137/137/137 154/154/154 155/155/155 +f 137/137/137 155/155/155 138/138/138 +f 138/138/138 155/155/155 156/156/156 +f 138/138/138 156/156/156 139/139/139 +f 139/139/139 156/156/156 157/157/157 +f 139/139/139 157/157/157 140/140/140 +f 140/140/140 157/157/157 158/158/158 +f 140/140/140 158/158/158 141/141/141 +f 141/141/141 158/158/158 159/159/159 +f 141/141/141 159/159/159 142/142/142 +f 142/142/142 159/159/159 160/160/160 +f 142/142/142 160/160/160 143/143/143 +f 143/143/143 160/160/160 161/161/161 +f 143/143/143 161/161/161 144/144/144 +f 144/144/144 161/161/161 162/162/162 +f 144/144/144 162/162/162 145/145/145 +f 145/145/145 162/162/162 163/163/163 +f 145/145/145 163/163/163 146/146/146 +f 146/146/146 163/163/163 164/164/164 +f 146/146/146 164/164/164 147/147/147 +f 147/147/147 164/164/164 165/165/165 +f 147/147/147 165/165/165 148/148/148 +f 148/148/148 165/165/165 166/166/166 +f 148/148/148 166/166/166 149/149/149 +f 149/149/149 166/166/166 167/167/167 +f 149/149/149 167/167/167 150/150/150 +f 150/150/150 167/167/167 168/168/168 +f 150/150/150 168/168/168 151/151/151 +f 151/151/151 168/168/168 169/169/169 +f 151/151/151 169/169/169 152/152/152 +f 152/152/152 169/169/169 170/170/170 +f 152/152/152 170/170/170 153/153/153 +f 154/154/154 171/171/171 172/172/172 +f 154/154/154 172/172/172 155/155/155 +f 155/155/155 172/172/172 173/173/173 +f 155/155/155 173/173/173 156/156/156 +f 156/156/156 173/173/173 174/174/174 +f 156/156/156 174/174/174 157/157/157 +f 157/157/157 174/174/174 175/175/175 +f 157/157/157 175/175/175 158/158/158 +f 158/158/158 175/175/175 176/176/176 +f 158/158/158 176/176/176 159/159/159 +f 159/159/159 176/176/176 177/177/177 +f 159/159/159 177/177/177 160/160/160 +f 160/160/160 177/177/177 178/178/178 +f 160/160/160 178/178/178 161/161/161 +f 161/161/161 178/178/178 179/179/179 +f 161/161/161 179/179/179 162/162/162 +f 162/162/162 179/179/179 180/180/180 +f 162/162/162 180/180/180 163/163/163 +f 163/163/163 180/180/180 181/181/181 +f 163/163/163 181/181/181 164/164/164 +f 164/164/164 181/181/181 182/182/182 +f 164/164/164 182/182/182 165/165/165 +f 165/165/165 182/182/182 183/183/183 +f 165/165/165 183/183/183 166/166/166 +f 166/166/166 183/183/183 184/184/184 +f 166/166/166 184/184/184 167/167/167 +f 167/167/167 184/184/184 185/185/185 +f 167/167/167 185/185/185 168/168/168 +f 168/168/168 185/185/185 186/186/186 +f 168/168/168 186/186/186 169/169/169 +f 169/169/169 186/186/186 187/187/187 +f 169/169/169 187/187/187 170/170/170 +f 171/171/171 188/188/188 189/189/189 +f 171/171/171 189/189/189 172/172/172 +f 172/172/172 189/189/189 190/190/190 +f 172/172/172 190/190/190 173/173/173 +f 173/173/173 190/190/190 191/191/191 +f 173/173/173 191/191/191 174/174/174 +f 174/174/174 191/191/191 192/192/192 +f 174/174/174 192/192/192 175/175/175 +f 175/175/175 192/192/192 193/193/193 +f 175/175/175 193/193/193 176/176/176 +f 176/176/176 193/193/193 194/194/194 +f 176/176/176 194/194/194 177/177/177 +f 177/177/177 194/194/194 195/195/195 +f 177/177/177 195/195/195 178/178/178 +f 178/178/178 195/195/195 196/196/196 +f 178/178/178 196/196/196 179/179/179 +f 179/179/179 196/196/196 197/197/197 +f 179/179/179 197/197/197 180/180/180 +f 180/180/180 197/197/197 198/198/198 +f 180/180/180 198/198/198 181/181/181 +f 181/181/181 198/198/198 199/199/199 +f 181/181/181 199/199/199 182/182/182 +f 182/182/182 199/199/199 200/200/200 +f 182/182/182 200/200/200 183/183/183 +f 183/183/183 200/200/200 201/201/201 +f 183/183/183 201/201/201 184/184/184 +f 184/184/184 201/201/201 202/202/202 +f 184/184/184 202/202/202 185/185/185 +f 185/185/185 202/202/202 203/203/203 +f 185/185/185 203/203/203 186/186/186 +f 186/186/186 203/203/203 204/204/204 +f 186/186/186 204/204/204 187/187/187 +f 188/188/188 205/205/205 206/206/206 +f 188/188/188 206/206/206 189/189/189 +f 189/189/189 206/206/206 207/207/207 +f 189/189/189 207/207/207 190/190/190 +f 190/190/190 207/207/207 208/208/208 +f 190/190/190 208/208/208 191/191/191 +f 191/191/191 208/208/208 209/209/209 +f 191/191/191 209/209/209 192/192/192 +f 192/192/192 209/209/209 210/210/210 +f 192/192/192 210/210/210 193/193/193 +f 193/193/193 210/210/210 211/211/211 +f 193/193/193 211/211/211 194/194/194 +f 194/194/194 211/211/211 212/212/212 +f 194/194/194 212/212/212 195/195/195 +f 195/195/195 212/212/212 213/213/213 +f 195/195/195 213/213/213 196/196/196 +f 196/196/196 213/213/213 214/214/214 +f 196/196/196 214/214/214 197/197/197 +f 197/197/197 214/214/214 215/215/215 +f 197/197/197 215/215/215 198/198/198 +f 198/198/198 215/215/215 216/216/216 +f 198/198/198 216/216/216 199/199/199 +f 199/199/199 216/216/216 217/217/217 +f 199/199/199 217/217/217 200/200/200 +f 200/200/200 217/217/217 218/218/218 +f 200/200/200 218/218/218 201/201/201 +f 201/201/201 218/218/218 219/219/219 +f 201/201/201 219/219/219 202/202/202 +f 202/202/202 219/219/219 220/220/220 +f 202/202/202 220/220/220 203/203/203 +f 203/203/203 220/220/220 221/221/221 +f 203/203/203 221/221/221 204/204/204 +f 205/205/205 222/222/222 223/223/223 +f 205/205/205 223/223/223 206/206/206 +f 206/206/206 223/223/223 224/224/224 +f 206/206/206 224/224/224 207/207/207 +f 207/207/207 224/224/224 225/225/225 +f 207/207/207 225/225/225 208/208/208 +f 208/208/208 225/225/225 226/226/226 +f 208/208/208 226/226/226 209/209/209 +f 209/209/209 226/226/226 227/227/227 +f 209/209/209 227/227/227 210/210/210 +f 210/210/210 227/227/227 228/228/228 +f 210/210/210 228/228/228 211/211/211 +f 211/211/211 228/228/228 229/229/229 +f 211/211/211 229/229/229 212/212/212 +f 212/212/212 229/229/229 230/230/230 +f 212/212/212 230/230/230 213/213/213 +f 213/213/213 230/230/230 231/231/231 +f 213/213/213 231/231/231 214/214/214 +f 214/214/214 231/231/231 232/232/232 +f 214/214/214 232/232/232 215/215/215 +f 215/215/215 232/232/232 233/233/233 +f 215/215/215 233/233/233 216/216/216 +f 216/216/216 233/233/233 234/234/234 +f 216/216/216 234/234/234 217/217/217 +f 217/217/217 234/234/234 235/235/235 +f 217/217/217 235/235/235 218/218/218 +f 218/218/218 235/235/235 236/236/236 +f 218/218/218 236/236/236 219/219/219 +f 219/219/219 236/236/236 237/237/237 +f 219/219/219 237/237/237 220/220/220 +f 220/220/220 237/237/237 238/238/238 +f 220/220/220 238/238/238 221/221/221 +f 222/222/222 239/239/239 240/240/240 +f 222/222/222 240/240/240 223/223/223 +f 223/223/223 240/240/240 241/241/241 +f 223/223/223 241/241/241 224/224/224 +f 224/224/224 241/241/241 242/242/242 +f 224/224/224 242/242/242 225/225/225 +f 225/225/225 242/242/242 243/243/243 +f 225/225/225 243/243/243 226/226/226 +f 226/226/226 243/243/243 244/244/244 +f 226/226/226 244/244/244 227/227/227 +f 227/227/227 244/244/244 245/245/245 +f 227/227/227 245/245/245 228/228/228 +f 228/228/228 245/245/245 246/246/246 +f 228/228/228 246/246/246 229/229/229 +f 229/229/229 246/246/246 247/247/247 +f 229/229/229 247/247/247 230/230/230 +f 230/230/230 247/247/247 248/248/248 +f 230/230/230 248/248/248 231/231/231 +f 231/231/231 248/248/248 249/249/249 +f 231/231/231 249/249/249 232/232/232 +f 232/232/232 249/249/249 250/250/250 +f 232/232/232 250/250/250 233/233/233 +f 233/233/233 250/250/250 251/251/251 +f 233/233/233 251/251/251 234/234/234 +f 234/234/234 251/251/251 252/252/252 +f 234/234/234 252/252/252 235/235/235 +f 235/235/235 252/252/252 253/253/253 +f 235/235/235 253/253/253 236/236/236 +f 236/236/236 253/253/253 254/254/254 +f 236/236/236 254/254/254 237/237/237 +f 237/237/237 254/254/254 255/255/255 +f 237/237/237 255/255/255 238/238/238 +f 239/239/239 256/256/256 257/257/257 +f 239/239/239 257/257/257 240/240/240 +f 240/240/240 257/257/257 258/258/258 +f 240/240/240 258/258/258 241/241/241 +f 241/241/241 258/258/258 259/259/259 +f 241/241/241 259/259/259 242/242/242 +f 242/242/242 259/259/259 260/260/260 +f 242/242/242 260/260/260 243/243/243 +f 243/243/243 260/260/260 261/261/261 +f 243/243/243 261/261/261 244/244/244 +f 244/244/244 261/261/261 262/262/262 +f 244/244/244 262/262/262 245/245/245 +f 245/245/245 262/262/262 263/263/263 +f 245/245/245 263/263/263 246/246/246 +f 246/246/246 263/263/263 264/264/264 +f 246/246/246 264/264/264 247/247/247 +f 247/247/247 264/264/264 265/265/265 +f 247/247/247 265/265/265 248/248/248 +f 248/248/248 265/265/265 266/266/266 +f 248/248/248 266/266/266 249/249/249 +f 249/249/249 266/266/266 267/267/267 +f 249/249/249 267/267/267 250/250/250 +f 250/250/250 267/267/267 268/268/268 +f 250/250/250 268/268/268 251/251/251 +f 251/251/251 268/268/268 269/269/269 +f 251/251/251 269/269/269 252/252/252 +f 252/252/252 269/269/269 270/270/270 +f 252/252/252 270/270/270 253/253/253 +f 253/253/253 270/270/270 271/271/271 +f 253/253/253 271/271/271 254/254/254 +f 254/254/254 271/271/271 272/272/272 +f 254/254/254 272/272/272 255/255/255 +f 256/256/256 273/273/273 274/274/274 +f 256/256/256 274/274/274 257/257/257 +f 257/257/257 274/274/274 275/275/275 +f 257/257/257 275/275/275 258/258/258 +f 258/258/258 275/275/275 276/276/276 +f 258/258/258 276/276/276 259/259/259 +f 259/259/259 276/276/276 277/277/277 +f 259/259/259 277/277/277 260/260/260 +f 260/260/260 277/277/277 278/278/278 +f 260/260/260 278/278/278 261/261/261 +f 261/261/261 278/278/278 279/279/279 +f 261/261/261 279/279/279 262/262/262 +f 262/262/262 279/279/279 280/280/280 +f 262/262/262 280/280/280 263/263/263 +f 263/263/263 280/280/280 281/281/281 +f 263/263/263 281/281/281 264/264/264 +f 264/264/264 281/281/281 282/282/282 +f 264/264/264 282/282/282 265/265/265 +f 265/265/265 282/282/282 283/283/283 +f 265/265/265 283/283/283 266/266/266 +f 266/266/266 283/283/283 284/284/284 +f 266/266/266 284/284/284 267/267/267 +f 267/267/267 284/284/284 285/285/285 +f 267/267/267 285/285/285 268/268/268 +f 268/268/268 285/285/285 286/286/286 +f 268/268/268 286/286/286 269/269/269 +f 269/269/269 286/286/286 287/287/287 +f 269/269/269 287/287/287 270/270/270 +f 270/270/270 287/287/287 288/288/288 +f 270/270/270 288/288/288 271/271/271 +f 271/271/271 288/288/288 289/289/289 +f 271/271/271 289/289/289 272/272/272 diff --git a/test/unit/assets/normal_mapped.mtl b/test/unit/assets/normal_mapped.mtl new file mode 100644 index 0000000000..75391a4a70 --- /dev/null +++ b/test/unit/assets/normal_mapped.mtl @@ -0,0 +1,6 @@ +newmtl m0 +Kd 0.8 0.8 0.8 +map_Bump spheremap.jpg + +newmtl m1 +Kd 0.5 0.5 0.5 diff --git a/test/unit/assets/normal_mapped.obj b/test/unit/assets/normal_mapped.obj new file mode 100644 index 0000000000..971557ca7f --- /dev/null +++ b/test/unit/assets/normal_mapped.obj @@ -0,0 +1,17 @@ +mtllib normal_mapped.mtl +v 0 0 0 +v 1 0 0 +v 0 1 0 +v 1 1 0 +vt 0 0 +vt 1 0 +vt 0 1 +vt 1 1 +vn 0 0 1 +vn 0 0 1 +vn 0 0 1 +vn 0 0 1 +usemtl m0 +f 1/1/1 2/2/2 3/3/3 +usemtl m1 +f 2/2/2 4/4/4 3/3/3 diff --git a/test/unit/io/loadModel.js b/test/unit/io/loadModel.js index 0c2efa9cf7..f94bd68c15 100644 --- a/test/unit/io/loadModel.js +++ b/test/unit/io/loadModel.js @@ -116,6 +116,24 @@ suite('loadModel', function () { } }); + test('a normal-mapped OBJ carries the normal map on its part', async function () { + const fakeImage = { width: 1, height: 1 }; + mockP5Prototype.loadImage = async () => fakeImage; + try { + const model = await mockP5Prototype.loadModel( + '/test/unit/assets/normal_mapped.obj' + ); + // two materials, so two parts + assert.equal(model.parts.length, 2); + // the part with the normal map carries it + const normalMapped = model.parts.find(p => p.partState.normalTexture); + assert.ok(normalMapped, 'a part has the normal map'); + assert.equal(normalMapped.partState.normalTexture, fakeImage); + } finally { + delete mockP5Prototype.loadImage; + } + }); + test('a texture that fails to load is skipped without failing the model', async function () { mockP5Prototype.loadImage = async () => { throw new Error('Not Found'); diff --git a/test/unit/io/parseMtl.js b/test/unit/io/parseMtl.js index eef4fd9269..1a7dc30c17 100644 --- a/test/unit/io/parseMtl.js +++ b/test/unit/io/parseMtl.js @@ -105,4 +105,10 @@ suite('mtlToPartState', function () { // the map scales a base shininess, which defaults to 1 expect(state.shininess).toEqual(1); }); + + test('a normal map lands on the part state', function () { + const img = { width: 1, height: 1 }; + const state = mtlToPartState({ normalTexture: img }); + expect(state.normalTexture).toBe(img); + }); }); diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index ed2af5fdc6..8c80c17e9e 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -411,6 +411,23 @@ visualSuite('WebGL', function () { screenshot(); } ); + visualTest( + 'a normal-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) + const model = await new Promise(resolve => + p5.loadModel('test/unit/assets/bump_sphere.obj', resolve) + ); + p5.background(255); + p5.pointLight(255, 255, 255, 100, -100, 200); + p5.noStroke(); + p5.scale(22); + p5.model(model); + screenshot(); + } + ); }); visualSuite('vertexProperty', function () { diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index 1e341bc9b3..1f430b2d11 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -2029,4 +2029,22 @@ visualSuite('WebGPU', function () { } ); }); + + visualSuite('3D Materials', function () { + visualTest( + 'a normal-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 + // variant (tangent attribute + normal sampling) is exercised end to end + const model = await p5.loadModel('test/unit/assets/bump_sphere.obj'); + p5.background(255); + p5.pointLight(255, 255, 255, 100, -100, 200); + p5.noStroke(); + p5.scale(22); + p5.model(model); + await screenshot(); + } + ); + }); }); 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 new file mode 100644 index 0000000000000000000000000000000000000000..71e09efd5b76c654f376a75aa2069787d7eb96db GIT binary patch literal 2458 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 Y0Mv1~29FrVLI3~&07*qoM6N<$f^2ofDF6Tf literal 0 HcmV?d00001 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 normal-mapped sphere shows surface detail under light/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/3DModel/a normal-mapped sphere shows surface detail under light/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file 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 new file mode 100644 index 0000000000000000000000000000000000000000..425c6c732c43d113d96debb0ca97fbdcb408a1b3 GIT binary patch literal 2459 zcmV;M31s$(P)%IQAv*kN8XJ*cfCKu!mth4sF%i90{T5Ff@o2LIzU|w-33Qv?%cTrL&z_yZx7h5G z>VK9LL56f`aHVsXi7Z~cI6nLAvv~E@S7ZA0>2d7Xv5L9w7m*>|CDSD$NPPV9$1!Ef zlsLOWnQ?~TysracG+cZj9ttMgt`))hKP(B zH7Z7q92uuipU!gX)TvCUtH6=J`s%A29@Z5yoe~*6dUS}!zJ2>*@7}#};>3wKc<^9! zhYUxYIddjNv{5#VlIf7h*s)`CBu<_@82|u+3=K#l>*Us3Z;kK2|6cbj$B!Qm($v%x z&ph)?uIuav89QS~{W>Hf68rb>kE2JA#+6rI83zs=h#oz9WKta&t+ri%{q-R=t92al z?YG}X&z?OSj*i2$BQot05skx#59j$)N}+u1wb$mf%9RcLTz>iGv2NYE^F6JR^1wOE zmMx1TM~;-LPIX6%zb=t+syzIHBkTnBc&{GUPPvzq0Z`3O0jF#uDIfgD?01iZu_A_hoX1y z-gVpC5Sch}V&0_?aB52_;YUDJ>RPE&nNrHN(o2Co&fp{|s;)a?oz#m;8zQz>(J&zb z%3awm3pk`xN@09T=SW4%ALq0bbNZHYXZnU4ZpdBgMWqdq@#Dvb5ZWRU5D8K|n=Li? zgN&%s?G3U1+G}fF57W&z-yApHbW`i5s3Edq#frR(*mBDRqC%ubo6-Mbg=iq+9Gf(0 zQh0r+PG3XBcBm0!R|#ohfvamk{?RTroazKimoBY7)2r~f_o+!N0~|9m|6 z+;j2NQ%}V^@4OS1K^0b0^+DNMD5>T7pB4vYnd9Dj@6G3oHF?J!cf?(H-4zc%{BRam zof~hwF*w^MWGgS@=o%ucR;`MwuDU9CfF~Y&@WFh?((ytCV8TgkrIh0K+i%an0d|+$ zym@mx^2j4`-F4Sxz?^D<&zmn$PjIX}>Dt`Fk zhxqyDpX1Lz|BOHW_#=M&@yGb&lTYI9x8G(m{`%{$7(94z3>h*cKRcvzp`D)^A`>P| z$dl`fhR~T%S^p zgh(Lv&_fU9HS)_Zzr;7+d=uY&_g#GO!3VK$;lg}>GN;|m)=L9#4Z3`?!Lb`QYzSDR z@>fHo)bo;ZN-4?(k|7I@abD66C~$xx;n_6@LCGo_;%qRUI3vc4856c( zpb;oyOJ>jJ;E;4}fh3 z<;o1+M8Kz%@}Zg1Ia2Bfa_xyoio~8hd%`B-(g$P#O^X205AYHJL}dnTd;a?CucP%} z8zMT03XfTg2m!$ekS5XxaJob(rIfd4_4*NIqRa+I5TJ`@uPR6oM5K{HscU841E3O_bRc0Z}TV zUMh)(MeZJrgOROk^SMo@XdGHha=A|~_`t{deGnRTk?GkA|8x{X`st#h@#JmFpQ3o`@g^Xul3BX0_c^zoN>(Yltz&f(c zZrZddkoe+@FRD=5l-nhuCo;afDi^7GjJJS9hz67rEh00JmtBBV@v1{QFDfmcQnVW&9o09VCY&_^V#Pji~P5owqR zV+`9~OLTyi4nXt1XCR~wGKgtM0%`s1K!unw+KDtXhjobr?YU9u5|LsQA!GeTFg5h-=FKKfS2UTFEE61h+?7c%~mA@W}U00960Sn&-)00006Nkl Date: Tue, 11 Aug 2026 14:16:12 +0530 Subject: [PATCH 4/8] add bump strength (-bm) and a bumpTexture() method for setting normal maps in code --- src/core/p5.Renderer3D.js | 18 ++++++++++++++++++ src/webgl/loading.js | 16 +++++++++++++--- src/webgl/material.js | 30 ++++++++++++++++++++++++++++++ src/webgl/p5.GeometryPart.js | 3 ++- src/webgl/shaders/phong.frag | 3 +++ src/webgpu/shaders/material.js | 5 ++++- 6 files changed, 70 insertions(+), 5 deletions(-) diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 21c261beab..8a6adfe8de 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -152,6 +152,7 @@ export class Renderer3D extends Renderer { this.states._ambientTex = null; this.states._shininessTex = null; this.states._normalTex = null; + this.states._normalScale = 1; this.states.textureMode = constants.IMAGE; this.states.textureWrapX = constants.CLAMP; this.states.textureWrapY = constants.CLAMP; @@ -651,6 +652,19 @@ export class Renderer3D extends Renderer { this._useVertexColor = geometry.vertexColors.length > 0 && !geometry.vertexColors.isDefault; + // a normal map needs per-vertex tangents. loaded models compute them at load, + // but geometry drawn with bumpTexture() (built shapes, immediate mode) won't + // have them, so build them on demand once (cached on the geometry). + if ( + this.states._normalTex && + geometry.computeTangents && + (!geometry.vertexTangents || geometry.vertexTangents.length === 0) && + geometry.uvs.length > 0 && + geometry.vertexNormals.length > 0 + ) { + geometry.computeTangents(); + } + const shader = !this._drawingFilter && this.states.userFillShader ? this.states.userFillShader @@ -715,6 +729,9 @@ export class Renderer3D extends Renderer { } if (partState.normalTexture) { this.states.setValue('_normalTex', partState.normalTexture); + if (partState.normalScale != null) { + this.states.setValue('_normalScale', partState.normalScale); + } } } @@ -1634,6 +1651,7 @@ export class Renderer3D extends Renderer { // normal map (map_Bump): perturbs the surface normal in tangent space fillShader.setUniform('uHasNormalMap', !!this.states._normalTex); fillShader.setUniform('uNormalSampler', this.states._normalTex || empty); + fillShader.setUniform('uNormalScale', this.states._normalScale); } fillShader.setUniform( 'uTint', diff --git a/src/webgl/loading.js b/src/webgl/loading.js index 0013b11446..d96d90997e 100755 --- a/src/webgl/loading.js +++ b/src/webgl/loading.js @@ -78,9 +78,15 @@ function parseMtlData(data) { //shininess texture materials[currentMaterial].shininessTexturePath = tokens[1]; } else if (tokens[0] === 'map_Bump' || tokens[0] === 'bump') { - //bump map. -bm etc can precede the path so take the last token. parsed - //but not used until the renderer handles it. + //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). 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; + } } } @@ -117,7 +123,11 @@ 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; + if (material.normalTexture) { + state.normalTexture = material.normalTexture; + // a -bm multiplier scales the bump strength; defaults to 1 when omitted + if (material.bumpScale != null) state.normalScale = material.bumpScale; + } return state; } diff --git a/src/webgl/material.js b/src/webgl/material.js index 5c9f5e8809..26cf2feec9 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -2566,6 +2566,29 @@ function material(p5, fn) { return this; }; + /** + * Sets a normal (bump) map to add surface detail to shapes under lighting. + * + * `bumpTexture()` works like texture(), but for a + * tangent-space normal map. Call it before drawing a shape and its surface + * normals get perturbed by the map, so lights react to bumps that aren't + * actually in the geometry. Pass an optional `scale` to tune the bump strength. + * + * Call `bumpTexture(null)` to turn it off, or scope it between + * push() and pop(). + * + * @method bumpTexture + * @param {p5.Image|p5.MediaElement|p5.Graphics|p5.Texture|p5.Framebuffer|p5.FramebufferTexture} tex normal map, or `null` to clear it. + * @param {Number} [scale] bump strength multiplier. Defaults to 1. + * @chainable + */ + fn.bumpTexture = function (tex, scale) { + this._assert3d('bumpTexture'); + this._renderer.bumpTexture(tex || null, scale); + + return this; + }; + /** * Changes the coordinate system used for textures when they’re applied to * custom shapes. @@ -3819,6 +3842,13 @@ function material(p5, fn) { this.states.setValue('fillColor', new Color([1, 1, 1])); }; + Renderer3D.prototype.bumpTexture = 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); + }; + Renderer3D.prototype.normalMaterial = function (...args) { this.states.setValue('drawMode', constants.FILL); this.states.setValue('_useSpecularMaterial', false); diff --git a/src/webgl/p5.GeometryPart.js b/src/webgl/p5.GeometryPart.js index b10209cbdf..53079e43b1 100644 --- a/src/webgl/p5.GeometryPart.js +++ b/src/webgl/p5.GeometryPart.js @@ -17,7 +17,8 @@ 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 + normalTexture: null, // map_Bump -> p5.Image | null + normalScale: 1 // map_Bump -bm -> bump strength multiplier }; } diff --git a/src/webgl/shaders/phong.frag b/src/webgl/shaders/phong.frag index 9bb6e8b0b8..b58221ba11 100644 --- a/src/webgl/shaders/phong.frag +++ b/src/webgl/shaders/phong.frag @@ -24,6 +24,7 @@ uniform sampler2D uShininessSampler; uniform bool uHasShininessTex; uniform sampler2D uNormalSampler; uniform bool uHasNormalMap; +uniform float uNormalScale; #endif IN vec3 vNormal; @@ -71,6 +72,8 @@ void main(void) { 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) + mapN.xy *= uNormalScale; N = normalize(mat3(T, B, N) * mapN); } #endif diff --git a/src/webgpu/shaders/material.js b/src/webgpu/shaders/material.js index 4949627f9a..9df038a87f 100644 --- a/src/webgpu/shaders/material.js +++ b/src/webgpu/shaders/material.js @@ -13,6 +13,7 @@ struct MaterialUniforms { uShininess: f32, uMetallic: f32, uHasNormalMap: u32, + uNormalScale: f32, } // Group 0: Lighting @@ -395,7 +396,9 @@ ${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; - let mapN = textureSample(uNormalSampler, uNormalSampler_sampler, input.vTexCoord).rgb * 2.0 - 1.0; + 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) + mapN = vec3(mapN.xy * material.uNormalScale, mapN.z); N = normalize(mat3x3(T, B, N) * mapN); } ` : ''} var inputs = Inputs( From 92bcd6d6b2be9e752802ff59150c910bfeed2339 Mon Sep 17 00:00:00 2001 From: nityam Date: Tue, 11 Aug 2026 14:16:12 +0530 Subject: [PATCH 5/8] test bump strength parsing and bumpTexture on a built shape --- test/unit/io/parseMtl.js | 15 +++++++++ test/unit/visual/cases/webgl.js | 31 ++++++++++++++++++ test/unit/visual/cases/webgpu.js | 28 ++++++++++++++++ .../000.png | Bin 0 -> 2625 bytes .../metadata.json | 3 ++ .../000.png | Bin 0 -> 2616 bytes .../metadata.json | 3 ++ test/unit/webgl/p5.GeometryPart.js | 3 +- 8 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 test/unit/visual/screenshots/WebGL/3DModel/bumpTexture() adds surface detail to a built shape/000.png create mode 100644 test/unit/visual/screenshots/WebGL/3DModel/bumpTexture() adds surface detail to a built shape/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/3D Materials/bumpTexture() adds surface detail to a built shape/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/3D Materials/bumpTexture() adds surface detail to a built shape/metadata.json diff --git a/test/unit/io/parseMtl.js b/test/unit/io/parseMtl.js index 1a7dc30c17..2d1144a39e 100644 --- a/test/unit/io/parseMtl.js +++ b/test/unit/io/parseMtl.js @@ -32,6 +32,21 @@ suite('parseMtlData', function () { expect(m.shininessTexturePath).toEqual('shininess.png'); // bump options like -bm precede the path, so the path is the last token. expect(m.bumpTexturePath).toEqual('bump.png'); + // and the -bm value is parsed as the bump strength multiplier + expect(m.bumpScale).toEqual(0.5); + }); + + test('a normal 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 () { + const img = { width: 1, height: 1 }; + const state = mtlToPartState({ normalTexture: img }); + expect(state.normalScale).toEqual(1); }); test('Tr is read as the inverse of d', function () { diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index 8c80c17e9e..85f9f82a44 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -428,6 +428,37 @@ visualSuite('WebGL', function () { screenshot(); } ); + + visualTest( + 'bumpTexture() adds surface detail to a built shape', + function (p5, screenshot) { + p5.createCanvas(50, 50, p5.WEBGL); + // a procedural tangent-space normal map with diagonal ridges + const nmap = p5.createImage(32, 32); + nmap.loadPixels(); + for (let y = 0; y < nmap.height; y++) { + for (let x = 0; x < nmap.width; x++) { + const s = Math.sin(((x + y) / nmap.width) * Math.PI * 6) * 0.8; + const nx = s, ny = s, nz = 1; + const inv = 1 / Math.sqrt(nx * nx + ny * ny + nz * nz); + const off = (x + y * nmap.width) * 4; + nmap.pixels[off] = (nx * inv * 0.5 + 0.5) * 255; + nmap.pixels[off + 1] = (ny * inv * 0.5 + 0.5) * 255; + nmap.pixels[off + 2] = (nz * inv * 0.5 + 0.5) * 255; + nmap.pixels[off + 3] = 255; + } + } + nmap.updatePixels(); + p5.background(255); + p5.pointLight(255, 255, 255, 50, -50, 200); + p5.noStroke(); + p5.fill(200); + // tangents are built on demand for a shape that has none of its own + p5.bumpTexture(nmap); + p5.sphere(20); + screenshot(); + } + ); }); visualSuite('vertexProperty', function () { diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index 1f430b2d11..c385438999 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -2046,5 +2046,33 @@ visualSuite('WebGPU', function () { await screenshot(); } ); + + visualTest( + 'bumpTexture() adds surface detail to a built shape', + async function (p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + const nmap = p5.createImage(32, 32); + nmap.loadPixels(); + for (let y = 0; y < nmap.height; y++) { + for (let x = 0; x < nmap.width; x++) { + const s = Math.sin(((x + y) / nmap.width) * Math.PI * 6) * 0.8; + const inv = 1 / Math.sqrt(s * s + s * s + 1); + const off = (x + y * nmap.width) * 4; + nmap.pixels[off] = (s * inv * 0.5 + 0.5) * 255; + nmap.pixels[off + 1] = (s * inv * 0.5 + 0.5) * 255; + nmap.pixels[off + 2] = (inv * 0.5 + 0.5) * 255; + nmap.pixels[off + 3] = 255; + } + } + nmap.updatePixels(); + p5.background(255); + p5.pointLight(255, 255, 255, 50, -50, 200); + p5.noStroke(); + p5.fill(200); + p5.bumpTexture(nmap); + p5.sphere(20); + await screenshot(); + } + ); }); }); diff --git a/test/unit/visual/screenshots/WebGL/3DModel/bumpTexture() adds surface detail to a built shape/000.png b/test/unit/visual/screenshots/WebGL/3DModel/bumpTexture() adds surface detail to a built shape/000.png new file mode 100644 index 0000000000000000000000000000000000000000..7fa62a9a8ad5aebdca8edccf097d7ec565098293 GIT binary patch literal 2625 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 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGL/3DModel/bumpTexture() adds surface detail to a built shape/metadata.json b/test/unit/visual/screenshots/WebGL/3DModel/bumpTexture() adds surface detail to a built shape/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/3DModel/bumpTexture() adds surface detail to a built shape/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGPU/3D Materials/bumpTexture() adds surface detail to a built shape/000.png b/test/unit/visual/screenshots/WebGPU/3D Materials/bumpTexture() adds surface detail to a built shape/000.png new file mode 100644 index 0000000000000000000000000000000000000000..c03b225f136d04420f6b817601a63a382523a7f9 GIT binary patch literal 2616 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 aR09A+^g Date: Wed, 12 Aug 2026 12:47:58 +0530 Subject: [PATCH 6/8] rename bumpTexture to normalTexture and add specular/ambient/shininess map setters --- src/core/p5.Renderer3D.js | 2 +- src/webgl/material.js | 135 ++++++++++++++++-- .../000.png | Bin .../metadata.json | 0 .../000.png | Bin .../metadata.json | 0 6 files changed, 124 insertions(+), 13 deletions(-) rename test/unit/visual/screenshots/WebGL/3DModel/{bumpTexture() adds surface detail to a built shape => normalTexture() adds surface detail to a built shape}/000.png (100%) rename test/unit/visual/screenshots/WebGL/3DModel/{bumpTexture() adds surface detail to a built shape => normalTexture() adds surface detail to a built shape}/metadata.json (100%) rename test/unit/visual/screenshots/WebGPU/3D Materials/{bumpTexture() adds surface detail to a built shape => normalTexture() adds surface detail to a built shape}/000.png (100%) rename test/unit/visual/screenshots/WebGPU/3D Materials/{bumpTexture() adds surface detail to a built shape => normalTexture() adds surface detail to a built shape}/metadata.json (100%) diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 8a6adfe8de..57fb3f55e3 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -653,7 +653,7 @@ export class Renderer3D extends Renderer { geometry.vertexColors.length > 0 && !geometry.vertexColors.isDefault; // a normal map needs per-vertex tangents. loaded models compute them at load, - // but geometry drawn with bumpTexture() (built shapes, immediate mode) won't + // but geometry drawn with normalTexture() (built shapes, immediate mode) won't // have them, so build them on demand once (cached on the geometry). if ( this.states._normalTex && diff --git a/src/webgl/material.js b/src/webgl/material.js index 26cf2feec9..c130ecca90 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -2567,24 +2567,119 @@ function material(p5, fn) { }; /** - * Sets a normal (bump) map to add surface detail to shapes under lighting. + * Sets a normal map to add surface detail to shapes under lighting. * - * `bumpTexture()` works like texture(), but for a - * tangent-space normal map. Call it before drawing a shape and its surface - * normals get perturbed by the map, so lights react to bumps that aren't - * actually in the geometry. Pass an optional `scale` to tune the bump strength. + * `normalTexture()` works like texture(), but for a + * tangent-space normal map: an image whose red, green, and blue channels + * encode the direction of the surface normal (not brightness). Call it before + * drawing a shape and its surface normals get perturbed by the map, so lights + * react to detail that isn't actually in the geometry. This is the same kind + * of map glTF models use. Pass an optional `scale` to tune the strength. * - * Call `bumpTexture(null)` to turn it off, or scope it between + * Call `normalTexture(null)` to turn it off, or scope it between * push() and pop(). * - * @method bumpTexture + * @method normalTexture * @param {p5.Image|p5.MediaElement|p5.Graphics|p5.Texture|p5.Framebuffer|p5.FramebufferTexture} tex normal map, or `null` to clear it. - * @param {Number} [scale] bump strength multiplier. Defaults to 1. + * @param {Number} [scale=1] strength multiplier for the surface detail. + * @chainable + * + * @example + *
+ * + * let normalMap; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // build a small tangent-space normal map with diagonal ridges + * normalMap = createImage(32, 32); + * normalMap.loadPixels(); + * for (let y = 0; y < normalMap.height; y += 1) { + * for (let x = 0; x < normalMap.width; x += 1) { + * 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; + * normalMap.pixels[i] = (s * inv * 0.5 + 0.5) * 255; + * normalMap.pixels[i + 1] = (s * inv * 0.5 + 0.5) * 255; + * normalMap.pixels[i + 2] = (inv * 0.5 + 0.5) * 255; + * normalMap.pixels[i + 3] = 255; + * } + * } + * normalMap.updatePixels(); + * + * describe('A sphere lit from the left, its surface covered in ridges from a normal map.'); + * } + * + * function draw() { + * background(0); + * pointLight(255, 255, 255, -50, -50, 100); + * noStroke(); + * fill(200); + * normalTexture(normalMap); + * sphere(40); + * } + * + *
+ */ + fn.normalTexture = function (tex, scale) { + this._assert3d('normalTexture'); + this._renderer.normalTexture(tex || null, scale); + + return this; + }; + + /** + * Sets a specular map to vary the specular highlight across a shape's surface. + * + * Works like texture() but for the specular colour, + * modulating specularMaterial() per pixel. + * Call `specularTexture(null)` to clear it, or scope it with push()/pop(). + * + * @method specularTexture + * @param {p5.Image|p5.MediaElement|p5.Graphics|p5.Texture|p5.Framebuffer|p5.FramebufferTexture} tex specular map, or `null` to clear it. * @chainable */ - fn.bumpTexture = function (tex, scale) { - this._assert3d('bumpTexture'); - this._renderer.bumpTexture(tex || null, scale); + fn.specularTexture = function (tex) { + this._assert3d('specularTexture'); + this._renderer.specularTexture(tex || null); + + return this; + }; + + /** + * Sets an ambient map to vary the ambient colour across a shape's surface. + * + * Works like texture() but for the ambient colour, + * modulating ambientMaterial() per pixel. + * Call `ambientTexture(null)` to clear it, or scope it with push()/pop(). + * + * @method ambientTexture + * @param {p5.Image|p5.MediaElement|p5.Graphics|p5.Texture|p5.Framebuffer|p5.FramebufferTexture} tex ambient map, or `null` to clear it. + * @chainable + */ + fn.ambientTexture = function (tex) { + this._assert3d('ambientTexture'); + this._renderer.ambientTexture(tex || null); + + return this; + }; + + /** + * Sets a shininess map to vary the shininess across a shape's surface. + * + * Works like texture() but for shininess: the map's + * red channel scales the base shininess() value + * per pixel. Call `shininessTexture(null)` to clear it, or scope it with + * push()/pop(). + * + * @method shininessTexture + * @param {p5.Image|p5.MediaElement|p5.Graphics|p5.Texture|p5.Framebuffer|p5.FramebufferTexture} tex shininess map, or `null` to clear it. + * @chainable + */ + fn.shininessTexture = function (tex) { + this._assert3d('shininessTexture'); + this._renderer.shininessTexture(tex || null); return this; }; @@ -3842,13 +3937,29 @@ function material(p5, fn) { this.states.setValue('fillColor', new Color([1, 1, 1])); }; - Renderer3D.prototype.bumpTexture = function (tex, scale = 1) { + 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); }; + // the remaining map setters mirror _applyPartState: setting a map also turns + // on the material term it modulates, so the map has something to affect. + Renderer3D.prototype.specularTexture = function (tex) { + this.states.setValue('_specularTex', tex || null); + if (tex) this.states.setValue('_useSpecularMaterial', true); + }; + + Renderer3D.prototype.ambientTexture = function (tex) { + this.states.setValue('_ambientTex', tex || null); + if (tex) this.states.setValue('_hasSetAmbient', true); + }; + + Renderer3D.prototype.shininessTexture = function (tex) { + this.states.setValue('_shininessTex', tex || null); + }; + Renderer3D.prototype.normalMaterial = function (...args) { this.states.setValue('drawMode', constants.FILL); this.states.setValue('_useSpecularMaterial', false); diff --git a/test/unit/visual/screenshots/WebGL/3DModel/bumpTexture() adds surface detail to a built shape/000.png b/test/unit/visual/screenshots/WebGL/3DModel/normalTexture() adds surface detail to a built shape/000.png similarity index 100% rename from test/unit/visual/screenshots/WebGL/3DModel/bumpTexture() adds surface detail to a built shape/000.png rename to test/unit/visual/screenshots/WebGL/3DModel/normalTexture() adds surface detail to a built shape/000.png diff --git a/test/unit/visual/screenshots/WebGL/3DModel/bumpTexture() adds surface detail to a built shape/metadata.json b/test/unit/visual/screenshots/WebGL/3DModel/normalTexture() adds surface detail to a built shape/metadata.json similarity index 100% rename from test/unit/visual/screenshots/WebGL/3DModel/bumpTexture() adds surface detail to a built shape/metadata.json rename to test/unit/visual/screenshots/WebGL/3DModel/normalTexture() adds surface detail to a built shape/metadata.json diff --git a/test/unit/visual/screenshots/WebGPU/3D Materials/bumpTexture() adds surface detail to a built shape/000.png b/test/unit/visual/screenshots/WebGPU/3D Materials/normalTexture() adds surface detail to a built shape/000.png similarity index 100% rename from test/unit/visual/screenshots/WebGPU/3D Materials/bumpTexture() adds surface detail to a built shape/000.png rename to test/unit/visual/screenshots/WebGPU/3D Materials/normalTexture() adds surface detail to a built shape/000.png diff --git a/test/unit/visual/screenshots/WebGPU/3D Materials/bumpTexture() adds surface detail to a built shape/metadata.json b/test/unit/visual/screenshots/WebGPU/3D Materials/normalTexture() adds surface detail to a built shape/metadata.json similarity index 100% rename from test/unit/visual/screenshots/WebGPU/3D Materials/bumpTexture() adds surface detail to a built shape/metadata.json rename to test/unit/visual/screenshots/WebGPU/3D Materials/normalTexture() adds surface detail to a built shape/metadata.json From fce1a828df8cd9ebd59bc4121ee3a7d50b87d9b3 Mon Sep 17 00:00:00 2001 From: nityam Date: Wed, 12 Aug 2026 12:47:58 +0530 Subject: [PATCH 7/8] test the material map setters and re-point the visual test --- test/unit/visual/cases/webgl.js | 4 +-- test/unit/visual/cases/webgpu.js | 4 +-- test/unit/webgl/p5.RendererGL.js | 44 ++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index 419c225795..76896c3a56 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -447,7 +447,7 @@ visualSuite('WebGL', function () { ); visualTest( - 'bumpTexture() adds surface detail to a built shape', + 'normalTexture() adds surface detail to a built shape', function (p5, screenshot) { p5.createCanvas(50, 50, p5.WEBGL); // a procedural tangent-space normal map with diagonal ridges @@ -471,7 +471,7 @@ visualSuite('WebGL', function () { p5.noStroke(); p5.fill(200); // tangents are built on demand for a shape that has none of its own - p5.bumpTexture(nmap); + p5.normalTexture(nmap); p5.sphere(20); screenshot(); } diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index c385438999..d81d51ca7a 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -2048,7 +2048,7 @@ visualSuite('WebGPU', function () { ); visualTest( - 'bumpTexture() adds surface detail to a built shape', + 'normalTexture() adds surface detail to a built shape', async function (p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); const nmap = p5.createImage(32, 32); @@ -2069,7 +2069,7 @@ visualSuite('WebGPU', function () { p5.pointLight(255, 255, 255, 50, -50, 200); p5.noStroke(); p5.fill(200); - p5.bumpTexture(nmap); + p5.normalTexture(nmap); p5.sphere(20); await screenshot(); } diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index c0a9ed3846..e0616b4adb 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -3242,4 +3242,48 @@ void main() { expect(widthAfterPop).toBeLessThan(widthAt20); }); }); + + suite('material map setters', function () { + const img = { width: 1, height: 1 }; + + test('normalTexture() sets the normal map + strength and null clears it', + function () { + myp5.createCanvas(50, 50, myp5.WEBGL); + myp5.normalTexture(img, 2); + expect(myp5._renderer.states._normalTex).toBe(img); + expect(myp5._renderer.states._normalScale).toEqual(2); + myp5.normalTexture(null); + expect(myp5._renderer.states._normalTex).toBeNull(); + expect(myp5._renderer.states._normalScale).toEqual(1); + } + ); + + test('specularTexture() sets the map and turns on the specular term', + function () { + myp5.createCanvas(50, 50, myp5.WEBGL); + myp5.specularTexture(img); + expect(myp5._renderer.states._specularTex).toBe(img); + expect(myp5._renderer.states._useSpecularMaterial).toBe(true); + myp5.specularTexture(null); + expect(myp5._renderer.states._specularTex).toBeNull(); + } + ); + + test('ambientTexture() sets the map and marks ambient as set', function () { + myp5.createCanvas(50, 50, myp5.WEBGL); + myp5.ambientTexture(img); + expect(myp5._renderer.states._ambientTex).toBe(img); + expect(myp5._renderer.states._hasSetAmbient).toBe(true); + myp5.ambientTexture(null); + expect(myp5._renderer.states._ambientTex).toBeNull(); + }); + + test('shininessTexture() sets and clears the map', function () { + myp5.createCanvas(50, 50, myp5.WEBGL); + myp5.shininessTexture(img); + expect(myp5._renderer.states._shininessTex).toBe(img); + myp5.shininessTexture(null); + expect(myp5._renderer.states._shininessTex).toBeNull(); + }); + }); }); From 4342ebf8c94c4ae9211e8abe54dbc4cfecdafc47 Mon Sep 17 00:00:00 2001 From: nityam Date: Wed, 12 Aug 2026 13:01:22 +0530 Subject: [PATCH 8/8] rerun ci