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);
}
}
运行效果:
总结
目标达成。