hello大家好,最近看到我两三年前学习图形学的代码,感觉忘了不少,现在这里写文章回顾一下,这是之前写的成果 github.com/925558718/C…
本期先回顾一下如何画一个三角形
准备渲染环境canvas
const canvas = document.querySelector<HTMLCanvasElement>("#canvas")!;
const ctx = canvas.getContext("2d")!;
const buffer = new Uint8ClampedArray(500 * 500 * 4);
ctx.putImageData(new ImageData(buffer, 500, 500), 0, 0);
弄一个buffer,我们修改这个buffer,然后渲染到canvas里面
三角形数据定义
可以用一个数组保存三个顶点,如[p1,p2,p3]。
我们现在的canvas是500x500的,所以我们定义一个三角形为[[0,0],[250,500],[500,500]],这样它的包围盒就是整个canvas了
canvas的坐标系原点在左上角,所以我们的目标是渲染出一个尖头向左的三角形
画三角形的两种方式
扫描线画法
这个办法就是由上到下的竖直扫描,找出某一个水平线和三角形的两个交点,然后在遍历两个交点中的像素点并着色。 先排序一下顶点,确保高的顶点在前边
// 按 y 坐标排序顶点
const sortedVertices = [...points].sort((a, b) => a[1] - b[1]);
const [p0, p1, p2] = sortedVertices;
为了方便操作,我们对上下两个区域分别进行操作
开始遍历
// 求交点,其实也是算插值,已值中间点的y,求x分量
function interpolate(y: number, y0: number, x0: number, y1: number, x1: number): number {
if (y1 === y0) return x0;
return x0 + (x1 - x0) * (y - y0) / (y1 - y0);
}
function fillUpperTriangle() {
if (p1[1] === p0[1]) return; // 避免除零
for (let y = p0[1]; y <= p1[1]; y++) {
// 计算左右边界的 x 坐标
const x1 = interpolate(y, p0[1], p0[0], p1[1], p1[0]); // p0 到 p1 的边
const x2 = interpolate(y, p0[1], p0[0], p2[1], p2[0]); // p0 到 p2 的边
// 确保 xLeft < xRight
const xLeft = Math.min(x1, x2);
const xRight = Math.max(x1, x2);
// 填充该行
for (let x = Math.ceil(xLeft); x <= Math.floor(xRight); x++) {
setPixel(x, y, color[0], color[1], color[2], color[3]);
}
}
}
// 设置像素颜色的辅助函数
function setPixel(x: number, y: number, r: number, g: number, b: number, a: number) {
if (x < 0 || x >= 500 || y < 0 || y >= 500) return;
const index = (Math.floor(y) * 500 + Math.floor(x)) * 4;
buffer[index] = r; // Red
buffer[index + 1] = g; // Green
buffer[index + 2] = b; // Blue
buffer[index + 3] = a; // Alpha
}
下半部分也是类似的操作,遍历下半区域的就好
遍历+重心坐标
扫描线算法很快,但是能做的事有限,如果我想渲染一个彩色三角形阁下应该如何应对
渲染彩色三角形,可以规定三个顶点的颜色,可是中间的点的颜色如何定义,这时候就需要计算重心坐标
什么是重心坐标?
简单来说就是三角形中任一点 P=αP1+βP2+γP3 ,并且α+β+γ=1,α,β,γ都大于等于0,这样三个系数也能和颜色相乘,并算出该点的颜色插值。
如何计算重心坐标
面积法
点p连接其他三点构成三个区域,三个区域的面积在总面积的占比就是对应的系数 ,比如说点p3,他的系数就是Area(p,p2,p1)/Area(p1,p2,p3),正好是另外两点和点p区域的占总面积的百分比。
function getBarycentricCoordinates(px: number, py: number): [number, number, number] {
// 使用面积法计算重心坐标
const denominator = (p1[1] - p2[1]) * (p0[0] - p2[0]) + (p2[0] - p1[0]) * (p0[1] - p2[1]);
if (Math.abs(denominator) < 1e-10) {
return [0, 0, 0]; // 退化三角形
}
// 重心坐标 u, v, w (对应顶点 p0, p1, p2)
const u = ((p1[1] - p2[1]) * (px - p2[0]) + (p2[0] - p1[0]) * (py - p2[1])) / denominator;
const v = ((p2[1] - p0[1]) * (px - p2[0]) + (p0[0] - p2[0]) * (py - p2[1])) / denominator;
const w = 1 - u - v;
return [u, v, w];
}
然后开始遍历整个包围盒
// 遍历包围盒内的所有像素
for (let y = minY; y <= maxY; y++) {
for (let x = minX; x <= maxX; x++) {
// 计算当前像素的重心坐标
const [u, v, w] = getBarycentricCoordinates(x + 0.5, y + 0.5); // +0.5 用于像素中心采样
// 如果点在三角形内部,则填充
if (isInsideTriangle(u, v, w)) {
// 使用重心坐标插值计算颜色
const [r, g, b, a] = interpolateColor(u, v, w);
setPixel(x, y, r, g, b, a);
}
}
}
但是这样遍历包围盒每个点再计算重心比较费事,可以加一些优化判断点是否在三角形内再算重心坐标,这里不在赘述,直接看结果把。