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着色器的渲染框架。。我真是疯了。