JavaScript 3D渲染原理咨询 附旋转透视立方体示例代码
3D渲染核心思路讲解
我正在出于兴趣用JavaScript开发一款3D物理引擎,目前卡在3D渲染环节,不清楚具体实现逻辑,希望有人能讲解3D渲染的核心思路。我自己写了一段旋转的3D透视立方体代码,但对背后的渲染原理理解不透彻,之前搜了很多文章都没找到有用的内容。
3D渲染的核心步骤拆解
1. 3D空间顶点变换
所有3D物体的顶点首先在局部空间(物体自身的坐标系,比如立方体的顶点从(0,0,0)到(size,size,size))定义,之后需要通过变换把顶点转到世界空间:
- 旋转变换:通过三角函数计算顶点绕X/Y/Z轴旋转后的坐标,你代码里的
rotateX/rotateY/rotateZ函数就是干这个的,本质是用极坐标转换实现旋转。 - 平移变换:把物体的局部顶点加上物体的世界位置
pos,让物体出现在世界空间的对应位置。
2. 透视投影(3D转2D)
这是把3D坐标映射到2D屏幕的关键,核心是近大远小的透视原理:
- 首先定义视场角(FOV):你代码里的
fov就是水平和垂直方向的视场角,决定了相机能看到的范围。 - 计算相机到投影平面的距离(dist):通过屏幕宽度/高度和视场角的正切值计算,公式是
dist = (屏幕半宽) / tan(FOV/2),这个值是透视缩放的基准。 - 顶点投影计算:对于每个3D顶点,根据它的Z轴距离(离相机的远近),计算在2D屏幕上的坐标:
你代码里的屏幕X = (投影平面半宽 - 顶点X) * 画布宽度 / 投影平面宽度 屏幕Y = (投影平面半高 - 顶点Y) * 画布高度 / 投影平面高度x和y计算就是这个逻辑,本质是用相似三角形把3D顶点按距离缩放后映射到画布像素。
3. 图元绘制与隐藏面处理
把投影后的2D顶点连接成面(三角形、四边形)或者线条:
- 你代码里通过遍历顶点组合绘制三角形和线段,但现在的问题是没有处理隐藏面,会导致后面的面挡住前面的面,看起来混乱。解决方法有两种:
- 深度排序:把所有面按离相机的距离排序,先画远的面,再画近的面,覆盖掉远的面。
- 深度缓冲:为每个屏幕像素记录当前最前面的深度值,绘制新面时只更新深度更小(更近)的像素。
4. 动画渲染循环
用requestAnimationFrame创建循环,每次循环:
- 清空画布(
clearRect) - 更新物体的变换(比如你的立方体每次旋转1度)
- 重新计算所有顶点的投影位置
- 绘制所有图元
你提供的旋转立方体完整代码
<style> *{ margin: 0; } canvas { background: black; } </style> <canvas></canvas> <script> var canvas = document.querySelector("canvas") var fov = [120 * Math.PI / 180, 90 * Math.PI / 180] canvas.width = innerWidth var dist = (canvas.width / 2) / Math.tan(fov[0] / 2) canvas.height = (Math.tan(fov[1] / 2) * dist) * 2 if (canvas.height > innerHeight) { canvas.height = innerHeight var dist = (canvas.height / 2) / Math.tan(fov[1] / 2) canvas.width = (Math.tan(fov[0] / 2) * dist) * 2 } var c = canvas.getContext("2d") c.fillStyle = "red" c.strokeStyle = "white" var solids = [] function rotateX(vector, angle) { angle = angle * Math.PI / 180 var l = Math.hypot(vector.y, vector.z) var cAngle = Math.atan2(vector.z, vector.y) cAngle += angle vector.y = Math.cos(cAngle) * l vector.z = Math.sin(cAngle) * l } function rotateY(vector, angle) { angle = angle * Math.PI / 180 var l = Math.hypot(vector.x, vector.z) var cAngle = Math.atan2(vector.z, vector.x) cAngle += angle vector.x = Math.cos(cAngle) * l vector.z = Math.sin(cAngle) * l } function rotateZ(vector, angle) { angle = angle * Math.PI / 180 var l = Math.hypot(vector.x, vector.y) var cAngle = Math.atan2(vector.y, vector.x) cAngle += angle vector.x = Math.cos(cAngle) * l vector.y = Math.sin(cAngle) * l } class Vertex { constructor(x, y, z) { this.x = x, this.y = y, this.z = z } } class Cube { constructor(pos, size) { this.size = size this.pos = pos this.angleX = 0 this.angleY = 0 this.angleZ = 30 this.vertices = [] this.vertices.push(new Vertex(0, 0, 0)) this.vertices.push(new Vertex(size, 0, 0)) this.vertices.push(new Vertex(0, size, 0)) this.vertices.push(new Vertex(0, 0, size)) this.vertices.push(new Vertex(size, size, 0)) this.vertices.push(new Vertex(size, 0, size)) this.vertices.push(new Vertex(0, size, size)) this.vertices.push(new Vertex(size, size, size)) } draw() { this.vertices.forEach(v => { rotateX(v, 1) rotateY(v, 1) rotateZ(v, 1) var d = v.z + this.pos.z var width = (Math.tan(fov[0] / 2) * d) * 2 var height = (Math.tan(fov[1] / 2) * d) * 2 var x = (width / 2 - v.x - this.pos.x) * canvas.width / width var y = (height / 2 - v.y - this.pos.y) * canvas.height / height this.vertices.forEach(v2 => { var d2 = v2.z + this.pos.z var width2 = (Math.tan(fov[0] / 2) * d2) * 2 var height2 = (Math.tan(fov[1] / 2) * d2) * 2 var x2 = (width2 / 2 - v2.x - this.pos.x) * canvas.width / width2 var y2 = (height2 / 2 - v2.y - this.pos.y) * canvas.height / height2 this.vertices.forEach(v3 => { if (v != v2 && v2 != v3) { var d3 = v3.z + this.pos.z var width3 = (Math.tan(fov[0] / 2) * d3) * 2 var height3 = (Math.tan(fov[1] / 2) * d3) * 2 var x3 = (width3 / 2 - v3.x - this.pos.x) * canvas.width / width3 var y3 = (height3 / 2 - v3.y - this.pos.y) * canvas.height / height3 c.beginPath() c.moveTo(x, y) c.lineTo(x2, y2) c.lineTo(x3, y3) c.lineTo(x, y) c.fillStyle = "red" c.fill() } }) }) }) this.vertices.forEach(v => { var d = v.z + this.pos.z var width = (Math.tan(fov[0] / 2) * d) * 2 var height = (Math.tan(fov[1] / 2) * d) * 2 var x = (width / 2 - v.x - this.pos.x) * canvas.width / width var y = (height / 2 - v.y - this.pos.y) * canvas.height / height this.vertices.forEach(v2 => { var d2 = v2.z + this.pos.z var width2 = (Math.tan(fov[0] / 2) * d2) * 2 var height2 = (Math.tan(fov[1] / 2) * d2) * 2 var x2 = (width2 / 2 - v2.x - this.pos.x) * canvas.width / width2 var y2 = (height2 / 2 - v2.y - this.pos.y) * canvas.height / height2 if (Math.round(Math.hypot(v.x - v2.x, v.y - v2.y, v.z - v2.z)) == this.size) { var d2 = v2.z + this.pos.z var width2 = (Math.tan(fov[0] / 2) * d2) * 2 var height2 = (Math.tan(fov[1] / 2) * d2) * 2 var x2 = (width2 / 2 - v2.x - this.pos.x) * canvas.width / width2 var y2 = (height2 / 2 - v2.y - this.pos.y) * canvas.height / height2 c.beginPath() c.moveTo(x, y) c.lineTo(x2, y2) c.stroke() } }) c.beginPath() c.arc(x, y, 3, 0, Math.PI * 2, false) c.fillStyle = "blue" //c.fill() //c.stroke() }) } } solids.push(new Cube({x: 0, y: 0, z: 300}, 50)) function animate() { requestAnimationFrame(animate) c.clearRect(0, 0, canvas.width, canvas.height) solids.forEach(solid => { solid.draw() }) } animate() </script>
内容的提问来源于stack exchange,提问作者Mohammed Mostafa
相关产品推荐
相关产品推荐

