Java软光栅渲染器-渲染状态

目标

考虑一个问题:如果我想控制物体渲染的方式,例如把背面剔除改成正面剔除,把三角形的填充模式改成线框模式,应该怎么办?

请看下图:

目前我想分别渲染出这5张不同的效果图,只能先去修改源代码,然后运行,再改源码,再运行,再改源码,再运行,再改源码,再运行...

好麻烦啊。

一种比较合理的方式,是在渲染管线中增加一些状态,根据Geometry的状态值来决定如何渲染它。本文的目标就是实现渲染状态管理,使每一个Geometry都可以用不同的方式来渲染。

实现

渲染状态与每个具体的Geometry有关,用于控制如何渲染它。它应该和Material放在一起,同样负责Geometry的外观。

渲染状态主要在渲染管线中使用。在我的程序中,Renderer需要使用它来决定如何进行背面剔除,SoftwareRaster需要用它来决定如何光栅化三角形。

定义RenderState

创建RenderState类。

package net.jmecn.material;

/**
 * 渲染状态
 * 
 * @author yanmaoyuan
 *
 */
public class RenderState {

    /**
     * 剔除模式
     */
    public enum CullMode {
        NEVER,  // 不剔除
        FACE,   // 剔除正面
        BACK,   // 剔除背面
        ALWAYS  // 完全剔除
    }

    /**
     * 填充模式
     */
    public enum FillMode {
        POINT,  // 点填充模式,在每个顶点绘制一个像素
        LINE,   // 线框模式,在每个边绘制一条直线
        FACE,   // 面模式,对每个面进行填充
    }
    
    private CullMode cullMode;
    private FillMode fillMode;

    public RenderState() {
        fillMode = FillMode.FACE;
        cullMode = CullMode.BACK;
    }

    public FillMode getFillMode() {
        return fillMode;
    }

    public void setFillMode(FillMode fillMode) {
        this.fillMode = fillMode;
    }

    public CullMode getCullMode() {
        return cullMode;
    }

    public void setCullMode(CullMode faceCullMode) {
        this.cullMode = faceCullMode;
    }

}

在这个类中,我定义了两个枚举类型:

CullMode 用于描述面的剔除方式。默认值是BACK,即背面剔除。

  • CullMode.NEVER 不剔除,正面和背面都能看到。
  • CullMode.FACE 剔除正面,只有表面法线背向摄像机的面可见。
  • CullMode.BACK 剔除背面,只有表面法线朝向摄像机的面可见。
  • CullMode.ALWAYS 正反面都剔除,这个面彻底看不见了。

FillMode 用于描述光栅化阶段三角形的填充方式。默认值是FACE,即填充实心三角形。

  • FillMode.POINT 点模式,只光栅化三角形的顶点。
  • FillMode.LINE 线框模式,只光栅化三角形的边。
  • FillMode.FACE 面模式,光栅化实心三角形。

本文开头给出的5个效果图,分别是这样组合的:

图一:CullMode.FACE, FillMode.LINE

图二:CullMode.BACK, FillMode.LINE

图三:CullMode.NEVER, FillMode.LINE

图四:CullMode.FACE, FillMode.FACE

图五:CullMode.BACK, FillMode.FACE

改进Material

在Material中增加RenderState成员,这样创建Geometry时就可以通过Material来调整RenderState中的状态值。

package net.jmecn.material;

/**
 * 材质
 * 
 * @author yanmaoyuan
 *
 */
public class Material {

    protected RenderState renderState;

    public Material() {
        renderState = new RenderState();
    }

    public RenderState getRenderState() {
        return renderState;
    }

    public void setRenderState(RenderState renderState) {
        this.renderState = renderState;
    }

}

改写Renderer

修改Renderer中的cullBackFace方法,根据CullMode的值来决定如何剔除。

/**
 * 剔除背面
 * 
 * @param a
 * @param b
 * @param c
 * @return
 */
protected boolean cullBackFace(Vector3f a, Vector3f b, Vector3f c) {

    // 计算ab向量
    Vector3f ab = b.subtract(a, a);

    // 计算bc向量
    Vector3f bc = c.subtract(b, b);

    // 计算表面法线
    Vector3f faceNormal = ab.crossLocal(bc);
    
    float dot = faceNormal.dot(c);

    CullMode cullMode = material.getRenderState().getCullMode();
    switch (cullMode) {
    case NEVER:
        return false;
    case ALWAYS:
        return true;
    case BACK:
        return dot >= 0;
    case FACE:
        return dot < 0;
    default:
        return false;
    }
}

改写SoftwareRaster

为了方便光栅器查询当前的渲染状态,为SoftwareRaster类增加一个RenderState成员,以及对应的set方法。

// 渲染状态
protected RenderState renderState;

public void setRenderState(RenderState renderState) {
    this.renderState = renderState;
}

修改Renderer的render(Geometry )方法,在执行光栅化之前,先把RenderrState设置给SoftwareRaster。

/**
 * 渲染单个物体
 * @param geometry
 */
protected void render(Geometry geometry) {
    
    // 设置材质
    this.material = geometry.getMaterial();
    // 设置渲染状态
    this.raster.setRenderState(material.getRenderState());// <----- 加了这一句
    
    // 提取网格数据..
    
    // 遍历所有三角形
    for (int i = 0; i < indexes.length; i += 3) {

        // ...
    }
}

修改SoftwareRaster的rasterizeTriangle方法,根据FillMode来决定如何进行光栅化。

/**
 * 光栅化三角形
 * @param v0
 * @param v1
 * @param v2
 */
public void rasterizeTriangle(RasterizationVertex v0, RasterizationVertex v1, RasterizationVertex v2) {
    
    // 将顶点变换到投影平面
    v0.perspectiveDivide();
    v1.perspectiveDivide();
    v2.perspectiveDivide();
    
    Matrix4f viewportMatrix = renderer.getViewportMatrix();
    
    // 把顶点位置修正到屏幕空间。
    viewportMatrix.mult(v0.position, v0.position);
    viewportMatrix.mult(v1.position, v1.position);
    viewportMatrix.mult(v2.position, v2.position);

    switch (renderState.getFillMode()) {
    case POINT: {
        rasterizePixel((int)v0.position.x, (int)v0.position.y, v0);
        rasterizePixel((int)v1.position.x, (int)v1.position.y, v1);
        rasterizePixel((int)v2.position.x, (int)v2.position.y, v2);
        return;
    }
    case LINE : {
        rasterizeLine(v0, v1);
        rasterizeLine(v0, v2);
        rasterizeLine(v1, v2);
        return;
    }
    case FACE : {
        // 按Y坐标把三个顶点从上到下冒泡排序
        RasterizationVertex tmp;
        if (v0.position.y > v1.position.y) {
            tmp = v0;
            v0 = v1;
            v1 = tmp;
        }
        if (v1.position.y > v2.position.y) {
            tmp = v1;
            v1 = v2;
            v2 = tmp;
        }
        if (v0.position.y > v1.position.y) {
            tmp = v0;
            v0 = v1;
            v1 = tmp;
        }
        
        float y0 = v0.position.y;
        float y1 = v1.position.y;
        float y2 = v2.position.y;
        
        if (y0 == y1) {// 平顶
            fillTopLineTriangle(v0, v1, v2);
        } else if (y1 == y2) {// 平底
            fillBottomLineTriangle(v0, v1, v2);
        } else {// 分割三角形
            
            // 线性插值
            // FIXME 需要透视校正
            float t = (y1 - y0) / (y2 - y0);
            RasterizationVertex middleVert = new RasterizationVertex();
            middleVert.interpolateLocal(v0, v2, t);
            
            if (middleVert.position.x <= v1.position.x)  {// 左三角形
                // 画平底
                fillBottomLineTriangle(v0, middleVert, v1);
                // 画平顶
                fillTopLineTriangle(middleVert, v1, v2);
            } else {// 右三角形
                // 画平底
                fillBottomLineTriangle(v0, v1, middleVert);
                // 画平顶
                fillTopLineTriangle(v1, middleVert, v2);
            }
        }
        return;
    }
    }
}

改写测试用例

改写Test3DView这个测试用例,改变Material中的RenderState参数。

package net.jmecn.examples;

import net.jmecn.Application;
import net.jmecn.material.Material;
import net.jmecn.material.RenderState;
import net.jmecn.material.RenderState.CullMode;
import net.jmecn.material.RenderState.FillMode;
import net.jmecn.math.Vector3f;
import net.jmecn.renderer.Camera;
import net.jmecn.scene.Geometry;
import net.jmecn.scene.Mesh;
import net.jmecn.scene.shape.Box;

/**
 * 测试3D观察的效果
 * @author yanmaoyuan
 *
 */
public class Test3DView extends Application {

    public static void main(String[] args) {
        Test3DView app = new Test3DView();
        app.setResolution(400, 300);
        app.setTitle("3D View");
        app.setFrameRate(60);
        app.start();
    }

    private Geometry geom;
    
    private final static float PI = 3.1415626f;
    private final static float _2PI = PI * 2;
    private float angle = 0;// 旋转角度
    
    @Override
    protected void initialize() {
        
        // 网格
        Mesh mesh = new Box();
        
        // 材质
        Material material = new Material();
        RenderState renderState = material.getRenderState();

        // 不裁剪背面
        renderState.setCullMode(CullMode.NEVER);
        
        // 显示为线框
        renderState.setFillMode(FillMode.LINE);
        
        // 添加到场景中
        this.geom = new Geometry(mesh, material);
        rootNode.attachChild(geom);
        
        // 调整摄像机的位置
        Camera cam = getCamera();
        cam.lookAt(new Vector3f(2, 3, 5), Vector3f.ZERO, Vector3f.UNIT_Y);
    }

    @Override
    protected void update(float delta) {
        // 每秒旋转180°
        angle += delta * PI;
        
        // 若已经旋转360°,则减去360°。
        if (angle > _2PI) {
            angle -= _2PI;
        }
        
        // 计算旋转:绕Z轴顺时针方向旋转
        geom.getLocalTransform().getRotation().fromAxisAngle(Vector3f.UNIT_Y, -angle);
    }

}

执行结果,其实就是图三。

另一个测试用例

Test3DView已经用了很久啦,就让它保持这个线框状态吧,是时候写个新的测试用例了。

package net.jmecn.examples;

import net.jmecn.Application;
import net.jmecn.material.Material;
import net.jmecn.math.Vector3f;
import net.jmecn.scene.Geometry;
import net.jmecn.scene.shape.Box;

/**
 * 测试3D光栅化
 * @author yanmaoyuan
 *
 */
public class Test3DRasterization extends Application {

    public static void main(String[] args) {
        Test3DRasterization app = new Test3DRasterization();
        app.setResolution(400, 300);
        app.setTitle("Test 3D Rasterization");
        app.setFrameRate(60);
        app.start();
    }
    
    @Override
    protected void initialize() {
        // 初始化摄像机
        getCamera().lookAt(new Vector3f(3, 4, 5),
                Vector3f.ZERO, Vector3f.UNIT_Y);
        
        // 添加到场景中
        Geometry geom = new Geometry(new Box(), new Material());
        rootNode.attachChild(geom);
    }

    @Override
    protected void update(float delta) {}

}

这个测试用例没有设置RenderState,目的是测试默认值的光栅化方式。

结果其实就是图五。

总结

目标达成。通过RenderState可以更加灵活地控制不同物体的渲染方式,再也不用反复修改源代码了。