话说上一篇文章结尾讲到这一篇要做一个地球自转以及月球公转的三维动画,提笔,不对,是提键盘开始写的时候脑海中突然出现了几年前春晚风靡的那首歌:蒙古族小丫头唱的快乐的一家。闲言莫提,进入正题。
场景涉及两个对象,一个是地球、一个是月球,当然这基本是废话,不过还可以再添加一个对象,月球的公转轨迹。地球和月球都可以用一个球来模拟(Sphere),稍微困难的是公转轨迹,公转轨迹是一个圆,PhiloGL貌似没有直接提供圆的封装,但是有画线段的API,细细想来,什么是圆?祖冲之早就告诉我们了,所谓圆不过是多边形的无限逼近,那么我们就可以用多条细小的线段来逼近圆。地球自转很简单,而月球的公转就如同公转轨迹一样,只要将月球的位置设置到公转轨道上即可。
有了上述分析之后,我们就可以做出地球和月球的完美曲线来了。整体效果如下图所示:
创建整体场景即新建PhiloGL对象,配置GLSL、摄像头、灯光、贴图等。
PhiloGL('test1', {
program: [{
id: 'vertex',
from: 'defaults'
}, {
id: 'circle',
from: 'ids',
vs: 'shader-vs',
fs: 'shader-fs'
}],
camera: {
position: {
x: 0, y: 60, z: 60
}
},
textures: {
src: ['earth.jpg', 'moon.gif'],
},
onError: function (e) {
alert(e);
},
onLoad: function (app) {
var gl = app.gl,
canvas = app.canvas,
program = app.program,
camera = app.camera,
scene = app.scene,
view = new PhiloGL.Mat4;
function clear() {
gl.viewport(0, 0, +canvas.width, +canvas.height);
gl.clearColor(0, 0, 0, 1);
gl.clearDepth(1);
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
}
clear();
function animate() {
......
}
function drawScene() {
var lightConfig = scene.config.lights;
lightConfig.enable = true;
lightConfig.ambient = {
r: +ambient.r,
g: +ambient.g,
b: +ambient.b
};
// 线光源
lightConfig.directional.direction = {
x: +light.x,
y: +light.y,
z: +light.z
};
lightConfig.directional.color = {
r: +light.r,
g: +light.g,
b: +light.b
};
......
}
function tick() {
animate();
drawScene();
scene.render();
PhiloGL.Fx.requestAnimationFrame(tick);
}
tick();
}
});
其中program下面包含两个glsl,第一个vertex是PhiloGL提供的默认GLSL,用于地球和月球。第二个circle用于月球公转轨道,定义如下:
<script id="shader-fs" type="x-shader/x-fragment">
#ifdef GL_ES
precision highp float;
#endif
varying vec4 vColor;
void main(void) {
gl_FragColor = vColor;
}
</script>
<script id="shader-vs" type="x-shader/x-vertex">
attribute vec3 aVertexPosition;
attribute vec4 aVertexColor;
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;
varying vec4 vColor;
void main(void) {
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
vColor = aVertexColor;
}
</script>
摄像头、灯光、贴图前面几篇文章已经介绍过,这里与之前基本相同,不再赘述。
创建Sphere对象并设置地球的贴图。代码如下:
var earth = new PhiloGL.O3D.Sphere({
nlat: 30,
nlong: 30,
radius: 5,
textures: ['earth.jpg'],
colors: [1, 1, 1, 1],
program: 'vertex'
});
地球自转,位置无需变化,只需要随着时间让旋转角度递增即可。在外部设置旋转角度和旋转变量,此处旋转简单起见以Y轴为旋转轴,在animate方法中对旋转角度进行累加,在drawScene方法中设置earth对象进行旋转。
//外部设置旋转变量
var yRot = 1, ySpeed = 0.1;
// animate中旋转累加
yRot += ySpeed;
// drawScene中设置earth的旋转
earth.rotation.set(0, yRot, 0);
earth.position.set(0, 0, 0);
earth.update();
这样就可实现地球的自转。
同样创建Sphere对象。
var moon = new PhiloGL.O3D.Sphere({
nlat: 30,
nlong: 30,
radius: 1,
textures: ['moon.gif'],
colors: [1, 1, 1, 1],
program: 'vertex'
});
公转,只需要改变月球位置即可,让其位置处在公转圆上。
// 外部设置公转变量
var theta = 0, tSpeed = ySpeed / 30,
moon_x, moon_z, r = 30;
// animate中设置公转位置
theta -= tSpeed;
moon_x = r * Math.cos(theta);
moon_z = r * Math.sin(theta);
// drawScene中设置moon公转
moon.position.set(moon_x, 0, moon_z);
moon.update();
其中theta表示公转角度、tSpeed表示公转速度,其速度为地球自转速度的1/30,这里假设月球公转周期为30天,所以其公转速度为地球自转速度的1/30。地球以Y轴为旋转轴,假设月球的公转平面为XOZ平面,即Y值为0。根据三角函数可知,当旋转角度为θ时,X值为r cos(θ),Z值为r sin(θ),其中r为公转半径。
有了上面几部分的分析之后,公转轨道也很容易了。首先,先来解释一下PhiloGL绘制线段的原理。
PhiloGL使用gl.drawArrays(gl.LINES, 0, count)来绘制多条线段,其中count表示线段的数量,当然这里需要使用之前的GLSL知识,我们要为aVertexPosition和aVertexColor这两个attribute变量赋值。如下:
program.circle.setBuffers({
'aVertexPosition': {
value: new Float32Array(points),
size: 3
},
'aVertexColor': {
value: new Float32Array(colors),
size: 4
}
});
其中points表示线段的点的集合,colors表示线段的端点的颜色,一条线段由两个端点组成,颜色也是由两个端点的颜色渐变而成。所以如果需要绘制连续的线段那么必须要将除首尾端点外全部重复,否则会造成线段断开。而此处绘制的是个封闭的圆,那么必须要在最后一条线段后再添加最后一个点到第一个点的线段,这样才能形成一个封闭的圆,颜色同样如此。代码如下:
function createRoute() {
var points = [];
var colors = [];
for (var t = 0; t < Math.PI * 2; t += 0.01) {
if (t == 0) {
points.push(r * Math.cos(t), 0, r * Math.sin(t));
colors.push(1,1,1,1);
} else {
points.push(r * Math.cos(t), 0, r * Math.sin(t));
points.push(r * Math.cos(t), 0, r * Math.sin(t));
colors.push(1,1,1,1);
colors.push(1,1,1,1);
}
}
points.push(r * Math.cos(0), 0, r * Math.sin(0));
colors.push(1,1,1,1);
return {"points": points, "colors": colors};
}
points存储所有点的集合,colors存储点的颜色的集合。第一个点仅存一次,其余点存两次,当循环结束后再将第一个点存入其中。将其结果赋给上面setBuffers中的两个变量。
设置好后,在drawScene中执行gl.drawArrays(gl.LINES, 0, count)即可。
写到这里,本来已经完事了,奈何程序员总是有强迫症的,你瞅瞅这月球、这轨道,这明显就是一个大对象嘛,那我必须要对其进行封装,变成卫星。将卫星半径、卫星轨道、公转速度、公转轨道夹角、轨道颜色等封装起来。整体代码如下:
function Satllite(radius, theta, speed, sigmaY, color, globeRadius) {
this.sigmaY = sigmaY;
this.color = color;
this.radius = radius;
this.speed = speed;
this.theta = theta;
this.globeRadius = globeRadius;
this.circleLine = null;
this.model = null;
this.circleModel = null;
this.getModel = function () {
if (this.model == null) {
var mod = new PhiloGL.O3D.Sphere({
nlat: 30,
nlong: 30,
radius: this.radius,
textures: ['img/moon.gif'],
colors: [1, 1, 1, 1],
program: 'vertex'
});
this.model = mod;
}
return this.model;
};
this.getCircleModel = function () {
if (this.circleModel == null) {
if (this.circleLine == null) {
this.circleLine = this.getRoute();
}
var res = this.circleLine;
var circle = new PhiloGL.O3D.Model({
program: 'circle',
render: function (gl, program, camera) {
program.setUniform('uMVMatrix', camera.view);
program.setUniform('uPMatrix', camera.projection);
gl.lineWidth(this.lineWidth || 1);
gl.drawArrays(gl.LINES, 0, res.points.length / 3);
},
attributes: {
aVertexPosition: {
size: 3,
value: new Float32Array(res.points)
},
aVertexColor: {
value: new Float32Array(res.colors),
size: 4
}
}
});
this.circleModel = circle;
}
return this.circleModel;
};
this.getRoute = function () {
var points = [];
var colors = [];
for (var t = 0; t < Math.PI * 2; t += 0.05) {
var pos = this.getPosition(t);
if (t == 0) {
points.push(pos.x, pos.y, pos.z);
colors = colors.concat(this.color);
} else {
points.push(pos.x, pos.y, pos.z);
points.push(pos.x, pos.y, pos.z);
colors = colors.concat(this.color);
colors = colors.concat(this.color);
}
}
pos = this.getPosition(0);
points.push(pos.x, pos.y, pos.z);
colors = colors.concat(this.color);
return {"points": points, "colors": colors};
};
this.getPosition = function (thetaX) {
x = this.globeRadius * Math.cos(thetaX) * Math.cos(this.sigmaY);
y = this.globeRadius * Math.cos(thetaX) * Math.sin(this.sigmaY);
z = this.globeRadius * Math.sin(thetaX);
return {'x': x, 'y': y, 'z': z};
};
this.getRealPosition = function () {
return this.getPosition(this.theta);
};
this.updateTheta = function () {
this.theta -= this.speed;
};
this.updateModel = function () {
if (this.model != null) {
var pos = this.getRealPosition();
this.model.position.set(pos.x, pos.y, pos.z);
this.model.update();
this.circleModel.update();
}
};
};
radius表示卫星的半径, theta表示公转的角度, speed表示公转速度, sigmaY表示公转轨道与Y轴的夹角, color表示公转轨道颜色, globeRadius表示公转轨道半径。
getModel函数用于获取卫星实体对象;drawCircle函数用于绘制公转轨道;getRoute函数获取公转轨道信息,包括点位信息和颜色信息;getPosition函数用于计算当公转角度为theta时的位置坐标,其坐标值计算是在上文分析的基础上加入了Y轴旋转角度的影响;getRealPosition函数获取卫星公转实时位置信息;updateTheta函数用于更新卫星的旋转角度;updateModel直接更新卫星位置。
其调用方法如下:
// 创建
var sat = new Satllite(1, 0, 0.01, Math.PI, [1,1,1,1], 30);
sat.getModel();
scene.add(sat.model);// 加入场景
// animate中修改公转角度
sat.updateTheta();
// drawScene中绘制轨道以及更新位置
sat.updateModel();
多次按上述代码调用就能创建多个卫星对象,让地球的大家庭更加丰满,所以有了此类,只要知道了卫星轨道参数我们就能模拟出来地球外部的全部人造卫星,当然这还只是简单的情况,复杂的情况就要将轨道变成椭圆等等了。
之前做的时候轨道总是跟着地球一起旋转,不知什么原因,猜测是camera造成的,但是始终没有解决,后面我尝试将画圆对象封装成Model,结果完美解决了此问题。封装代码如下:
new PhiloGL.O3D.Model({
program: 'circle',
render: function (gl, program, camera) {
program.setUniform('uMVMatrix', camera.view);
program.setUniform('uPMatrix', camera.projection);
gl.lineWidth(this.lineWidth || 1);
gl.drawArrays(gl.LINES, 0, res.points.length / 3);
},
attributes: {
aVertexPosition: {
size: 3,
value: new Float32Array(res.points)
},
aVertexColor: {
value: new Float32Array(res.colors),
size: 4
}
}
});
其中circle为上面定义的GLSL模块,attributes中是vs的两个attribute变量,这个与之前没有区别,一个控制点一个控制颜色。render函数是最终的渲染函数,两个setUniform设置camera,gl.drawArrays(gl.LINES, 0, res.points.length / 3)控制最终绘制,这与之前都没有区别,所以这里只是封装。有了此对象之后,其添加、更新等就与球等对象相同了。
本文简单介绍了绘制自转的地球以及公转的月球,看似很简单,其实中间有很多的坑,当然是因为自己确实水平有限,然而这正是我做此场景的本意,当然做完之后更加深刻的感受到了这一点。你看地球自转多么简单,月球公转多么简单,而人类才是地球上多么微不足道的一点点,你把自己搞那么复杂干什么,面对着永不停息转动的地球和月球你感受不到自己的渺小吗?再上升到整个宇宙不敢想象!所以,迈开自己的腿,多去看看更大的世界,不求能出得了宇宙,只求能够在有生之年走遍大部分地球,做一个见多识广的程序员,有一个快乐的一家!本系列文章写到这里,已经基本结束,后面如果有新的感悟也会继续写出。