Java软光栅渲染器-光栅化2D三角形

目标

光栅化2D三角形(scan conversion)。避免重复光栅化相邻三角形边界的像素(edge equation)。

  • 光栅化空心三角形
  • 光栅化实心三角形
  • 避免重复光栅化相邻三角形边界的像素

实现

继续为ImageRaster增加光栅化三角形的功能。

空心三角形

这一步几乎不需要做任何多余的事情,直接调用3次 drawLine 就行。

/**
 * 画空心三角形
 * 
 */
public void drawTriangle(int x0, int y0, int x1, int y1, int x2, int y2, ColorRGBA color) {
    drawLine(x0, y0, x1, y1, color);
    drawLine(x0, y0, x2, y2, color);
    drawLine(x2, y2, x1, y1, color);
}

实心三角形

这个功能参考:从零实现3D图像引擎:(15)三角形的光栅化

基本思路是,先把任意三角形转变成“平底三角形”或“平顶三角形”,然后逐行扫描划线。

平顶实心三角形的画法实现如下。

/**
 * 画平顶实心三角形
 */
private void fillTopLineTriangle(int x0, int y0, int x1, int y1, int x2, int y2, ColorRGBA color) {
    for (int y = y0; y <= y2; y++) {
        int xs, xe;
        xs = (int) ((y - y0) * (x2 - x0) / (y2 - y0) + x0 + 0.5);
        xe = (int) ((y - y1) * (x2 - x1) / (y2 - y1) + x1 + 0.5);
        drawLine(xs, y, xe, y, color);
    }
}

平底实心三角形的画法几乎一模一样,只是把y坐标改了改。

/**
 * 画平底实心三角形
 */
private void fillBottomLineTriangle(int x0, int y0, int x1, int y1, int x2, int y2, ColorRGBA color) {
    for (int y = y0; y <= y1; y++) {
        int xs, xe;
        xs = (int) ((y - y0) * (x1 - x0) / (y1 - y0) + x0 + 0.5);
        xe = (int) ((y - y0) * (x2 - x0) / (y2 - y0) + x0 + 0.5);

        drawLine(xs, y, xe, y, color);
    }
}

完整的实心三角形画法如下。

/**
 * 画实心三角形
 */
public void fillTriangle(int x0, int y0, int x1, int y1, int x2, int y2, ColorRGBA color) {
    if (y0 == y1) {
        if (y2 <= y0) // 平底
        {
            fillBottomLineTriangle(x2, y2, x0, y0, x1, y1, color);
        } else // 平顶
        {
            fillTopLineTriangle(x0, y0, x1, y1, x2, y2, color);
        }
    } else if (y0 == y2) {
        if (y1 <= y0) // 平底
        {
            fillBottomLineTriangle(x1, y1, x0, y0, x2, y2, color);
        } else // 平顶
        {
            fillTopLineTriangle(x0, y0, x2, y2, x1, y1, color);
        }
    } else if (y1 == y2) {
        if (y0 <= y1) // 平底
        {
            fillBottomLineTriangle(x0, y0, x1, y1, x2, y2, color);
        } else // 平顶
        {
            fillTopLineTriangle(x1, y1, x2, y2, x0, y0, color);
        }
    } else {
        int xtop = 0, ytop = 0, xmiddle = 0, ymiddle = 0, xbottom = 0, ybottom = 0;
        if (y0 < y1 && y1 < y2) // y1 y2 y3
        {
            xtop = x0;
            ytop = y0;
            xmiddle = x1;
            ymiddle = y1;
            xbottom = x2;
            ybottom = y2;
        } else if (y0 < y2 && y2 < y1) // y1 y3 y2
        {
            xtop = x0;
            ytop = y0;
            xmiddle = x2;
            ymiddle = y2;
            xbottom = x1;
            ybottom = y1;
        } else if (y1 < y0 && y0 < y2) // y2 y1 y3
        {
            xtop = x1;
            ytop = y1;
            xmiddle = x0;
            ymiddle = y0;
            xbottom = x2;
            ybottom = y2;
        } else if (y1 < y2 && y2 < y0) // y2 y3 y1
        {
            xtop = x1;
            ytop = y1;
            xmiddle = x2;
            ymiddle = y2;
            xbottom = x0;
            ybottom = y0;
        } else if (y2 < y0 && y0 < y1) // y3 y1 y2
        {
            xtop = x2;
            ytop = y2;
            xmiddle = x0;
            ymiddle = y0;
            xbottom = x1;
            ybottom = y1;
        } else if (y2 < y1 && y1 < y0) // y3 y2 y1
        {
            xtop = x2;
            ytop = y2;
            xmiddle = x1;
            ymiddle = y1;
            xbottom = x0;
            ybottom = y0;
        }
        int xl; // 长边在ymiddle时的x,来决定长边是在左边还是右边
        xl = (int) ((ymiddle - ytop) * (xbottom - xtop) / (ybottom - ytop) + xtop + 0.5);

        if (xl <= xmiddle) // 左三角形
        {
            // 画平底
            fillBottomLineTriangle(xtop, ytop, xl, ymiddle, xmiddle, ymiddle, color);

            // 画平顶
            fillTopLineTriangle(xl, ymiddle, xmiddle, ymiddle, xbottom, ybottom, color);
        } else // 右三角形
        {
            // 画平底
            fillBottomLineTriangle(xtop, ytop, xmiddle, ymiddle, xl, ymiddle, color);

            // 画平顶
            fillTopLineTriangle(xmiddle, ymiddle, xl, ymiddle, xbottom, ybottom, color);
        }
    }
}

测试用例

让我写个例子测试一下效果如何。

先定义一个2D三角形,实现Drawable接口。在draw方法中调用ImageRaster绘制三角形。

package net.jmecn.geom;

import net.jmecn.math.ColorRGBA;
import net.jmecn.renderer.ImageRaster;

/**
 * 代表一个三角形。
 * 
 * @author yanmaoyuan
 *
 */
public class Triangle2D implements Drawable {

    public int x0, y0;
    public int x1, y1;
    public int x2, y2;
    public ColorRGBA color = ColorRGBA.RED;
    public boolean isSolid = true;// 是否实心

    @Override
    public void draw(ImageRaster imageRaster) {
        if (isSolid) {
            imageRaster.fillTriangle(x0, y0, x1, y1, x2, y2, color);
        } else {
            imageRaster.drawTriangle(x0, y0, x1, y1, x2, y2, color);
        }
    }

}

再写个 Test2DTriangle 类来显示结果。

package net.jmecn.examples;

import java.util.Random;

import net.jmecn.Application;
import net.jmecn.geom.Triangle2D;
import net.jmecn.math.ColorRGBA;

/**
 * 绘制2D三角形
 * 
 * @author yanmaoyuan
 *
 */
public class Test2DTriangles extends Application {

    public static void main(String[] args) {
        Test2DTriangles app = new Test2DTriangles();
        app.setResolution(720, 405);
        app.setTitle("2D Triangles");
        app.setFrameRate(120);
        app.start();
    }

    /**
     * 初始化
     */
    @Override
    protected void initialize() {
        Random rand = new Random();

        /**
         * 随机生成三角形
         */
        for(int i=0; i<10; i++) {
            Triangle2D tri = new Triangle2D();
            tri.x0 = rand.nextInt(width);
            tri.y0 = rand.nextInt(height);
            tri.x1 = rand.nextInt(width);
            tri.y1 = rand.nextInt(height);
            tri.x2 = rand.nextInt(width);
            tri.y2 = rand.nextInt(height);
            tri.color = new ColorRGBA(rand.nextInt(0x4FFFFFFF));
            tri.isSolid = rand.nextFloat() > 0.5f;
            // 添加到场景中
            scene.add(tri);
        }
    }

    @Override
    protected void update(float delta) {
    }

}

效果如下:

总结

实现了基本的三角形光栅化,没有达成第三个目标,即解决“避免重复光栅化相邻三角形边界的像素”这个问题。

fillTriangle方法还有一些可以优化的余地,优化思路有:

  • 为了判断三角形是否有一条边是“平”的,可以先按y坐标对三个顶点进行排序;
  • 在画扫描线的时候,线段的两个端点坐标,还可以通过Bresenham算法来同步计算;
  • 避免重复光栅化相邻三角形边界的像素。

但是我现在对2D光栅化的实现已经有些腻了。关于这些算法,网上有大量的资料和可参考的代码,对我学习和理解这些算法的原理来说已经足够,没有必要再实现一遍。

其次,我想早点开始实现3D渲染管线。目前的2D光栅器在3D渲染阶段早晚要重做,所以干脆直接进入下一阶段吧!等完成了3D渲染之后,再回头来做这些优化也不迟。

原计划,2D阶段还有这些功能要做,现在放弃。

  • 光栅化2D多边形;
  • 顶点着色/颜色插值;
  • 绘制bmp图片;
  • 二维几何变换;
  • 按键控制窗口运动,改变观察范围。