Java软光栅渲染器-渲染个球

目标

  • 创建Sphere网格
  • 使用UnshadedShader渲染出一个地球
  • 实现Blinn-Phong着色器

实现

用立方体实在是很难看出光照的效果,所以我打算用一个球体来进行渲染。

渲染所使用的纹理,是这个全球地图。它在工程下的路径为res/earth.jpg

Sphere

首先需要一个Sphere网格。

package net.jmecn.scene.shape;

import net.jmecn.math.Vector2f;
import net.jmecn.math.Vector3f;
import net.jmecn.scene.Mesh;
import net.jmecn.scene.Vertex;

/**
 * 球体网格
 * @author yanmaoyuan
 *
 */
public class Sphere extends Mesh {

    private final static float HALF_PI = (float)(0.5 * Math.PI);
    private final static float TWO_PI = (float)(2.0 * Math.PI);

    // 半径
    private float radius;
    // 经线数量 longitude
    private int lonCount;
    // 纬线数量 latitude
    private int latCount;
    // 法线方向是否朝内?
    private boolean interior = false;

    private int vertCount;// 顶点数量
    private int triCount; // 三角形数量

    public Sphere() {
        this(1f);
    }

    public Sphere(float radius) {
        this(radius, 36, 21);
    }

    public Sphere(float radius, int lonCount, int latCount) {
        this(radius, lonCount, latCount, false);
    }

    public Sphere(float radius, int lonCount, int latCount, boolean interior) {
        this.radius = radius;
        this.lonCount = lonCount;
        this.latCount =latCount;
        this.interior = interior;

        createVertexBuffer();
        createIndexBuffer();
    }

    /**
     * 生成球体网格
     */
    private void createVertexBuffer() {
        this.vertCount = (latCount - 2) * (lonCount + 1) + 2;
        this.vertexes = new Vertex[vertCount];

        // 生成球体

        // 计算圆截面上每根经线的坐标
        float[] sin = new float[(lonCount + 1)];
        float[] cos = new float[(lonCount + 1)];

        float invLonCount = 1.0f / lonCount;
        for (int i = 0; i < lonCount; i++) {
            float angle = TWO_PI * invLonCount * i;
            cos[i] = (float)Math.cos(angle);
            sin[i] = (float)Math.sin(angle);
        }
        sin[lonCount] = sin[0];
        cos[lonCount] = cos[0];

        // 生成Sphere顶点数据
        Vertex v;
        float factor = 2.0f / (latCount - 1);
        int i = 0;
        for (int iY = 1; iY < (latCount - 1); iY++) {
            float fAFraction = HALF_PI * (-1.0f + factor * iY); // in (-pi/2, pi/2)

            float sinZ = (float) Math.sin(fAFraction);

            // 计算圆截面高度和半径
            float sliceHeight = (float) sinZ * radius;
            float sliceRadius = (float) Math.cos(fAFraction) * radius;

            // 计算圆截面上的顶点坐标,首位两个顶点共用相同的位置和法线。
            int iSave = i;
            for (int iR = 0; iR < lonCount; iR++) {
                v = vertexes[i] = new Vertex();

                // 顶点坐标
                v.position = new Vector3f(cos[iR] * sliceRadius, sliceHeight, sin[iR] * sliceRadius);

                // 法线方向
                v.normal = new Vector3f(v.position);
                v.normal.normalizeLocal();
                if (interior) v.normal.negateLocal();

                // 纹理坐标
                v.texCoord = new Vector2f(1f - iR * invLonCount, 0.5f * (factor * iY));

                i++;
            }

            v = vertexes[i] = new Vertex();
            v.position = vertexes[iSave].position;
            v.normal = vertexes[iSave].normal;
            v.texCoord = new Vector2f(0f, 0.5f * (factor * iY));
            i++;
        }

        // 南极点
        v = vertexes[i] = new Vertex();
        v.position = new Vector3f(0, -radius, 0);
        v.normal = new Vector3f(0, interior ? 1 : -1, 0);
        v.texCoord = new Vector2f(0.5f, 0.0f);

        i++;

        // 北极点
        v = vertexes[i] = new Vertex();
        v.position = new Vector3f(0, radius, 0);
        v.normal = new Vector3f(0, interior ? -1 : 1, 0);
        v.texCoord = new Vector2f(0.5f, 1.0f);
    }

    /**
     * 计算顶点索引
     */
    private void createIndexBuffer() {
        this.triCount = 2 * (latCount - 2) * lonCount;
        this.indexes = new int[3 * triCount];

        // 生成三角形
        int index = 0;
        for (int y = 0, yStart = 0; y < (latCount - 3); y++) {
            int i0 = yStart;
            int i1 = i0 + 1;
            yStart += (lonCount + 1);
            int i2 = yStart;
            int i3 = i2 + 1;
            for (int i = 0; i < lonCount; i++, index += 6) {
                if (!interior) {
                    indexes[index] = i0++;
                    indexes[index+1] = i2;
                    indexes[index+2] = i1;
                    indexes[index+3] = i1++;
                    indexes[index+4] = i2++;
                    indexes[index+5] = i3++;
                } else { // 内部
                    indexes[index] = i0++;
                    indexes[index+1] = i1;
                    indexes[index+2] = i2;
                    indexes[index+3] = i1++;
                    indexes[index+4] = i3++;
                    indexes[index+5] = i2++;
                }
            }
        }

        // 南极点
        for (int i = 0; i < lonCount; i++, index += 3) {
            if (!interior) {
                indexes[index] = i;
                indexes[index+1] = i + 1;
                indexes[index+2] = vertCount - 2;
            } else { // 内部
                indexes[index] = i;
                indexes[index+1] = vertCount - 2;
                indexes[index+2] = i + 1;
            }
        }

        // 北极点
        int iOffset = (latCount - 3) * (lonCount + 1);
        for (int i = 0; i < lonCount; i++, index += 3) {
            if (!interior) {
                indexes[index] = i + iOffset;
                indexes[index+1] = vertCount - 1;
                indexes[index+2] = i + 1 + iOffset;
            } else { // 内部
                indexes[index] = i + iOffset;
                indexes[index+1] = i + 1 + iOffset;
                indexes[index+2] = vertCount - 1;
            }
        }
    }
}

测试球体网格

写个测试用例,检查一下Sphere的形状。

package net.jmecn.examples;

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.Mesh;
import net.jmecn.scene.shape.Sphere;
import net.jmecn.shader.UnshadedShader;

/**
 * 测试Sphere网格
 * @author yanmaoyuan
 *
 */
public class TestSphere extends Application {

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

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

        // 创建网格
        Mesh mesh = new Sphere(2f, 36, 32);

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

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

        try {
            material.setDiffuseMap(new Texture(new Image("res/earth.jpg")));
        } catch (Exception e){
            material.setDiffuseMap(new Texture());
        }

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

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

    @Override
    protected void update(float delta) {}
}

效果如下:

设置为线框模式,看看网格的形状。

material.getRenderState().setFillMode(FillMode.LINE);

效果如下:

关闭背面剔除,并设为顶点模式。

material.getRenderState().setCullMode(CullMode.NEVER);
material.getRenderState().setFillMode(FillMode.POINT);

效果如下:

Blinn-Phong着色

Phong着色是在fragment shader中完成的,看起来比Gouraud着色更加平滑。Blinn在Phong着色的基础上,把高光的计算方式从反射向量改成了半角向量,从而提高了执行效率。

为了实现Blinn-Phong着色,我需要在fragmentShader中拿到插值后的顶点坐标,而且是位于世界空间中的顶点坐标。目前这个数据是没有的,因为RasterizationVertex中的position在进入fragment shader之前已经被变换到了屏幕空间,不适合使用了。

为此,我要在RasterizationVertex中再增加一个 worldSpacePosition 成员,并在interpolateLocal方法中完成线性插值,在perspectiveDivide方法中完成透视校正。

// 顶点在世界空间中的模型坐标
public Vector3f worldSpacePosition = new Vector3f();

/**
 * 插值
 * @param 
 * @param v1
 * @param t
 * @return
 */
public RasterizationVertex interpolateLocal(RasterizationVertex v0,
        RasterizationVertex v1, float t) {
    // 顶点插值
    position.interpolateLocal(v0.position, v1.position, t);
    // 法线插值
    normal.interpolateLocal(v0.normal, v1.normal, t);
    // 颜色插值
    color.interpolateLocal(v0.color, v1.color, t);
    // 纹理插值
    texCoord.interpolateLocal(v0.texCoord, v1.texCoord, t);

    worldSpacePosition.interpolateLocal(v0.worldSpacePosition, v1.worldSpacePosition, t);
    return this;
}

/**
 * 透视除法
 */
public void perspectiveDivide() {
    float oneOverW = 1f / position.w;
    // 透视除法
    position.multLocal(oneOverW);
    texCoord.multLocal(oneOverW);
    color.multLocal(oneOverW);
    normal.multLocal(oneOverW);
    // 记录1 / w
    position.w = oneOverW;

    worldSpacePosition.multLocal(oneOverW);
}

在SoftwareRaster调用着色器之前,要把worldSpacePosition的值修正。

/**
 * 光栅化点
 * @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;
    }

    // 透视投影修正..

    frag.worldSpacePosition.multLocal(w);

    // 执行片段着色器
    if ( !shader.fragmentShader(frag) )
        return;
    // ...
}

然后实现Blinn-Phong着色器。

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;

/**
 * Blinn-Phong着色器
 * @author yanmaoyuan
 *
 */
public class BlinnPhongShader 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 frag
     * @param light
     */
    private Vector3f lighting(RasterizationVertex frag, 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(frag.worldSpacePosition);

            // 顶点法线
            normal.set(frag.normal);

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

            // 计算顶点到眼睛的方向向量
            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(vertex.position, out.worldSpacePosition);
        // 模型-观察-透视 变换
        worldViewProjectionMatrix.mult(out.position, out.position);

        return out;
    }

    @Override
    public boolean fragmentShader(RasterizationVertex frag) {

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

        Texture texture = material.getDiffuseMap();
        if (texture != null) {
            Vector4f texColor = texture.sample2d(frag.texCoord);
            frag.color.multLocal(texColor);
        }

        return true;
    }

}

测试Blinn-Phong着色器

写个新的测试用例,用Sphere网格来测试一下Blinn-Phong着色器。

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.Mesh;
import net.jmecn.scene.shape.Sphere;
import net.jmecn.shader.BlinnPhongShader;

/**
 * 测试Blinn-Phong Shader
 * @author yanmaoyuan
 *
 */
public class TestPhongShader extends Application {

    public static void main(String[] args) {
        TestPhongShader app = new TestPhongShader();
        app.setResolution(400, 300);
        app.setTitle("Test Blinn-Phong 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);

        // 设置场景
        setupScene();

        // 设置光源
        setupLights();
    }

    /**
     * 设置场景
     */
    private void setupScene() {
        // 创建网格
        Mesh mesh = new Sphere(1f, 32, 32);

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

        // 设置颜色
        material.setDiffuse(new Vector4f(1, 0, 0, 1));
        material.setAmbient(new Vector4f(1, 0, 0, 1));
        material.setSpecular(new Vector4f(1, 1, 1, 1));
        material.setShininess(16);

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

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

    /**
     * 设置光源
     */
    private void setupLights() {
        // 环境光
        AmbientLight ambient = new AmbientLight();
        ambient.setColor(new Vector4f(1, 1, 1, 0.3f));

        // 定向光
        DirectionalLight dl = new DirectionalLight();
        dl.setColor(new Vector4f(1, 1, 1, 0.7f));
        dl.setDirection(new Vector3f(-3,-4,-5).normalizeLocal());

        lights.add(ambient);
        lights.add(dl);
    }

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

运行效果:

总结

目标达成。