如何在Three.js中通过GLSL实现带高镜面高光的Phong着色
问题描述
我正尝试在Three.js中使用GLSL着色器渲染两个球体,实现自定义Phong着色。希望左侧绿色球体拥有带有锐利镜面反射的高光泽高光效果,但当前左侧球体过于偏向漫反射,缺乏足够的高光强度。要求仅修改顶点着色器与片元着色器,不改动其他代码,且保持片元着色器的参数值不变。
参考图效果:
当前效果:
已尝试操作:
- 提高matSh与matSpec参数值
- 调整srcSpec与srcAmbi参数值
预期效果:绿色球体呈现高镜面高光,具备光泽感与反射质感。
当前代码
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>phong shading</title> <style> body { margin: 0; } canvas { display: block; } </style> </head> <body> <script id="vertexShader" type="x-shader/x-vertex"> uniform mat4 modelViewMatrix; // optional when using ShaderMaterial uniform mat4 projectionMatrix; // optional when using ShaderMaterial uniform mat4 viewMatrix; // optional when using ShaderMaterial uniform mat4 modelMatrix; // optional when using ShaderMaterial uniform mat3 normalMatrix; uniform mat4 inverseTransposeM; uniform vec3 eyePos; attribute vec3 position; attribute vec3 normal; varying vec3 v_normal, v_view; void main() { v_normal = normal; gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4( position, 1.0 ); } </script> <script id="fragmentShader" type="x-shader/x-fragment"> precision mediump float; precision mediump int; vec3 matDiff, matSpec, matAmbi, matEmit; float matSh; vec3 srcDiff, srcSpec, srcAmbi; uniform vec3 lightDir; varying vec3 v_normal, v_view; void main() { // Material properties matDiff = vec3(0.0, 1.0, 0.0); // Diffuse color (green) matSpec = vec3(2.0, 2.0, 2.0); // Increase specular intensity matAmbi = vec3(0.0, 0.15, 0.0); // Slightly increased ambient intensity matSh = 500.0; // High shininess for a tight highlight // Light properties srcDiff = vec3(1.0, 1.0, 1.0); // Diffuse light color srcSpec = vec3(2.0, 2.0, 2.0); // Specular light color srcAmbi = vec3(1.0, 1.0, 1.0); // Ambient light color // Normalize the normal and light direction vec3 N = normalize(v_normal); vec3 L = normalize(lightDir); vec3 V = normalize(v_view); // View direction (towards the camera) vec3 R = reflect(-L, N); // Reflection direction // Ambient component vec3 ambient = matAmbi * srcAmbi; // Diffuse component float diffFactor = max(dot(N, L), 0.0); vec3 diffuse = matDiff * srcDiff * diffFactor; // Specular component float specFactor = pow(max(dot(R, V), 0.0), matSh); vec3 specular = matSpec * srcSpec * specFactor; // Combine components vec3 color = ambient + diffuse + specular; // Set fragment color gl_FragColor = vec4(color, 1.0); } </script> <script type="importmap"> { "imports": { "three": "https://unpkg.com/three@v0.157.0/build/three.module.js", "three/addons/": "https://unpkg.com/three@v0.157.0/examples/jsm/" } } </script> <script type="module"> import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; const renderer = new THREE.WebGLRenderer(); renderer.setSize( window.innerWidth, window.innerHeight ); document.body.appendChild( renderer.domElement ); const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 500 ); camera.position.set( 0, 3, -10 ); camera.lookAt( 0, 0, 0 ); const scene = new THREE.Scene(); scene.background = new THREE.Color('black'); const controls = new OrbitControls( camera, renderer.domElement ); const ambilight = new THREE.AmbientLight(0xFFFFFF, 0.1); scene.add(ambilight); const dlight = new THREE.DirectionalLight(0xFFFFFF, 1); dlight.position.set(5, 2, -5); dlight.target.position.set(0, 0, 0); scene.add(dlight); scene.add(dlight.target); const geom = new THREE.SphereGeometry( 1, 30, 20 ); const mat = new THREE.MeshPhongMaterial({ color: 0xFF0000, flatShading: false, }); const ball = new THREE.Mesh(geom,mat); scene.add(ball); ball.position.set(1.5,0,0); let m = new THREE.Matrix4(); let uniforms = { inverseTransposeM:{type:"mat4", value:m}, eyePos:{type:"vec3", value:camera.position}, lightDir:{type:"vec3", value:dlight.position} } const mat2 = new THREE.RawShaderMaterial({ uniforms: uniforms, vertexShader: document.getElementById( 'vertexShader' ).textContent, fragmentShader: document.getElementById( 'fragmentShader' ).textContent }); const ball2 = new THREE.Mesh(geom,mat2) scene.add(ball2); ball2.position.set(-1.5,0,0); ball2.material.uniforms.inverseTransposeM.value = ball2.matrixWorld.invert().transpose(); function animate(time) { controls.update(); requestAnimationFrame( animate ); renderer.render( scene, camera ); } animate(); </script> </body> </html>
解决方案
问题核心在于顶点着色器没有正确传递用于高光计算的关键变量:
- 原始法线未经过模型变换校正,导致光照计算时法线方向错误
v_view变量未初始化,片元着色器中使用的是无效值,直接导致高光失效
仅修改顶点着色器即可解决问题,片元着色器保持参数不变:
uniform mat4 modelViewMatrix; uniform mat4 projectionMatrix; uniform mat4 viewMatrix; uniform mat4 modelMatrix; uniform mat3 normalMatrix; uniform mat4 inverseTransposeM; uniform vec3 eyePos; attribute vec3 position; attribute vec3 normal; varying vec3 v_normal, v_view; void main() { // 将法线转换到世界空间并归一化,确保模型变换后法线方向正确 v_normal = normalize(mat3(inverseTransposeM) * normal); // 计算世界空间下的顶点位置 vec3 worldPos = (modelMatrix * vec4(position, 1.0)).xyz; // 计算视线方向(相机位置到顶点的向量) v_view = eyePos - worldPos; gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4( position, 1.0 ); }
修改说明:
- 用
inverseTransposeM矩阵将顶点法线转换到世界空间,避免模型缩放导致法线变形 - 计算世界空间顶点位置,进而得到正确的视线方向向量,传递给片元着色器用于高光计算
- 片元着色器中原本的高光计算逻辑是正确的,只要传入有效的
v_normal和v_view就能得到预期的锐利高光效果
内容的提问来源于stack exchange,提问作者peachi
相关产品推荐
相关产品推荐

