Java软光栅渲染器-顶点光照

目标

使用Gouraud着色算法实现顶点着色

实现

增加Uniform

进行顶点着色之前,需要先给Shader增加两个Uniform变量。其一是法向量变换矩阵,其二是摄像机在世界空间的坐标。

如果你不记得什么是“法向量变换”,可以回顾这篇文章 Java软光栅渲染器-空间变换 中的“法向量变换”部分。

在Renderer中添加一个法向量变换矩阵和一个摄像机位置,并在 render(List geomList, Camera camera) 方法中计算它们的值。

// 法向量变换矩阵
private Matrix3f normalMatrix = new Matrix3f();
// 摄像机位置
private Vector3f cameraPosition = new Vector3f();

/**
 * 渲染场景
 * @param scene
 * @param camera
 */
public void render(List<Geometry> geomList, Camera camera) {

    // 根据Camera初始化观察变换矩阵。
    viewMatrix.set(camera.getViewMatrix());
    projectionMatrix.set(camera.getProjectionMatrix());
    viewProjectionMatrix.set(camera.getViewProjectionMatrix());
    cameraPosition.set(camera.getLocation());

    // TODO 剔除那些不可见的物体

    // 遍历场景中的Mesh
    for(int i=0; i<geomList.size(); i++) {
        Geometry geom = geomList.get(i);

        // 根据物体的世界变换,计算MVP等变换矩阵。
        worldMatrix.set(geom.getWorldTransform().toTransformMatrix());
        viewMatrix.mult(worldMatrix, worldViewMatrix);
        viewProjectionMatrix.mult(worldMatrix, worldViewProjectionMatrix);

        // 计算法向量变换矩阵
        worldMatrix.toRotationMatrix(normalMatrix);
        // FIXME 先判断是否为正交矩阵,然后在决定是否要计算Invert、Transpose矩阵。
        normalMatrix.invertLocal();
        normalMatrix.transposeLocal();

        // TODO 使用包围体,剔除不可见物体

        // 渲染
        render(geom);
    }
}

在Shader类中定义同样两个protected成员,并在Renderer中调用shader的set方法设置这些值。

/**
 * 渲染单个物体
 * @param geometry
 */
protected void render(Geometry geometry) {

    // 设置材质
    this.material = geometry.getMaterial();
    // 设置渲染状态
    this.raster.setRenderState(material.getRenderState());

    // 设置着色器
    Shader shader = material.getShader();
    shader.setLights(lights);
    raster.setShader(shader);

    // 设置全局变量
    shader.setWorldMatrix(worldMatrix);
    shader.setViewMatrix(viewMatrix);
    shader.setProjectionMatrix(projectionMatrix);
    shader.setWorldViewMatrix(worldViewMatrix);
    shader.setViewProjectionMatrix(viewProjectionMatrix);
    shader.setWorldViewProjectionMatrix(worldViewProjectionMatrix);
    shader.setNormalMatrix(normalMatrix);//<------ 这里
    shader.setCameraPosition(cameraPosition);//<------ 这里

    // 遍历所有三角形
    for (int i = 0; i < indexes.length; i += 3) {

        // ..
    }
}

Gouraud着色器

创建GouraudShader类,实现顶点光照计算。

出于性能考虑,我没有实现refract函数来计算光线的反射方向向量,而是使用光线和视线的半途向量(halfVector)来计算反射光的强度。

package net.jmecn.shader;

import net.jmecn.light.AmbientLight;
import net.jmecn.light.DirectionalLight;
import net.jmecn.light.Light;
import net.jmecn.material.Texture;
import net.jmecn.math.Vector3f;
import net.jmecn.math.Vector4f;
import net.jmecn.scene.RasterizationVertex;
import net.jmecn.scene.Vertex;

/**
 * Gouraud着色器
 * @author yanmaoyuan
 *
 */
public class GouraudShader extends Shader {

    /// 下列向量,均处于世界空间中
    /// 将它们定义为类的成员,避免在光照计算时总是实例化新的对象。

    // 顶点坐标
    private Vector3f position = new Vector3f();
    // 顶点法线
    private Vector3f normal = new Vector3f();
    // 顶点到光源方向向量
    private Vector3f lightVector = new Vector3f();
    // 顶点到眼睛方向向量
    private Vector3f eyeVector = new Vector3f();
    // 光线和眼睛向量之间的半途向量,用于计算高光反射强度。
    private Vector3f halfVector = new Vector3f();

    // 光照颜色
    private Vector4f ambient = new Vector4f();
    private Vector4f diffuse = new Vector4f();
    private Vector4f specular = new Vector4f();

    private Vector3f color = new Vector3f();

    /**
     * 计算光照
     * @param vert
     * @param light
     */
    private Vector3f lighting(RasterizationVertex vert, Light light) {
        color.set(0, 0, 0);

        if (light instanceof AmbientLight) {
            // 环境光
            material.getAmbient().mult(light.getColor(), ambient);
            ambient.multLocal(light.getColor().w);
            return color.set(ambient.x, ambient.y, ambient.z);
        } else if (light instanceof DirectionalLight) {
            DirectionalLight dl = (DirectionalLight) light;

            // 顶点位置
            position.set(vert.position.x, vert.position.y, vert.position.z);
            // 顶点法线
            normal.set(vert.normal);

            // 计算顶点到光源的方向向量
            lightVector.set(dl.getDirection().negate());

            // 计算顶点到眼睛的方向向量
            cameraPosition.subtract(position, eyeVector);
            eyeVector.normalizeLocal();

            // 计算光线和眼睛向量之间的半途向量,用于计算高光反射强度。
            lightVector.add(eyeVector, halfVector);
            halfVector.normalizeLocal();

            // 计算漫反射强度
            float kd = Math.max(normal.dot(lightVector), 0.0f);

            // 计算高光强度
            float ks = Math.max(normal.dot(halfVector), 0.0f);
            ks = (float) Math.pow(ks, material.getShininess());

            // 计算漫射光颜色
            material.getDiffuse().mult(light.getColor(), diffuse);
            diffuse.multLocal(kd);

            // 计算高光颜色
            material.getSpecular().mult(light.getColor(), specular);
            specular.multLocal(ks);

            // 计算光最终的颜色
            diffuse.addLocal(specular).multLocal(light.getColor().w);

            return color.set(diffuse.x, diffuse.y, diffuse.z);
        }

        return color;
    }

    @Override
    public RasterizationVertex vertexShader(Vertex vertex) {
        RasterizationVertex out = copy(vertex);

        // 顶点法线
        normalMatrix.mult(out.normal, out.normal);
        out.normal.normalizeLocal();

        // 顶点位置
        worldMatrix.mult(out.position, out.position);

        out.color.set(0, 0, 0, 1);

        // 计算光照
        for(int i=0; i < lights.size(); i++) {
            Light l = lights.get(i);
            Vector3f color = lighting(out, l);
            out.color.x += color.x;
            out.color.y += color.y;
            out.color.z += color.z;
        }

        // 模型-观察-透视 变换
        viewProjectionMatrix.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;
    }

}

测试用例

写个测试用例,验证一下顶点光照的效果。

package net.jmecn.examples;

import net.jmecn.Application;
import net.jmecn.light.AmbientLight;
import net.jmecn.light.DirectionalLight;
import net.jmecn.material.Material;
import net.jmecn.math.Quaternion;
import net.jmecn.math.Vector3f;
import net.jmecn.math.Vector4f;
import net.jmecn.renderer.Camera;
import net.jmecn.scene.Geometry;
import net.jmecn.scene.shape.Box;
import net.jmecn.shader.GouraudShader;

/**
 * 测试Gouraud Shader
 * @author yanmaoyuan
 *
 */
public class TestGouraudShader extends Application {

    public static void main(String[] args) {
        TestGouraudShader app = new TestGouraudShader();
        app.setResolution(400, 300);
        app.setTitle("Test Gouraud Shader");
        app.setFrameRate(60);
        app.start();
    }

    // 几何体
    private Geometry geometry;
    // 旋转
    private Quaternion rot = new Quaternion();

    @Override
    protected void initialize() {
        // 初始化摄像机
        Camera cam = getCamera();
        cam.lookAt(new Vector3f(3, 4, 5), Vector3f.ZERO, Vector3f.UNIT_Y);

        // 创建材质
        Material material = new Material();

        // 设置着色器
        material.setShader(new GouraudShader());

        // 设置颜色
        material.setDiffuse(new Vector4f(1, 1, 1, 1));

        // 添加到场景中
        geometry = new Geometry(new Box(), material);
        rootNode.attachChild(geometry);

        // 添加光源
        lights.add(new AmbientLight(new Vector4f(0.3f, 0.0f, 0.0f, 1f)));
        lights.add(new DirectionalLight(new Vector4f(0.7f, 0.0f, 0.0f, 1f), new Vector3f(-3, -2, -4).normalizeLocal()));
    }

    @Override
    protected void update(float delta) {
        rot.rotateY(delta);
        geometry.getLocalTransform().getRotation().multLocal(rot);
    }
}

运行程序,效果如下:

有点古怪。。为什么有个面迎着光也是暗的?

检查一下Box类的法线数据,发觉left面和right面的法线方向反了。left面应该指向 (1, 0, 0),而right面应该指向(-1, 0, 0)。

    // 顶点法线
    float[] normals = {
            // back
            0, 0,-1,   0, 0,-1,   0, 0,-1,   0, 0,-1,
            //front
            0, 0, 1,   0, 0, 1,   0, 0, 1,   0, 0, 1,
            // left
           -1, 0, 0,  -1, 0, 0,  -1, 0, 0,  -1, 0, 0,
            // right
            1, 0, 0,   1, 0, 0,   1, 0, 0,   1, 0, 0,
            // top
            0, 1, 0,   0, 1, 0,   0, 1, 0,   0, 1, 0,
            // bottom
            0,-1, 0,   0,-1, 0,   0,-1, 0,   0,-1, 0,
    };

交换一下这两个面的法线数据。

    // 顶点法线
    float[] normals = {
            // back
            0, 0,-1,   0, 0,-1,   0, 0,-1,   0, 0,-1,
            //front
            0, 0, 1,   0, 0, 1,   0, 0, 1,   0, 0, 1,
            // left
            1, 0, 0,   1, 0, 0,   1, 0, 0,   1, 0, 0,// <---
            // right
           -1, 0, 0,  -1, 0, 0,  -1, 0, 0,  -1, 0, 0,// <---
            // top
            0, 1, 0,   0, 1, 0,   0, 1, 0,   0, 1, 0,
            // bottom
            0,-1, 0,   0,-1, 0,   0,-1, 0,   0,-1, 0,
    };

重新运行测试用例。

Perfect

总结

目标达成。