Three.js高度图地形Triplanar Mapping纹理映射异常问题
Three.js 高度图地形三平面映射纹理渲染异常排查
在Three.js环境下,针对基于高度图数据生成的平面网格地形,尝试通过triplanar mapping(三平面映射)技术实现纹理映射时,渲染结果不符合预期,具体实现过程与异常如下:
初始实现逻辑
纹理加载代码
let diffuseTex; loader.load( "./resources/images/satellite.png", function (image) { diffuseTex = image; }, undefined, function () { console.error("An error happened."); } );
初始材质创建代码
加载得到的漫反射纹理会作为uniform传入RawShaderMaterial,绑定自定义顶点着色器与片元着色器:
var uniforms1 = { diffuseTexture: { type: "t", value: diffuseTex }, }; const _meshMaterial = new THREE.RawShaderMaterial({ uniforms: uniforms1, vertexShader: terrainShader._VS, fragmentShader: terrainShader._FS, });
自定义着色器代码
片元着色器内实现了基础的三平面映射逻辑:
// 顶点着色器 const _VS = `#version 300 es precision highp float; uniform mat4 modelMatrix; uniform mat4 modelViewMatrix; uniform mat4 projectionMatrix; in vec3 position; in vec3 normal; out vec3 vNormal; out vec3 vPosition; #define saturate(a) clamp( a, 0.0, 1.0 ) void main(){ vNormal = normal; vPosition = position.xyz; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); vPosition = vec3(gl_Position.xyz)*0.5 + 0.5; } `; // 片元着色器 const _FS = `#version 300 es precision mediump sampler2DArray; precision highp float; precision highp int; uniform mat4 modelMatrix; uniform mat4 modelViewMatrix; uniform vec3 cameraPosition; uniform sampler2D diffuseMap; in vec3 vNormal; in vec3 vPosition; out vec4 out_FragColor; vec3 blendNormal(vec3 normal){ vec3 blending = abs(normal); blending = normalize(max(blending, 0.00001)); blending /= vec3(blending.x + blending.y + blending.z); return blending; } vec3 triplanarMapping (sampler2D tex, vec3 normal, vec3 position) { vec3 normalBlend = blendNormal(normal); vec3 xColor = texture(tex, position.yz).rgb; vec3 yColor = texture(tex, position.xz).rgb; vec3 zColor = texture(tex, position.xy).rgb; return (xColor * normalBlend.x + yColor * normalBlend.y + zColor * normalBlend.z); } void main(){ vec3 color = triplanarMapping(diffuseMap, vNormal, vPosition); out_FragColor = vec4(color, 1.0); } `; export const terrainShader = { _VS, _FS };
地形网格创建代码
使用PlaneGeometry构建地形网格,写入高度图对应的顶点数据、计算顶点法线后,绑定前述自定义材质加入场景:
let geometry = new THREE.PlaneGeometry( partial.x, partial.y, partial.x - 1, partial.y - 1 ); geometry.setFromPoints(chunk[i]); geometry.computeVertexNormals(); let mesh = new THREE.Mesh(geometry, _meshMaterial); mesh.castShadow = false; mesh.receiveShadow = true; scene.add(mesh);
异常现象
- 初始版本运行时地形整体渲染为纯黑色,完全未呈现待映射的卫星纹理,与预期的纹理完整贴合地形的效果差距极大。
- 后续调整实现逻辑,将材质创建逻辑移入纹理加载的回调函数内,地形不再为全黑,但渲染效果依然不符合纹理正确映射的预期。
调整后的纹理回调内材质创建代码:
loader.load( "./resources/images/satellite.png", function (texture) { var uniforms1 = { diffuseTexture: { type: "t", value: texture }, }; _meshMaterial = new THREE.RawShaderMaterial({ uniforms: uniforms1, vertexShader: terrainShader._VS, fragmentShader: terrainShader._FS, });
核心问题点与修复方案
代码中存在4个直接导致渲染异常的问题:
- Uniform变量名不匹配:JS代码中传入的纹理uniform名为
diffuseTexture,但片元着色器中声明的采样器uniform名为diffuseMap,二者名称不一致会导致着色器无法获取正确的纹理数据,是初始版本全黑的核心原因。 - 采样坐标空间错误:顶点着色器中最终传给片元着色器的
vPosition是裁剪空间经过*0.5+0.5变换后的NDC坐标,和地形实际的局部/世界空间坐标范围不匹配,直接导致纹理采样坐标完全错乱。三平面映射采样需要使用局部空间或世界空间坐标,不能使用投影后的裁剪/NDC坐标。 - 坐标空间不统一:顶点着色器直接传递局部空间法线,若采样使用世界坐标,法线未通过法线矩阵转换到对应空间,会导致三平面权重计算错误。
- 纹理采样缩放缺失:直接使用顶点坐标作为UV采样,坐标尺度和纹理分辨率不匹配时会出现纹理拉伸、显示比例异常的问题,需要新增缩放系数控制纹理平铺次数。
参考修复代码
顶点着色器修改
const _VS = `#version 300 es precision highp float; uniform mat4 modelMatrix; uniform mat4 modelViewMatrix; uniform mat4 projectionMatrix; uniform mat3 normalMatrix; in vec3 position; in vec3 normal; out vec3 vNormal; out vec3 vPosition; void main(){ // 转换法线到世界空间 vNormal = normalize(normalMatrix * normal); // 传递世界空间坐标用于三平面采样 vPosition = (modelMatrix * vec4(position, 1.0)).xyz; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `;
片元着色器修改
const _FS = `#version 300 es precision highp float; uniform sampler2D diffuseTexture; // 新增纹理缩放参数,控制平铺密度 uniform float texScale; in vec3 vNormal; in vec3 vPosition; out vec4 out_FragColor; vec3 blendNormal(vec3 normal){ vec3 blending = abs(normal); blending = normalize(max(blending, 0.00001)); blending /= (blending.x + blending.y + blending.z); return blending; } vec3 triplanarMapping (sampler2D tex, vec3 normal, vec3 position, float scale) { vec3 scaledPos = position * scale; vec3 normalBlend = blendNormal(normal); vec3 xColor = texture(tex, scaledPos.yz).rgb; vec3 yColor = texture(tex, scaledPos.xz).rgb; vec3 zColor = texture(tex, scaledPos.xy).rgb; return (xColor * normalBlend.x + yColor * normalBlend.y + zColor * normalBlend.z); } void main(){ vec3 color = triplanarMapping(diffuseTexture, vNormal, vPosition, texScale); out_FragColor = vec4(color, 1.0); } `;
JS端逻辑补充
- 材质uniform新增缩放参数,新版Three.js不需要手动声明uniform类型,会自动识别:
var uniforms1 = { diffuseTexture: { value: texture }, texScale: { value: 0.02 } // 根据地形实际尺寸调整该值,直到纹理显示比例符合预期 };
- 纹理加载完成后设置重复包裹模式,支持纹理平铺:
texture.wrapS = THREE.RepeatWrapping; texture.wrapT = THREE.RepeatWrapping;
PlaneGeometry默认生成在XY平面、法线朝Z轴,作为地形使用时需要将网格绕X轴旋转-90度,让平面落在XZ平面、Y轴朝上,保证顶视卫星纹理正确映射到地形顶面。- 删除片元着色器中无用的
precision mediump sampler2DArray声明,避免不必要的兼容问题。
内容的提问来源于stack exchange,提问作者Pravin Poudel
相关产品推荐
相关产品推荐

