diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 96768d08ea..57fb3f55e3 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -151,6 +151,8 @@ export class Renderer3D extends Renderer { this.states._specularTex = null; 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; @@ -295,7 +297,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( @@ -640,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 normalTexture() (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 @@ -702,6 +727,12 @@ export class Renderer3D extends Renderer { if (partState.shininessTexture) { this.states.setValue('_shininessTex', partState.shininessTexture); } + if (partState.normalTexture) { + this.states.setValue('_normalTex', partState.normalTexture); + if (partState.normalScale != null) { + this.states.setValue('_normalScale', partState.normalScale); + } + } } _drawStrokes(geometry, { count } = {}) { @@ -1483,13 +1514,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 +1633,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 +1648,10 @@ 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('uNormalScale', this.states._normalScale); } fillShader.setUniform( 'uTint', diff --git a/src/webgl/loading.js b/src/webgl/loading.js index 603a75afaa..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,6 +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; + // a -bm multiplier scales the bump strength; defaults to 1 when omitted + if (material.bumpScale != null) state.normalScale = material.bumpScale; + } return state; } @@ -126,7 +137,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 +185,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 +203,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 +834,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/material.js b/src/webgl/material.js index 5c9f5e8809..c130ecca90 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -2566,6 +2566,124 @@ function material(p5, fn) { return this; }; + /** + * Sets a normal map to add surface detail to shapes under lighting. + * + * `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 `normalTexture(null)` to turn it off, or scope it between + * push() and pop(). + * + * @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=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.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; + }; + /** * Changes the coordinate system used for textures when they’re applied to * custom shapes. @@ -3819,6 +3937,29 @@ function material(p5, fn) { this.states.setValue('fillColor', new Color([1, 1, 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/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index ecde4d75e9..5f7dfeff78 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -42,6 +42,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 = []; @@ -254,6 +259,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) { @@ -1260,6 +1266,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 93f4fa6665..2edd05e478 100644 --- a/src/webgl/p5.GeometryPart.js +++ b/src/webgl/p5.GeometryPart.js @@ -16,7 +16,9 @@ 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 + normalScale: 1 // map_Bump -bm -> bump strength multiplier }; } @@ -86,6 +88,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..b58221ba11 100644 --- a/src/webgl/shaders/phong.frag +++ b/src/webgl/shaders/phong.frag @@ -22,12 +22,18 @@ uniform sampler2D uAmbientSampler; uniform bool uHasAmbientTex; uniform sampler2D uShininessSampler; uniform bool uHasShininessTex; +uniform sampler2D uNormalSampler; +uniform bool uHasNormalMap; +uniform float uNormalScale; #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 +62,22 @@ 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; + // scale the tangent-space slope so the bump strength can be tuned (-bm) + mapN.xy *= uNormalScale; + 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(); 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..9df038a87f 100644 --- a/src/webgpu/shaders/material.js +++ b/src/webgpu/shaders/material.js @@ -12,6 +12,8 @@ struct MaterialUniforms { uSpecular: u32, uShininess: f32, uMetallic: f32, + uHasNormalMap: u32, + uNormalScale: f32, } // Group 0: Lighting @@ -59,12 +61,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 +77,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 +105,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 +113,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 +135,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 +143,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 +160,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 +389,20 @@ 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; + 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( + N, input.vTexCoord, material.uAmbientColor, select(color.rgb, material.uAmbientMatColor.rgb, material.uHasSetAmbient == 1), 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 87b40d9aab..3d143fa013 100644 --- a/test/unit/io/loadModel.js +++ b/test/unit/io/loadModel.js @@ -117,6 +117,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 d39f90e863..3880bcb7a2 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 () { @@ -120,4 +135,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 2c3c92ebca..76896c3a56 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(); + } + ); visualTest( 'multi-material OBJ renders each material part', async function (p5, screenshot) { @@ -428,6 +445,38 @@ visualSuite('WebGL', function () { screenshot(); } ); + + visualTest( + '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 + 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.normalTexture(nmap); + p5.sphere(20); + screenshot(); + } + ); + visualTest( 'a slice with a missing texture falls back to its colour', async function (p5, screenshot) { @@ -445,6 +494,7 @@ visualSuite('WebGL', function () { screenshot(); } ); + visualTest( 'a 12-material OBJ renders every material', async function (p5, screenshot) { @@ -459,6 +509,7 @@ visualSuite('WebGL', function () { screenshot(); } ); + visualTest( 'a single-material OBJ renders through the part path', async function (p5, screenshot) { diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index 1e341bc9b3..d81d51ca7a 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -2029,4 +2029,50 @@ 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(); + } + ); + + visualTest( + '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); + 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.normalTexture(nmap); + p5.sphere(20); + 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 0000000000..71e09efd5b Binary files /dev/null and b/test/unit/visual/screenshots/WebGL/3DModel/a normal-mapped sphere shows surface detail under light/000.png differ diff --git a/test/unit/visual/screenshots/WebGL/3DModel/a normal-mapped sphere shows surface detail under light/metadata.json b/test/unit/visual/screenshots/WebGL/3DModel/a 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/WebGL/3DModel/normalTexture() 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 new file mode 100644 index 0000000000..7fa62a9a8a Binary files /dev/null and b/test/unit/visual/screenshots/WebGL/3DModel/normalTexture() adds surface detail to a built shape/000.png differ diff --git a/test/unit/visual/screenshots/WebGL/3DModel/normalTexture() 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 new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/3DModel/normalTexture() 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/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 0000000000..425c6c732c Binary files /dev/null and b/test/unit/visual/screenshots/WebGPU/3D Materials/a normal-mapped sphere shows surface detail under light/000.png differ diff --git a/test/unit/visual/screenshots/WebGPU/3D Materials/a normal-mapped sphere shows surface detail under light/metadata.json b/test/unit/visual/screenshots/WebGPU/3D Materials/a normal-mapped sphere shows surface detail under light/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGPU/3D Materials/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/normalTexture() 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 new file mode 100644 index 0000000000..c03b225f13 Binary files /dev/null and b/test/unit/visual/screenshots/WebGPU/3D Materials/normalTexture() adds surface detail to a built shape/000.png differ diff --git a/test/unit/visual/screenshots/WebGPU/3D Materials/normalTexture() 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 new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGPU/3D Materials/normalTexture() 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/webgl/p5.Geometry.js b/test/unit/webgl/p5.Geometry.js index 7c3ab813c4..e1848231c7 100644 --- a/test/unit/webgl/p5.Geometry.js +++ b/test/unit/webgl/p5.Geometry.js @@ -15,6 +15,56 @@ suite('p5.Geometry', function () { myp5.remove(); }); + suite('computeTangents', function () { + test('a uv-mapped triangle gets a +u tangent with correct handedness', + function () { + const geom = new p5.Geometry(); + // triangle in the xy plane, facing +z, uvs aligned to x (u) and y (v) + geom.vertices.push( + myp5.createVector(0, 0, 0), + myp5.createVector(1, 0, 0), + myp5.createVector(0, 1, 0) + ); + geom.uvs.push(0, 0, 1, 0, 0, 1); + geom.vertexNormals.push( + myp5.createVector(0, 0, 1), + myp5.createVector(0, 0, 1), + myp5.createVector(0, 0, 1) + ); + geom.faces.push([0, 1, 2]); + + geom.computeTangents(); + + // 4 components per vertex (x, y, z, handedness) + expect(geom.vertexTangents.length).toEqual(12); + // tangent points along +u (the +x direction here), handedness +1 + for (let i = 0; i < 3; i++) { + expect(geom.vertexTangents[i * 4]).toBeCloseTo(1, 5); + expect(geom.vertexTangents[i * 4 + 1]).toBeCloseTo(0, 5); + expect(geom.vertexTangents[i * 4 + 2]).toBeCloseTo(0, 5); + expect(geom.vertexTangents[i * 4 + 3]).toEqual(1); + } + } + ); + + test('no uvs means no tangents', function () { + const geom = new p5.Geometry(); + geom.vertices.push( + myp5.createVector(0, 0, 0), + myp5.createVector(1, 0, 0), + myp5.createVector(0, 1, 0) + ); + geom.vertexNormals.push( + myp5.createVector(0, 0, 1), + myp5.createVector(0, 0, 1), + myp5.createVector(0, 0, 1) + ); + geom.faces.push([0, 1, 2]); + geom.computeTangents(); + expect(geom.vertexTangents.length).toEqual(0); + }); + }); + suite('generating edge geometry', function () { let geom; diff --git a/test/unit/webgl/p5.GeometryPart.js b/test/unit/webgl/p5.GeometryPart.js index 9bb22354f7..c7fbe074df 100644 --- a/test/unit/webgl/p5.GeometryPart.js +++ b/test/unit/webgl/p5.GeometryPart.js @@ -43,7 +43,9 @@ suite('p5.GeometryPart', function () { texture: null, specularTexture: null, ambientTexture: null, - shininessTexture: null + shininessTexture: null, + normalTexture: null, + normalScale: 1 }); }); 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(); + }); + }); });