diff --git a/src/webgl/material.js b/src/webgl/material.js
index 3fa865b64a..894785c8c6 100644
--- a/src/webgl/material.js
+++ b/src/webgl/material.js
@@ -2590,6 +2590,11 @@ function material(p5, fn) {
* `loadModel()` apply their own normal map from
* the `.mtl` file's `map_Bump`.
*
+ * Note: On a shape whose texture coordinates wrap all the way around, such as
+ * `sphere()`, the two edges of the image meet. The
+ * image has to tile for them to line up, otherwise a seam shows where they
+ * join.
+ *
* Note: `normalTexture()` can only be used in WebGL mode.
*
* @method normalTexture
@@ -2682,6 +2687,13 @@ function material(p5, fn) {
*
* A light source is needed to see the effect.
*
+ * Note: On a shape whose texture coordinates wrap all the way around, such as
+ * `sphere()`, the two edges of the image meet. The
+ * image has to tile for them to line up. Because a bump map is read by
+ * comparing neighbouring pixels, also call
+ * `textureWrap(REPEAT)` so those comparisons
+ * carry across the join instead of stopping at the edge.
+ *
* Note: `bumpTexture()` can only be used in WebGL mode.
*
* @method bumpTexture
diff --git a/src/webgl/shaders/phong.frag b/src/webgl/shaders/phong.frag
index 527826f460..64e39d0b2c 100644
--- a/src/webgl/shaders/phong.frag
+++ b/src/webgl/shaders/phong.frag
@@ -76,12 +76,17 @@ void main(void) {
vec3 mapN;
if (uNormalMapMode == 1) {
// bump map: brightness is height, so the tangent-space normal comes from
- // how fast that height changes between neighbouring texels.
- float h = TEXTURE(uNormalSampler, vTexCoord).r;
- float hu = TEXTURE(uNormalSampler, vTexCoord + vec2(uNormalTexelSize.x, 0.0)).r;
- float hv = TEXTURE(uNormalSampler, vTexCoord + vec2(0.0, uNormalTexelSize.y)).r;
+ // how fast that height changes between neighbouring texels. sampling both
+ // sides keeps the slope right at the edges of the map, where reaching past
+ // one side would otherwise clamp and read back the same texel.
+ vec2 du = vec2(uNormalTexelSize.x, 0.0);
+ vec2 dv = vec2(0.0, uNormalTexelSize.y);
+ float hl = TEXTURE(uNormalSampler, vTexCoord - du).r;
+ float hr = TEXTURE(uNormalSampler, vTexCoord + du).r;
+ float hd = TEXTURE(uNormalSampler, vTexCoord - dv).r;
+ float hu = TEXTURE(uNormalSampler, vTexCoord + dv).r;
// the surface leans away from the direction height increases in
- mapN = normalize(vec3(h - hu, h - hv, 1.0));
+ mapN = normalize(vec3((hl - hr) * 0.5, (hd - hu) * 0.5, 1.0));
} else {
// normal map: rgb already holds the tangent-space normal
mapN = TEXTURE(uNormalSampler, vTexCoord).rgb * 2.0 - 1.0;
diff --git a/src/webgpu/shaders/material.js b/src/webgpu/shaders/material.js
index 8541967642..5288c3b9cf 100644
--- a/src/webgpu/shaders/material.js
+++ b/src/webgpu/shaders/material.js
@@ -401,12 +401,17 @@ ${useTextureMaps ? ` if (material.uHasNormalMap == 1) {
var mapN: vec3;
if (material.uNormalMapMode == 1u) {
// bump map: brightness is height, so the tangent-space normal comes from
- // how fast that height changes between neighbouring texels.
- let h = textureSample(uNormalSampler, uNormalSampler_sampler, input.vTexCoord).r;
- let hu = textureSample(uNormalSampler, uNormalSampler_sampler, input.vTexCoord + vec2(material.uNormalTexelSize.x, 0.0)).r;
- let hv = textureSample(uNormalSampler, uNormalSampler_sampler, input.vTexCoord + vec2(0.0, material.uNormalTexelSize.y)).r;
+ // how fast that height changes between neighbouring texels. sampling both
+ // sides keeps the slope right at the edges of the map, where reaching past
+ // one side would otherwise clamp and read back the same texel.
+ let du = vec2(material.uNormalTexelSize.x, 0.0);
+ let dv = vec2(0.0, material.uNormalTexelSize.y);
+ let hl = textureSample(uNormalSampler, uNormalSampler_sampler, input.vTexCoord - du).r;
+ let hr = textureSample(uNormalSampler, uNormalSampler_sampler, input.vTexCoord + du).r;
+ let hd = textureSample(uNormalSampler, uNormalSampler_sampler, input.vTexCoord - dv).r;
+ let hu = textureSample(uNormalSampler, uNormalSampler_sampler, input.vTexCoord + dv).r;
// the surface leans away from the direction height increases in
- mapN = normalize(vec3(h - hu, h - hv, 1.0));
+ mapN = normalize(vec3((hl - hr) * 0.5, (hd - hu) * 0.5, 1.0));
} else {
// normal map: rgb already holds the tangent-space normal
mapN = textureSample(uNormalSampler, uNormalSampler_sampler, input.vTexCoord).rgb * 2.0 - 1.0;