Java软光栅渲染器-深度缓冲

目标

深度缓冲(Depth Buffer)又称Z缓冲区,是一个与渲染目标有相同大小的缓冲,这个缓冲记录每个像素的深度。深度缓冲的目的在于正确地生成通常的深度感知效果:较近的物体遮挡较远的物体。

像素的深度值是由视矩阵和投影矩阵决定的。在近裁平面上的像素深度值为0,在远裁平面上的像素的深度值为1。假设摄像机在原点,那么深度值越小的离摄像机越近。

本文的目标就是实现深度缓冲。

参考资料:

实现

测试用例

在实现深度缓冲之前,先写一个测试用例,看看没有深度缓冲时会发生什么。

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;

/**
 * 测试深度缓冲
 * @author yanmaoyuan
 *
 */
public class Test3DDepthBuffer extends Application {

    public static void main(String[] args) {
        Test3DDepthBuffer app = new Test3DDepthBuffer();
        app.setResolution(400, 300);
        app.setTitle("Test DepthBuffer");
        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);
        
        // 第二个方块
        geom = new Geometry(new Box(), new Material());
        geom.getLocalTransform().setTranslation(0.5f, -0.5f, -0.5f);
        rootNode.attachChild(geom);
    }

    @Override
    protected void update(float delta) {}

}

我在场景中创建了2个立方体。第二个立方体在第一个立方体偏右、偏下、偏后的位置(0.5f, -0.5f, -0.5f)。按照预期,应该看到第二个立方体被第一个立方体遮挡大了部分,但是右后侧露了一点头。

运行一下,看看效果:

这并不是期望的画面,正确的结果应该是这样的:

在没有实现深度缓冲时,后绘制的像素总是会覆盖先绘制的像素,结果就没办法正确体现出物体到摄像机的距离感。

初始化深度缓冲

不知道你还记不记得,我在上一次重构渲染管线时,已经在SoftwareRenderer中定义了一个float[]数组作为深度缓冲。在SoftwareRaster的构造方法中,根据Image(也就是FrameBuffer)的大小初始化了depthBuffer。

// 深度缓冲
protected float[] depthBuffer;

public SoftwareRaster(Renderer renderer, Image image) {
    super(image);
    this.depthBuffer = new float[width * height];
    this.renderer = renderer;
}

在每次渲染之前,要先清除深度缓冲。我在SoftwareRenderer中创建了clearDepthBuffer方法,用于把depthBuffer中的值初始化为1.0。

/**
 * 清除深度缓冲
 */
public void clearDepthBuffer() {
    int length = width * height;
    for(int i=0; i<length; i++) {
        depthBuffer[i] = 1.0f;
    }
}

在Renderer的clear方法中调用clearDepthBuffer,这样每次在渲染之前,depthBuffer都会被清理掉。

/**
 * 使用背景色填充图像数据
 */
public void clear() {
    raster.fill(clearColor);
    raster.clearDepthBuffer();// 清除深度缓冲
}

深度测试

深度测试发生在光栅化的最后一步,也就是光栅化像素的阶段。在把颜色写入frameBuffer之前,先比较position.z和depthBuffer中对应值的大小。如果不通过深度测试,就舍弃这个像素的值。

/**
 * 光栅化点
 * @param x
 * @param y
 * @param frag
 */
public void rasterizePixel(int x, int y, RasterizationVertex frag) {
    
    if (x < 0 || y < 0 || x >= width || y >= height) {
        return;
    }
   
    // 执行片段着色器
    fragmentShader(frag);

    int index = x + y * width;
    float depth = frag.position.z / frag.position.w;
    
    // 深度测试
    if (depthBuffer[index] < depth)
        return;
    else
        depthBuffer[index] = depth;
    
    // TODO Alpha测试

    // TODO 颜色混合
    
    // 写入frameBuffer
    index *= 4;
    Vector4f destColor = frag.color;;

    components[index] = (byte)(destColor.x * 0xFF);
    components[index + 1] = (byte)(destColor.y * 0xFF);
    components[index + 2] = (byte)(destColor.z * 0xFF);
    components[index + 3] = (byte)(destColor.w * 0xFF);
}

这样就实现了深度测试。

再次执行测试用例,看看效果。

嗯,这下正常了。

控制深度测试的状态

如同CullMode和FillMode一样,深度测试的方式也可以用一组状态值来控制,通常有三个参数。

  • boolean isDepthTest 是否进行深度测试
  • boolean isDepthWrite 是否将z值写入深度缓冲
  • DepthFunc depthFunc 深度测试的方式

isDepthTest 相当深度测试的开关,DepthFunc则决定了 if (depthDepth[index] < depth) 这一句的具体执行方式。

DepthFunc 和 CullMode、FaceMode一样,是一个枚举类型。

  • ALWAYS 总是通过测试
  • NEVER 总不通过测试(这个测试真的有用吗?)
  • LESS 判断 if (depthDepth[index] < depth)
  • LESS_EQUAL 判断 if (depthDepth[index] <= depth)
  • GREATER 判断 if (depthDepth[index] > depth)
  • GREATER_EQUAL 判断 if (depthDepth[index] >= depth)
  • EQUAL 判断 if (depthDepth[index] == depth)
  • NOT_EQUAL 判断 if (depthDepth[index] != depth)

DepthFunc的默认值为LESS。

定义DepthFunc

在RenderState中定义与深度缓冲有关的三个状态参数,尤其是定义DepthFunc。

package net.jmecn.material;

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

    // 前略..

    /**
     * 深度测试模式
     */
    public enum DepthFunc {
        ALWAYS,
        LESS,
        NEVER,
        LESS_EQUAL,
        GREATER,
        GREATER_EQUAL,
        EQUAL,
        NOT_EQUAL
    }

    private DepthFunc depthFunc;
    private boolean isDepthTest;
    private boolean isDepthWrite;

    public RenderState() {
        fillMode = FillMode.FACE;
        cullMode = CullMode.BACK;
                   
        depthFunc = DepthFunc.LESS;
        isDepthTest = true;
        isDepthWrite = true;

    }

    // 略...

    public DepthFunc getDepthFunc() {
        return depthFunc;
    }

    public void setDepthFunc(DepthFunc depthFunc) {
        this.depthFunc = depthFunc;
    }

    public boolean isDepthTest() {
        return isDepthTest;
    }

    public void setDepthTest(boolean isDepthTest) {
        this.isDepthTest = isDepthTest;
    }

    public boolean isDepthWrite() {
        return isDepthWrite;
    }

    public void setDepthWrite(boolean isDepthWrite) {
        this.isDepthWrite = isDepthWrite;
    }

}

使用RenderState来控制深度测试

在SoftwareRenderer中,实现depthTest方法,根据DepthFunc的值来决定如何进行检测。

/**
 * 深度测试
 * @param oldDepth
 * @param newDepth
 * @return
 */
private boolean depthTest(float oldDepth, float newDepth) {
    switch (renderState.getDepthFunc()) {
    case ALWAYS:
        return true;
    case NEVER:
        return false;
    case LESS:
        return newDepth < oldDepth;
    case LESS_EQUAL:
        return newDepth <= oldDepth;
    case GREATER:
        return newDepth > oldDepth;
    case GREATER_EQUAL:
        return newDepth >= oldDepth;
    case EQUAL:
        return newDepth == oldDepth;
    case NOT_EQUAL:
        return newDepth != oldDepth;
    }
    return false;
}

改写rasterizePixel方法,使用renderState来控制深度测试的方式。

/**
 * 光栅化点
 * @param x
 * @param y
 * @param frag
 */
public void rasterizePixel(int x, int y, RasterizationVertex frag) {
    
    if (x < 0 || y < 0 || x >= width || y >= height) {
        return;
    }
   
    // 执行片段着色器
    fragmentShader(frag);

    int index = x + y * width;
    float depth = frag.position.z / frag.position.w;
    
    // 深度测试
    if (renderState.isDepthTest()) {
        if (!depthTest(depthBuffer[index], depth))
        return;
    }
    
    // TODO Alpha测试

    // TODO 颜色混合

    // 写入depthBuffer
    if (renderState.isDepthWrite()) {
        depthBuffer[index] = depth;
    }
    
    // 写入frameBuffer..
}

改写测试用例

在Test3DDepthBuffer中搞一点小小的“破坏”。。

package net.jmecn.examples;

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

/**
 * 测试深度缓冲
 * @author yanmaoyuan
 *
 */
public class Test3DDepthBuffer extends Application {

    public static void main(String[] args) {
        Test3DDepthBuffer app = new Test3DDepthBuffer();
        app.setResolution(400, 300);
        app.setTitle("Test DepthBuffer");
        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);
        
        // 第二个方块
        geom = new Geometry(new Box(), new Material());
        geom.getMaterial().getRenderState().setDepthFunc(DepthFunc.GREATER); // <---- 这里
        geom.getLocalTransform().setTranslation(0.5f, -0.5f, -0.5f);
        rootNode.attachChild(geom);
    }

    @Override
    protected void update(float delta) {}

}

我把第二个物体的DepthFunc改成了GREATR,这意味着只有位于当前像素后方的像素才会被写入FrameBuffer和DepthBuffer。

效果如何呢?

如上图,本该“露头”的部分不见了,本该被遮挡的部分画出来了。

总结

目标达成。