目标
- 定义3D网格的数据结构
- 使用世界坐标变换,绘制网格。
实现
网格
前面已经基本完成了3D数学库,现在来定义3D模型的数据结构。
package net.jmecn.scene;
import net.jmecn.geom.Drawable;
import net.jmecn.math.ColorRGBA;
import net.jmecn.math.Matrix4f;
import net.jmecn.math.Transform;
import net.jmecn.math.Vector3f;
import net.jmecn.renderer.ImageRaster;
/**
* 定义三角形网格
* @author yanmaoyuan
*
*/
public class Mesh implements Drawable {
/**
* 顶点数组
*/
private Vector3f[] positions;
/**
* 顶点索引
*/
private int[] indexes;
/**
* 空间变换
*/
private Transform transform;
/**
* 初始化三角网格。
*
* @param vertexes
* @param indexes
*/
public Mesh(Vector3f[] positions, int[] indexes) {
this.positions = positions;
this.indexes = indexes;
this.transform = new Transform();
}
public Transform getTransform() {
return transform;
}
@Override
public void draw(ImageRaster imageRaster) {
}
}
这个定义很简单,Mesh类中保存了2个数组,分别是网格中顶点的位置数组,以及将它们组合成三角形的索引数组。
除此之外,还定义了一个Transform对象,用来表示模型在3D空间中的空间变换。由于目前不考虑复杂的场景,可以认为这个Transform就代表了模型的世界空间变换。
画图
实现 Drawable 接口,把这个网格画出来。
@Override
public void draw(ImageRaster imageRaster) {
// 世界变换矩阵
Matrix4f worldMat = transform.toTransformMatrix();
// 用于保存变换后的向量坐标。
Vector3f v1 = new Vector3f();
Vector3f v2 = new Vector3f();
Vector3f v3 = new Vector3f();
// 遍历所有三角形
for (int i = 0; i < indexes.length - 2; i += 3) {
int a = indexes[i];
int b = indexes[i + 1];
int c = indexes[i + 2];
// 画三角形
Vector3f va = positions[a];
Vector3f vb = positions[b];
Vector3f vc = positions[c];
// 世界空间变换
worldMat.mult(va, v1);
worldMat.mult(vb, v2);
worldMat.mult(vc, v3);
// 画三角形
imageRaster.drawTriangle((int)v1.x, (int)v1.y, (int)v2.x, (int)v2.y, (int)v3.x, (int)v3.y, ColorRGBA.WHITE);
}
}
如你所见,这个draw方法非常……简陋。它仅包含一个世界空间变换,用来把顶点坐标从 模型空间(Model Spcae) 转换到 世界空间(World Space)。再就是遍历网格中所有的三角形,然后用光栅器把三角形画出来。
测试用例
我等不及要写个测试用例来看看实际的效果了。在下面这段代码中,定义了一个简单的正方形。它的左下角位于 模型空间 的原点,边长为1。
package net.jmecn.examples;
import net.jmecn.Application;
import net.jmecn.math.Vector3f;
import net.jmecn.scene.Mesh;
/**
* 测试世界变换矩阵
* @author yanmaoyuan
*
*/
public class Test3DWorldMatrix extends Application {
public static void main(String[] args) {
Test3DWorldMatrix app = new Test3DWorldMatrix();
app.setResolution(400, 400);
app.setTitle("3D WorldMatrix");
app.setFrameRate(60);
app.start();
}
private Mesh mesh;// 网格
@Override
protected void initialize() {
// 一个四边形的顶点
Vector3f[] positions = {
new Vector3f(0, 0, 0),
new Vector3f(1, 0, 0),
new Vector3f(1, 1, 0),
new Vector3f(0, 1, 0),
};
// 定义两个三角形
int[] indexes = {
0, 1, 2,
0, 2, 3,
};
mesh = new Mesh(positions, indexes);
// 初始化空间变换
mesh.getTransform().setTranslation(200, 200, 0);
mesh.getTransform().setScale(50);
// 添加到场景中
scene.add(mesh);
}
@Override
protected void update(float delta) {
// TODO
}
}
由于目前我没有实现摄像机,没有视口透视矩阵,因此场景中的一切看起来都是扁平的,而且长度单位与屏幕的单位一样,是像素。边长为1的正方形,在画面上看起来就只是一个像素点而已。
为了能够观察到这个正方形,我使用Transform来对它进行了比例变换,将其放大了50倍,这样就能在画面上看到一个边长为50像素的正方形了。我还使用了一个平移变换,把这个正方形的原点移动到了(200, 200, 0)处。
运行程序,可以看到这样的画面:
这个结果与我想象的有点不一样,但是符合实际情况。我定义正方形时使用的是OpenGL坐标系,原点在xoy平面的左下角,z轴指向我。而在Canvas中绘图时,xoy平面的原点在左上角,z轴指向屏幕深处。因此这个正方形看起来像是被垂直翻转了。
这没关系,可以用**屏幕空间矩阵(Screen Space Matrix)**来修正。
修正的工作以后再做,我觉得现在这个例子并不能体现世界变换的作用。得让程序更复杂一些,否则感觉我简直白写数学库了。
改进测试用例
我决定让这个正方形动起来。
我定义了一个angle变量来计算旋转角(弧度),然后在update方法中计算了这个正方形的旋转变换(旋转角度)和平移变换(位置)。
private final static float PI = 3.1415626f;
private final static float _2PI = PI * 2;
private float angle = 0;// 旋转角度
@Override
protected void update(float delta) {
// 每秒旋转180°
angle += delta * PI;
// 若已经旋转360°,则减去360°。
if (angle > _2PI) {
angle -= _2PI;
}
// 计算位移:以(200, 200)为中心,半径100做圆周运动。
float x = (float) (Math.cos(angle) * 100 + 200);
float y = (float) (Math.sin(angle) * 100 + 200);
mesh.getTransform().setTranslation(x, y, 0);
// 计算旋转:绕Z轴顺时针方向旋转
mesh.getTransform().getRotation().fromAxisAngle(Vector3f.UNIT_Z, -angle);
}
运行一下,看看效果:
WTF!?虽然看起来蛮酷炫的,但这并是我想要的东西。
这看起来是因为没有清理画面导致的。检查一下ImageRaster中的fill方法,发现了问题:if语句里面不应该用 “与” 判断,而应该用 “或” 。
public void fill(ColorRGBA color) {
int length = width * height;
for (int i = 0; i < length; i++) {
int index = i * 4;
// 使用一个判断,避免无谓的赋值。
if (components[index] != color.r && components[index + 1] != color.g && components[index + 2] != color.b
|| components[index + 3] != color.a) {
components[index] = color.r;
components[index + 1] = color.g;
components[index + 2] = color.b;
components[index + 3] = color.a;
}
}
}
犯这种错真丢人。。赶紧改过来。
public void fill(ColorRGBA color) {
int length = width * height;
for (int i = 0; i < length; i++) {
int index = i * 4;
// 使用一个判断,避免无谓的赋值。
if (components[index] != color.r || components[index + 1] != color.g || components[index + 2] != color.b
|| components[index + 3] != color.a) {
components[index] = color.r;
components[index + 1] = color.g;
components[index + 2] = color.b;
components[index + 3] = color.a;
}
}
}
重新运行一下:
总结
本章定义了一个最基本的网格数据结构,并且实现了世界坐标变换。
不过目前看来,这个3D世界太“平”了,下一节将正式引入摄像机,实现一系列空间变换了。