Canvas绘制三角形出现伪影的原因及解决方案疑问
问题描述
我已经检查Mesh、变换矩阵及所有能想到的内容十多次,伪影总是在time变量达到1800时出现(该变量每次setTimeout递增),我将初始time设为1700以便观察。请运行下方代码片段。
编辑:我找到了解决方案,但不清楚原理。若将fill语句移至绘制三角形后,三角形仍会残留!我仅将分开的rect、fill命令改为fillRect就修复了问题,这是为什么?
代码片段
JavaScript
/* classes */ class Vec3 { constructor(x = null, y = null, z = null) { this.x = x this.y = y this.z = z } } class Triangle { constructor(x1 = null, y1 = null, z1 = null, x2 = null, y2 = null, z2 = null, x3 = null, y3 = null, z3 = null) { this.v = [ new Vec3(x1, y1, z1), new Vec3(x2, y2, z2), new Vec3(x3, y3, z3), ] } } class Mesh { constructor(tris = []) { this.t = tris.map(t => new Triangle(...t)) } } class Mat4x4 { constructor(fn = () => {}) { this.m = [ [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], ] fn(this.m) } } /* helpers */ function degToRad(deg) { return deg * (math.pi / 180) } /* engine */ function matMultiplyVec3(mat, vec, vecP) { vecP.x = vec.x * mat.m[0][0] + vec.y * mat.m[1][0] + vec.z * mat.m[2][0] + mat.m[3][0] vecP.y = vec.x * mat.m[0][1] + vec.y * mat.m[1][1] + vec.z * mat.m[2][1] + mat.m[3][1] vecP.z = vec.x * mat.m[0][2] + vec.y * mat.m[1][2] + vec.z * mat.m[2][2] + mat.m[3][2] const w = vec.x * mat.m[0][3] + vec.y * mat.m[1][3] + vec.z * mat.m[2][3] + mat.m[3][3] if (w !== 0) { vecP.x /= w; vecP.y /= w; vecP.z /= w; } // const m1 = math.matrix(mat.m) // const m2 = math.matrix([[vec.x], [vec.y], [vec.z], [1]]) // const [[xP], [yP], [zP], [w]] = math.multiply(m1, m2).toArray() // vecP.x = xP; vecP.y = yP; vecP.z = zP // if (w != 0) { // vecP.x /= w; vecP.y /= w; vecP.z /= w // } } /* canvas */ function setupCanvas(width, height) { const c = document.getElementById('canvas') c.setAttribute('width', width) c.setAttribute('height', height) c.style.width = `${width}px` c.style.height = `${height}px` const ctx = c.getContext('2d') return { c, ctx, width, height } } function fill(ctx, x, y, width, height, color) { ctx.rect(x, y, width, height) ctx.fillStyle = color ctx.fill() } function drawTriangle(ctx, tri, color) { ctx.strokeStyle = color ctx.lineWidth = 1 ctx.beginPath() ctx.moveTo(tri.v[0].x, tri.v[0].y) ctx.lineTo(tri.v[1].x, tri.v[1].y) ctx.lineTo(tri.v[2].x, tri.v[2].y) ctx.stroke() } /* main */ function main() { // canvas setup const { ctx, width, height } = setupCanvas(600, 337) // mesh const cubeMesh = new Mesh([ [-1,-1,1, 1,-1,1, 1,1,1], [-1,-1,1,1,-1,1,1,1,1], [-1,-1,1,1,1,1,-1,1,1], [1,-1,1,1,-1,-1,1,1,-1], [1,-1,1,1,1,-1,1,1,1], [1,-1,-1,-1,-1,-1,-1,1,-1], [1,-1,-1,-1,1,-1,1,1,-1], [-1,-1,-1,-1,-1,1,-1,1,1], [-1,-1,-1,-1,1,1,-1,1,-1], [-1,1,1,1,1,1,1,1,-1], [-1,1,1,1,1,-1,-1,1,-1], [1,-1,1,-1,-1,-1,1,-1,-1], [1,-1,1,-1,-1,1,-1,-1,-1], ]) // projection matrix const zNear = 0.1 const zFar = 1000 const fov = 90 const scalingFactor = 1 / math.tan(degToRad(fov) / 2) const aspectRatio = height / width const matProj = new Mat4x4(m => { m[0][0] = aspectRatio * scalingFactor m[1][1] = scalingFactor m[2][2] = zFar / (zFar - zNear) m[3][2] = (-zFar * zNear) / (zFar - zNear) m[2][3] = 1 }) // game loop let time = 1500 const update = () => { // clear screen fill(ctx, 0, 0, width, height, 'gray') // rotation matrices const theta = time * 0.005 const matRotZ = new Mat4x4(m => { m[0][0] = math.cos(theta) m[0][1] = math.sin(theta) m[1][0] = -math.sin(theta) m[1][1] = math.cos(theta) m[2][2] = 1 m[3][3] = 1 }) const matRotX = new Mat4x4(m => { m[0][0] = 1 m[1][1] = math.cos(theta / 2) m[1][2] = math.sin(theta / 2) m[2][1] = -math.sin(theta / 2) m[2][2] = math.cos(theta / 2) m[3][3] = 1 }) // draw mesh cubeMesh.t.forEach(tri => { // rotate Z const triRZ = new Triangle matMultiplyVec3(matRotZ, tri.v[0], triRZ.v[0]) matMultiplyVec3(matRotZ, tri.v[1], triRZ.v[1]) matMultiplyVec3(matRotZ, tri.v[2], triRZ.v[2]) // rotate X const triRZX = new Triangle matMultiplyVec3(matRotX, triRZ.v[0], triRZX.v[0]) matMultiplyVec3(matRotX, triRZ.v[1], triRZX.v[1]) matMultiplyVec3(matRotX, triRZ.v[2], triRZX.v[2]) // offset z a bit const triT = triRZX triT.v[0].z = triRZX.v[0].z + 3 triT.v[1].z = triRZX.v[1].z + 3 triT.v[2].z = triRZX.v[2].z + 3 // project const triP = new Triangle matMultiplyVec3(matProj, triT.v[0], triP.v[0]) matMultiplyVec3(matProj, triT.v[1], triP.v[1]) matMultiplyVec3(matProj, triT.v[2], triP.v[2]) // scale into view triP.v[0].x += 1; triP.v[0].y += 1 triP.v[1].x += 1; triP.v[1].y += 1 triP.v[2].x += 1; triP.v[2].y += 1 triP.v[0].x = triP.v[0].x / 2 * width triP.v[0].y = triP.v[0].y / 2 * height triP.v[1].x = triP.v[1].x / 2 * width triP.v[1].y = triP.v[1].y / 2 * height triP.v[2].x = triP.v[2].x / 2 * width triP.v[2].y = triP.v[2].y / 2 * height // rasterize drawTriangle(ctx, triP, 'white') }) } setInterval(() => { time += 1 update() }, 10) } main()
HTML
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Cube</title> <style> body { margin: 20px; } </style> </head> <body> <canvas id="canvas"></canvas> <script src="https://cdnjs.cloudflare.com/ajax/libs/mathjs/11.4.0/math.js"></script> </body> </html>
问题解答
问题核心是Canvas的路径状态累积:
你原来的
fill函数用ctx.rect()把矩形添加到当前路径,再调用ctx.fill()填充,但没有调用ctx.beginPath()重置路径——这意味着每次循环调用fill时,都会把新矩形追加到之前的路径里,路径会随着循环次数越来越庞大。当
time到1800时,计算出的旋转角度theta会让三角形变换坐标出现细微数值精度误差,再加上累积了几百次的路径,就会触发渲染异常产生伪影;哪怕把填充放到绘制三角形之后,残留的累积路径也会影响下一次渲染。而
fillRect()是独立的Canvas绘制API,它不需要依赖当前路径——调用时会自动创建全新的矩形路径,填充后不会保留该路径,自然不会出现路径累积的问题,伪影和残留现象也就解决了。
如果不想改用fillRect,也可以修改原fill函数,在ctx.rect()前加ctx.beginPath(),确保每次填充都基于全新路径:
function fill(ctx, x, y, width, height, color) { ctx.beginPath() // 添加此行重置路径 ctx.rect(x, y, width, height) ctx.fillStyle = color ctx.fill() }
内容的提问来源于stack exchange,提问作者Tallboy
相关产品推荐
相关产品推荐

