Three.js片元着色器中NDC坐标转世界坐标及鼠标平面画圆实现
如何将NDC坐标转换为世界坐标并绘制光标圆形
首先,咱们得明确:NDC(标准化设备坐标)是渲染管线最后一步的坐标,要转成世界坐标,本质上是逆着渲染管线的变换流程走——也就是把NDC坐标通过逆视图投影矩阵(或分开的逆投影+逆视图矩阵)转换回去。下面分两种高效的方案来实现:
方案1:CPU端直接转换(推荐,更高效)
既然你已经拿到了鼠标的NDC坐标,完全可以在CPU端直接把它转换成世界坐标,再传到片元着色器里,这样不用在每个片元里做矩阵运算,性能更好。
代码示例(以Three.js为例,其他引擎逻辑类似)
// 假设你有camera实例(PerspectiveCamera/OrthographicCamera) const ndcPoint = new THREE.Vector3( this._currentPoint.x, this._currentPoint.y, 0.5 // 这里的z值对应屏幕深度:0.0是近平面,1.0是远平面,0.5是中间 ); // 用相机的unproject方法直接转世界坐标 const worldCursorPos = ndcPoint.unproject(camera); // 把世界坐标传到片元着色器的uniform里 this._cursorMaterial.uniforms.uBrushWorldPos.value = worldCursorPos;
然后在着色器里直接用这个世界坐标计算距离:
顶点着色器(需要传递片元的世界坐标)
uniform mat4 modelMatrix; varying vec3 vWorldPos; void main() { vec4 worldPos = modelMatrix * vec4(position, 1.0); vWorldPos = worldPos.xyz; gl_Position = projectionMatrix * viewMatrix * worldPos; }
片元着色器
uniform vec3 uBrushWorldPos; // CPU传过来的光标世界坐标 varying vec3 vWorldPos; // 片元自身的世界坐标 void main() { // 计算片元到光标世界位置的距离 float distanceToCursor = length(vWorldPos - uBrushWorldPos); // 世界空间下的圆形半径(根据你的需求调整) float circleRadius = 0.2; if (distanceToCursor < circleRadius) { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 红色光标圆 } else { // 原渲染逻辑 gl_FragColor = ...; } }
方案2:着色器内通过逆矩阵转换
如果需要在着色器里动态处理坐标转换(比如实时调整相机参数),可以把逆视图投影矩阵传到着色器,在片元里完成转换:
CPU端准备矩阵
// 计算视图投影矩阵的逆矩阵 const invViewProj = new THREE.Matrix4() .multiplyMatrices(camera.projectionMatrixInverse, camera.matrixWorld); // 传递uniform:NDC光标坐标 + 逆视图投影矩阵 this._cursorMaterial.uniforms.uBrushNdcPos.value = this._currentPoint; this._cursorMaterial.uniforms.uInvViewProj.value = invViewProj;
片元着色器内转换
uniform vec2 uBrushNdcPos; uniform mat4 uInvViewProj; varying vec3 vWorldPos; void main() { // 把2D NDC转成齐次裁剪坐标(z设为0.5对应屏幕中间深度,w=1.0) vec4 clipPos = vec4(uBrushNdcPos.x, uBrushNdcPos.y, 0.5, 1.0); // 乘以逆矩阵得到世界坐标,再做齐次除法 vec4 worldCursorPos = uInvViewProj * clipPos; worldCursorPos.xyz /= worldCursorPos.w; // 后续绘制圆形的逻辑和方案1一致 float distanceToCursor = length(vWorldPos - worldCursorPos.xyz); float circleRadius = 0.2; if (distanceToCursor < circleRadius) { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); } else { gl_FragColor = ...; } }
关键注意点
- 深度值的选择:如果是透视相机,NDC的z值决定了光标在世界空间的深度,你可以根据需求调整(比如设为0.0对应近平面,1.0对应远平面)。
- 矩阵顺序:逆视图投影矩阵的计算是
投影矩阵逆 × 视图矩阵逆(因为渲染时是投影 × 视图 × 模型,逆变换要反过来)。 - 模型矩阵:如果你的物体有模型变换(缩放、平移、旋转),顶点着色器里一定要用
modelMatrix计算世界坐标,不然距离计算会出错。
内容的提问来源于stack exchange,提问作者Nicolas
相关产品推荐
相关产品推荐

