Java软光栅渲染器-光照、材质与着色器
目标
为实现顶点光照着色做准备,本章将要定义一些新东西。
- 定义光源
- 定义材质
- 定义着色器接口
- 实现着色器
实现
我越来越兴奋了。
定义光源
一般来说,3D图形引擎应该支持4种光源,分别是:
- 环境光
- 定向光(平行光)
- 点光源
- 聚光灯
我打算先创建光源的抽象类,然后再慢慢实现每一种光源。
Light
新建一个net.jmecn.light包,定义四种光源的父类。
Light类只有颜色属性,没有其它参数。
默认颜色为白色。
package net.jmecn.light;
import net.jmecn.math.Vector4f;
/**
* 光源
* @author yanmaoyuan
*
*/
public abstract class Light {
// 光源的颜色
protected Vector4f color;
public Light() {
color = new Vector4f(1, 1, 1, 1);
}
public Light(Vector4f color) {
this.color = color;
}
public Vector4f getColor() {
return color;
}
public void setColor(Vector4f color) {
this.color = color;
}
}
AmbientLight
环境光跟父类基本上没区别。
package net.jmecn.light;
import net.jmecn.math.Vector4f;
/**
* 环境光
* @author yanmaoyuan
*
*/
public class AmbientLight extends Light {
public AmbientLight() {
super();
}
public AmbientLight(Vector4f color) {
super(color);
}
}
DirectionalLight
定向光,或者说平行光,多了一个方向属性。
package net.jmecn.light;
import net.jmecn.math.Vector3f;
import net.jmecn.math.Vector4f;
/**
* 定向光源(平行光)
* @author yanmaoyuan
*
*/
public class DirectionalLight extends Light{
// 光照方向
protected Vector3f direction;
public DirectionalLight(Vector3f direction) {
super();
this.direction = direction;
}
public DirectionalLight(Vector4f color) {
super(color);
this.direction = new Vector3f(0, 0, -1);
}
public DirectionalLight(Vector4f color, Vector3f direction) {
super(color);
this.direction = direction;
}
public Vector3f getDirection() {
return direction;
}
public void setDirection(Vector3f direction) {
this.direction = direction;
}
}
管理光源
光源将用于渲染三维场景。在Renderer中添加一个
List<Light> lights
成员,并提供set方法。
// 光源
private List<Light> lights;
/**
* 设置光源
* @param lights
*/
public void setLights(List<Light> lights) {
this.lights = lights;
}
在 Application 中也添加一个 List<Light> lights
成员,用于管理场景中的光源。这个成员是protected权限的,Application的子类可以直接通过lights.add(..)方法来添加光源。
// 光源
protected List<Light> lights;
/**
* 构造方法
*/
public Application() {
// ..
// 光源
lights = new ArrayList<Light>();
// ..
}
一般来说,光源需要在应用程序的主循环中更新,然后在每次调用renderer.render()方法之前设置给Renderer。不过我现在用的是Java语言,可以直接让Renderer和Application共用同一个对象的引用。
直接在Application的start方法中,把lights的引用传给Renderer即可。
/**
* 启动程序
*/
public void start() {
// 计时器..
// 创建主窗口..
// 创建渲染器
renderer = new Renderer(width, height);
renderer.setBackgroundColor(ColorRGBA.DARKGRAY);
renderer.setLights(lights);// <----
// ..
// 初始化
initialize();
while (isRunning) {
// ..
}
// ..
}
定义材质
目前Material类中只有一个Texture和一个RenderState,还远不能用来描述模型的外观。给Material增加各种参数,这样在着色阶段才能够正常使用。
如果你不太理解这些参数的作用,可以查阅这篇文章:http://blog.jmecn.net/3d-game-terminology/
Material的代码如下:
/**
* 材质
*
* @author yanmaoyuan
*
*/
public class Material {
protected RenderState renderState;
private boolean isUseVertexColor; // 是否使用顶点色
private Vector4f emssive; // 自发光色
private Vector4f diffuse; // 漫反射光色
private Vector4f ambient; // 环境光色
private Vector4f specular; // 高光颜色
private float shininess; // 光泽度
private Texture emssiveMap; // 发光贴图
private Texture diffuseMap; // 漫反射贴图
private Texture specularMap; // 高光贴图
private Texture normalMap; // 法线贴图
public Material() {
// 初始化渲染状态
renderState = new RenderState();
// 初始化材质参数
isUseVertexColor = false; // 不使用顶点色
emssive = new Vector4f(0); // 黑色
diffuse = new Vector4f(1); // 白色
ambient = new Vector4f(1); // 白色
specular = new Vector4f(0); // 黑色
shininess = 1f; // 不光泽
emssiveMap = null;
diffuseMap = null;
specularMap = null;
normalMap = null;
}
}
然后,为Material增加一些getter和setter。
public boolean isUseVertexColor() {
return isUseVertexColor;
}
public void setUseVertexColor(boolean isUseVertexColor) {
this.isUseVertexColor = isUseVertexColor;
}
public Vector4f getEmssive() {
return emssive;
}
public void setEmssive(Vector4f emssive) {
this.emssive.set(emssive);
}
public Vector4f getDiffuse() {
return diffuse;
}
public void setDiffuse(Vector4f diffuse) {
this.diffuse.set(diffuse);
}
public Vector4f getAmbient() {
return ambient;
}
public void setAmbient(Vector4f ambient) {
this.ambient.set(ambient);
}
public Vector4f getSpecular() {
return specular;
}
public void setSpecular(Vector4f specular) {
this.specular.set(specular);
}
public float getShininess() {
return shininess;
}
public void setShininess(float shininess) {
this.shininess = shininess;
}
public Texture getDiffuseMap() {
return diffuseMap;
}
public void setDiffuseMap(Texture diffuseMap) {
this.diffuseMap = diffuseMap;
}
public Texture getSpecularMap() {
return specularMap;
}
public void setSpecularMap(Texture specularMap) {
this.specularMap = specularMap;
}
public Texture getEmssiveMap() {
return emssiveMap;
}
public void setEmssiveMap(Texture emssiveMap) {
this.emssiveMap = emssiveMap;
}
public Texture getNormalMap() {
return normalMap;
}
public void setNormalMap(Texture normalMap) {
this.normalMap = normalMap;
}
注意,Material中原来的texture成员被我删掉了,用diffuseMap来取代。在与Texture有关的几个测试用例中,都要把setTexture方法改成setDiffuseMap。
定义着色器
用Java来实现着色器?听起来挺疯狂的。
创建一个Shader抽象类,主要包含顶点着色和片段着色两个接口,用来取代渲染管线中的vertexShader方法和fragmentShader方法。
package net.jmecn.shader;
import net.jmecn.scene.RasterizationVertex;
import net.jmecn.scene.Vertex;
/**
* 着色器
* @author yanmaoyuan
*
*/
public abstract class Shader {
/**
* 顶点着色器
* @param vertex
* @return
*/
public abstract RasterizationVertex vertexShader(Vertex vertex);
/**
* 片段着色器
* @param frag
*/
public abstract boolean fragmentShader(RasterizationVertex frag);
}
Uniform
着色器在工作时需要各种uniform变量(主要是变换矩阵),把各种空间变换矩阵都定义成Shader的protected成员,这样Shader的实现类就可以直接访问它们了。
// uniforms
protected Matrix4f worldMatrix;
protected Matrix4f viewMatrix;
protected Matrix4f projectionMatrix;
protected Matrix4f viewProjectionMatrix;
protected Matrix4f worldViewMatrix;
protected Matrix4f worldViewProjectionMatrix;
// getter/setters
public Matrix4f getWorldMatrix() {
return worldMatrix;
}
public void setWorldMatrix(Matrix4f worldMatrix) {
this.worldMatrix = worldMatrix;
}
public Matrix4f getViewMatrix() {
return viewMatrix;
}
public void setViewMatrix(Matrix4f viewMatrix) {
this.viewMatrix = viewMatrix;
}
public Matrix4f getProjectionMatrix() {
return projectionMatrix;
}
public void setProjectionMatrix(Matrix4f projectionMatrix) {
this.projectionMatrix = projectionMatrix;
}
public Matrix4f getViewProjectionMatrix() {
return viewProjectionMatrix;
}
public void setViewProjectionMatrix(Matrix4f viewProjectionMatrix) {
this.viewProjectionMatrix = viewProjectionMatrix;
}
public Matrix4f getWorldViewMatrix() {
return worldViewMatrix;
}
public void setWorldViewMatrix(Matrix4f worldViewMatrix) {
this.worldViewMatrix = worldViewMatrix;
}
public Matrix4f getWorldViewProjectionMatrix() {
return worldViewProjectionMatrix;
}
public void setWorldViewProjectionMatrix(Matrix4f worldViewProjectionMatrix) {
this.worldViewProjectionMatrix = worldViewProjectionMatrix;
}
这些Uniform变量未必够用。比如计算法线时可能需要用WorldMaterix的逆矩阵的转置矩阵(简称WorldMatrix_IT,或者NormalMatrix)。需要但未定义的Uniform可以留到以后再来处理,现在先把Shader系统设计出来。
Attributes
Attribute属性是着色时使用的变量,由应用程序传给Shader。不过既然我用的是Java,大可不必专门维持一个Attributes列表,可以用简单点的办法。
// attributes
protected Material material;
protected List<Light> lights;
public Material getMaterial() {
return material;
}
public void setMaterial(Material material) {
this.material = material;
}
public List<Light> getLights() {
return lights;
}
public void setLights(List<Light> lights) {
this.lights = lights;
}
如你所见,我把材质和光源直接定义成了Shader的Attribute属性,只要在使用shader之前先把material和lights设置给它就好了。如果需要其他的Attribute,还可以继续扩展。
毕竟是Java,不像GLSL有那么多的限制。
默认着色器实现
创建一个DefaultShader类,实现目前Renderer类和SoftwareRaster类中的着色功能。
package net.jmecn.shader;
import net.jmecn.material.Texture;
import net.jmecn.math.Vector4f;
import net.jmecn.scene.RasterizationVertex;
import net.jmecn.scene.Vertex;
/**
* 默认着色器
* @author yanmaoyuan
*
*/
public class DefaultShader extends Shader {
@Override
public RasterizationVertex vertexShader(Vertex vertex) {
RasterizationVertex out = new RasterizationVertex();
// 顶点位置
out.position.set(vertex.position, 1f);
// 顶点法线
if (vertex.normal != null) {
out.normal.set(vertex.normal);
out.hasNormal = true;
}
// 纹理坐标
if (vertex.texCoord != null) {
out.texCoord.set(vertex.texCoord);
out.hasTexCoord = true;
}
// 顶点颜色
if (vertex.color != null) {
out.color.set(vertex.color);
out.hasVertexColor = true;
}
// 模型-观察-透视 变换
worldViewProjectionMatrix.mult(out.position, out.position);
return out;
}
@Override
public boolean fragmentShader(RasterizationVertex frag) {
Texture texture = material.getDiffuseMap();
if (texture != null && frag.hasTexCoord) {
Vector4f texColor = texture.sample2d(frag.texCoord);
frag.color.multLocal(texColor);
}
return true;
}
}
vertexShader的实现有一点臃肿,这个方法中很大的篇幅是在复制Vertex对象的值,未来实现其他Shader算法时都会用到。不如把它提取出来做成一个方法,写在Shader抽象类中,这样之类就可以直接调用了。
/**
* 复制顶点数据
* @param vertex
* @return
*/
protected RasterizationVertex copy(Vertex vertex) {
RasterizationVertex out = new RasterizationVertex();
// 顶点位置
out.position.set(vertex.position, 1f);
// 顶点法线
if (vertex.normal != null) {
out.normal.set(vertex.normal);
out.hasNormal = true;
}
// 纹理坐标
if (vertex.texCoord != null) {
out.texCoord.set(vertex.texCoord);
out.hasTexCoord = true;
}
// 顶点颜色
if (vertex.color != null) {
out.color.set(vertex.color);
out.hasVertexColor = true;
}
return out;
}
这样的话,DefaultShader中的代码就可以简化了。
package net.jmecn.shader;
import net.jmecn.material.Texture;
import net.jmecn.math.Vector4f;
import net.jmecn.scene.RasterizationVertex;
import net.jmecn.scene.Vertex;
/**
* 默认着色器
* @author yanmaoyuan
*
*/
public class DefaultShader extends Shader {
@Override
public RasterizationVertex vertexShader(Vertex vertex) {
RasterizationVertex out = copy(vertex);
// 模型-观察-透视 变换
worldViewProjectionMatrix.mult(out.position, out.position);
return out;
}
@Override
public boolean fragmentShader(RasterizationVertex frag) {
Texture texture = material.getDiffuseMap();
if (texture != null && frag.hasTexCoord) {
Vector4f texColor = texture.sample2d(frag.texCoord);
frag.color.multLocal(texColor);
}
return true;
}
}
在材质中增加着色器
在Material类中增加一个Shader成员,默认使用DefaultShader。
在设置Shader时把Material对象的引用传给shader,这样在shader中就可以访问这个Material中的属性了。
package net.jmecn.material;
import net.jmecn.math.Vector4f;
import net.jmecn.shader.DefaultShader;
import net.jmecn.shader.Shader;
/**
* 材质
*
* @author yanmaoyuan
*
*/
public class Material {
// ..
// 着色器
private Shader shader;
public Material() {
// 初始化渲染状态..
// 初始化材质参数..
// 设置默认着色器
shader = new DefaultShader();
shader.setMaterial(this);
}
public Shader getShader() {
return shader;
}
public void setShader(Shader shader) {
if (shader != null) {
shader.setMaterial(this);
this.shader = shader;
}
}
}
改造渲染管线
先在SoftwareRaster类中定义一个Shader成员,并提供set方法。
// 着色器
private Shader shader;
public void setShader(Shader shader) {
this.shader = shader;
}
修改 rasterizePixel 方法,不再调用原来的 fragmentShader 方法,而是调用shader.fragmentShader()。
/**
* 光栅化点
* @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;
}
// 透视投影修正..
// 执行片段着色器
if ( !shader.fragmentShader(frag) )
return;
// 深度测试..
// Alpha测试..
// 颜色混合..
// 写入depthBuffer..
// 写入frameBuffer..
}
注意,为了实现着色器中的discard功能,我把fragmentShader方法的返回值设计为boolean类型,这样 fragmentShader 就可以通过 return false
来舍弃某些像素值。现在完全可以用 fragmentShader 来实现Alpha测试。
接着,修改 Renderer 类的 render(Geometry geometry) 方法。在执行光栅化之前,先把Material中的shader对象取出来,把各种矩阵的值设置给它,再把shader设给raster。
遍历三角形时,使用 shader.vertexShader() 来取代原来的 vertexShader 方法。
/**
* 渲染单个物体
* @param geometry
*/
protected void render(Geometry geometry) {
// 设置材质
this.material = geometry.getMaterial();
// 设置渲染状态
this.raster.setRenderState(material.getRenderState());
// 设置着色器
Shader shader = material.getShader();
shader.setLights(lights);
// 设置全局变量
shader.setWorldMatrix(worldMatrix);
shader.setViewMatrix(viewMatrix);
shader.setProjectionMatrix(projectionMatrix);
shader.setWorldViewMatrix(worldViewMatrix);
shader.setViewProjectionMatrix(viewProjectionMatrix);
shader.setWorldViewProjectionMatrix(worldViewProjectionMatrix);
raster.setShader(shader);
// 用于保存变换后的向量坐标。
Vector3f a = new Vector3f();
Vector3f b = new Vector3f();
Vector3f c = new Vector3f();
// 提取网格数据
Mesh mesh = geometry.getMesh();
int[] indexes = mesh.getIndexes();
Vertex[] vertexes = mesh.getVertexes();
// 遍历所有三角形
for (int i = 0; i < indexes.length; i += 3) {
Vertex v0 = vertexes[indexes[i]];
Vertex v1 = vertexes[indexes[i+1]];
Vertex v2 = vertexes[indexes[i+2]];
// 执行顶点着色器
RasterizationVertex out0 = shader.vertexShader(v0);
RasterizationVertex out1 = shader.vertexShader(v1);
RasterizationVertex out2 = shader.vertexShader(v2);
// 在观察空间进行背面消隐
worldViewMatrix.mult(v0.position, a);
worldViewMatrix.mult(v1.position, b);
worldViewMatrix.mult(v2.position, c);
if (cullBackFace(a, b, c))
continue;
// TODO 视锥裁剪
raster.rasterizeTriangle(out0, out1, out2);
}
}
这样对渲染管线的改造就完成了。
测试着色器
UnshadedShader
创建一个UnshadedShader。
在顶点着色阶段,它将把Material中的Diffuse颜色赋予顶点。在片段着色阶段,它和DefaultShader一样把纹理采样的颜色赋予像素。
通过这个简单的着色器,就可以利用Material来改变物体表面的颜色了。
package net.jmecn.shader;
import net.jmecn.material.Texture;
import net.jmecn.math.Vector4f;
import net.jmecn.scene.RasterizationVertex;
import net.jmecn.scene.Vertex;
/**
* Unshaded着色器
* @author yanmaoyuan
*
*/
public class UnshadedShader extends Shader {
@Override
public RasterizationVertex vertexShader(Vertex vertex) {
RasterizationVertex out = copy(vertex);
if (material.isUseVertexColor()) {
out.color.multLocal(material.getDiffuse());
} else {
out.color.set(material.getDiffuse());
}
// 模型-观察-透视 变换
worldViewProjectionMatrix.mult(out.position, out.position);
return out;
}
@Override
public boolean fragmentShader(RasterizationVertex frag) {
Texture texture = material.getDiffuseMap();
if (texture != null && frag.hasTexCoord) {
Vector4f texColor = texture.sample2d(frag.texCoord);
frag.color.multLocal(texColor);
}
return true;
}
}
测试用例
写一个测试用例,实验一下UnshadedShader的作用。在这个测试用例中,我将使用下面这个素材:
代码如下:
package net.jmecn.examples;
import java.io.IOException;
import net.jmecn.Application;
import net.jmecn.material.Material;
import net.jmecn.material.Texture;
import net.jmecn.math.Vector3f;
import net.jmecn.math.Vector4f;
import net.jmecn.renderer.Camera;
import net.jmecn.renderer.Image;
import net.jmecn.scene.Geometry;
import net.jmecn.scene.shape.Box;
import net.jmecn.shader.UnshadedShader;
/**
* 测试Unshaded Shader
* @author yanmaoyuan
*
*/
public class TestUnahdedShader extends Application {
public static void main(String[] args) {
TestUnahdedShader app = new TestUnahdedShader();
app.setResolution(400, 300);
app.setTitle("Test Unshaded Shader");
app.setFrameRate(60);
app.start();
}
@Override
protected void initialize() {
// 初始化摄像机
Camera cam = getCamera();
cam.lookAt(new Vector3f(2, 3, 4), Vector3f.ZERO, Vector3f.UNIT_Y);
// 创建材质
Material material = new Material();
// 设置着色器
material.setShader(new UnshadedShader());
// 设置颜色
material.setDiffuse(new Vector4f(1, 1, 1, 1));
// 设置纹理
try {
// 加载一幅图片作为纹理
Image image = new Image("res/Crate.png");
Texture diffuseMap = new Texture(image);
material.setDiffuseMap(diffuseMap);
} catch (IOException e) {
// 使用默认程序纹理
material.setDiffuseMap(new Texture());
}
// 添加到场景中
Geometry geometry = new Geometry(new Box(), material);
rootNode.attachChild(geometry);
}
@Override
protected void update(float delta) {}
}
运行效果如下图:
总结
一个基于Java着色器的渲染框架。。我真是疯了。