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。
效果如何呢?
如上图,本该“露头”的部分不见了,本该被遮挡的部分画出来了。
总结
目标达成。