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可以更加灵活地控制不同物体的渲染方式,再也不用反复修改源代码了。