话说上一篇文章结尾讲到这一篇要做一个地球自转以及月球公转的三维动画,提笔,不对,是提键盘开始写的时候脑海中突然出现了几年前春晚风靡的那首歌:蒙古族小丫头唱的快乐的一家。闲言莫提,进入正题。
场景涉及两个对象,一个是地球、一个是月球,当然这基本是废话,不过还可以再添加一个对象,月球的公转轨迹。地球和月球都可以用一个球来模拟(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.getModel = function () {
- var mod = new PhiloGL.O3D.Sphere({
- nlat: 30,
- nlong: 30,
- radius: this.radius,
- textures: ['img/moon.gif'],
- colors: [1, 1, 1, 1],
- program: 'vertex'
- });
- return mod;
- };
- this.drawCircle = function (app) {
- program = app.program;
- gl = app.gl;
- var res = this.getRoute();
- program.circle.setBuffers({
- 'aVertexPosition': {
- value: new Float32Array(res.points),
- size: 3
- },
- 'aVertexColor': {
- value: new Float32Array(res.colors),
- size: 4
- }
- });
- program.circle.setUniform('uMVMatrix', camera.view);
- program.circle.setUniform('uPMatrix', camera.projection);
- gl.drawArrays(gl.LINES, 0, res.points.length / 3);
- };
- 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 (theta) {
- x = this.globeRadius * Math.cos(theta) * Math.cos(this.sigmaY);
- y = this.globeRadius * Math.cos(theta) * Math.sin(this.sigmaY);
- z = this.globeRadius * Math.sin(theta);
- return {'x': x, 'y': y, 'z': z};
- }
- this.getRealPosition = function () {
- return this.getPosition(this.theta);
- }
- };
radius表示卫星的半径, theta表示公转的角度, speed表示公转速度, sigmaY表示公转轨道与Y轴的夹角, color表示公转轨道颜色, globeRadius表示公转轨道半径。
getModel函数用于获取卫星实体对象;drawCircle函数用于绘制公转轨道;getRoute函数获取公转轨道信息,包括点位信息和颜色信息;getPosition函数用于计算当公转角度为theta时的位置坐标,其坐标值计算是在上文分析的基础上加入了Y轴旋转角度的影响;getRealPosition函数获取卫星公转实时位置信息。
其调用方法如下:
- // 创建
- var sat1 = new Satllite(1, 0, 0.01, Math.PI, [1, 1, 1, 1], 30);
- var moon1 = sat1.getModel();
- // animate中修改公转角度
- sat3.theta -= sat3.speed;
- // drawScene中绘制轨道以及更新位置
- sat3.drawCircle(app);
- var pos1 = sat1.getRealPosition();
- moon1.position.set(pos1.x, pos1.y, pos1.z);
- moon1.update();
多次按上述代码调用就能创建多个卫星对象,让地球的大家庭更加丰满,所以有了此类,只要知道了微信轨道参数我们就能模拟出来地球外部的全部人造卫星,当然这还只是简单的情况,复杂的情况就要将轨道变成椭圆等等了。
眼尖的人应该可以看到我效果图中的地球没有公转,其实不是我骗人忽悠大家,只是让我诧异的是如果地球自转了,所有的轨道都会跟着地球转动,但是卫星并不跟着转,目前我还没有分析出来是什么原因,大概是由于摄像机造成的,还不能确定。还有一个问题就是轨道颜色并没有随着我改变而改变,应该是drawArray以及GLSL造成的,关于此两点有知道的,欢迎指点。
本文简单介绍了绘制自转的地球以及公转的月球,看似很简单,其实中间有很多的坑,当然是因为自己确实水平有限,然而这正是我做此场景的本意,当然做完之后更加深刻的感受到了这一点。你看地球自转多么简单,月球公转多么简单,而人类才是地球上多么微不足道的一点点,你把自己搞那么复杂干什么,面对着永不停息转动的地球和月球你感受不到自己的渺小吗?再上升到整个宇宙不敢想象!所以,迈开自己的腿,多去看看更大的世界,不求能出得了宇宙,只求能够在有生之年走遍大部分地球,做一个见多识广的程序员,有一个快乐的一家!本系列文章写到这里,已经基本结束,后面如果有新的感悟也会继续写出。
来源: http://www.cnblogs.com/shoufengwei/p/7755199.html