随着游戏中时间的变化,会有日出和日落,也会有四季变化。不同的时刻,太阳光照的角度不同,人在地上的影子方向也不一样。比如夏天太阳高度会高一些,冬天则会低一些。
我把游戏世界的位置定位在地球北纬30°(跟武汉一样),那么一年中,春分和秋分两个节气的中午12时,太阳日照高度就是60°。夏至和冬至的太阳高度则要分别加减黄赤夹角(23°26′),为83.5°和36.5°。
我假设游戏中地球公转轨迹是一个非常完美的圆形,那么太阳的高度变化正好是一个正弦函数。地球公转第day天,那么转过的角度就是:
引用
beta = 2 * 3.1415926 * day / 360,
日照高度theta为:
引用
theta = 60°+ 23.5°* sin(beta)。
废话不多说了,上代码。
package org.pstale.utils; import com.jme3.math.Vector3f; /** * 游戏中的时间系统 * @author yanmaoyuan * */ public class GameDate { // 我们假设游戏中时间流动的速度是现实的30倍! public final static int GAME_MINUTE = 2;// 游戏1分钟 = 现实2秒 public final static int GAME_HOUR = 120;// 游戏1小时 = 60游戏分钟 = 现实2分钟 public final static int GAME_DAY = 2880;// 游戏1天 = 24游戏小时 = 现实48分钟 public final static int GAME_MONTH = 86400;// 游戏1月 =30游戏天 = 现实1天 public final static int GAME_YEAR = 1036800;// 游戏1年 = 12游戏月 = 现实12天 // 地球黄赤交角为23°26′ private final static double ECLIPTIC_OBLIQUITY = Math.PI * 23.5f / 180f; // 假设世界的纬度为30°N,春分时中午12点太阳高度为60° private final static double WUHAN_LATITUDE = Math.PI * 60f / 180f; private final static double SEC_DEG = 2 * Math.PI / GAME_DAY;// 地球每秒转动的角度 private final static double SIN_HOUR_DEG = Math.sin(Math.PI / 12);// 日出一小时的高度 private long totalSec;// 游戏从开始到现在总共经过的秒数 private int year_sec; private int month_sec; private int date_sec; private int hour_sec; private int year;// 年份>=0 private int month;// 月份 [0~11] private int date;// 日期 [0~29] private int day;// 一年中的第几天[0~359] private int hour;// 小时[0~23] private int minute;// 分钟[0~59] private double alpha;// 我们假设6点钟日出,α代表时针相对于6点钟的位置。 private double theta;// 我们假设一年每个月正午12点阳光的高度为θ public GameDate() { totalSec = 0l; sunDirection = new Vector3f(); updateTime(); } public GameDate(long lastTime) { totalSec = lastTime; sunDirection = new Vector3f(); updateTime(); } public void update() { totalSec++; updateTime(); } private void updateTime() { //////////// 年月日 year_sec = (int) (totalSec % GAME_YEAR);// 游戏中的一年过了多少秒。 year = (int) (totalSec / GAME_YEAR);// 经过了几年了? day = year_sec / GAME_DAY;// 这是一年的第几天? month_sec = year_sec % GAME_MONTH;// 游戏中一个月过了多少秒 month = year_sec/GAME_MONTH;// 经过了几个月了? date_sec = month_sec % GAME_DAY;// 游戏中的一天过了多少秒 date = month_sec/GAME_DAY;// 经过了几天了? /////////////时分秒 hour_sec = date_sec % GAME_HOUR;// 游戏中的一小时过了多少秒 hour = date_sec / GAME_HOUR;// 今天过了几小时了? minute = (int) (hour_sec / GAME_MINUTE);// 一小时过了几分钟了? // 我们假设6点钟日出,α代表时针相对于6点钟的位置。 alpha = SEC_DEG * (date_sec - GAME_HOUR * 6);// 根据一天的时间,计算时钟的角度 updateDayAndNight(); theta = getTheta();// 根据地球公转的角度,计算日照高度。 updateSunDirection(); } /** * 游戏从开始到现在总共经过的秒数 * @return */ public long currentTimeInSecond() { return totalSec; } /** * 下面来计算太阳高度。太阳每天升起的高度都不一样,随地球公转而变化。 */ public double getTheta() { // 春分是春三月的中节,因此日期要回退45天 double year_angle = Math.PI * 2 * (day - 45) / 360; // 世界的实际日照角度为 this.theta = WUHAN_LATITUDE + ECLIPTIC_OBLIQUITY * Math.sin(year_angle); return theta; } private float lightPower;// 光照强度 private boolean isDay;// 是否是白天 public void updateDayAndNight() { // 计算阳光强度 // 日出和日落时,太阳的亮度会渐变,当高度达到PI/6的时候,天就整个亮了。 // 让日出时间提前1个小时,让日落时间推后1个小时。 lightPower = (float) ((Math.sin(alpha) + SIN_HOUR_DEG) * 2); if (lightPower > 1f) lightPower = 1f; if (lightPower < 0) {// 太阳落下了 lightPower = 0f; if (isDay) isDay = false;// 黑夜 } else { if (!isDay) isDay = true;// 白天 } } public float getLightPower() { return lightPower; } public boolean isDay() { return isDay; } private Vector3f sunDirection;// 光照角度 /** * 下面来计算光照角度 */ public void updateSunDirection() { double x = -Math.cos(alpha); double y = -Math.sin(alpha) * Math.sin(theta); double z = -Math.sin(alpha) * Math.cos(theta); sunDirection.set((float) x, (float) y, (float) z); } public Vector3f getSunDirection() { return sunDirection; } public int getYear() { return year; } public int getMonth() { return month; } public int getDate() { return date; } public int getHour() { return hour; } public int getMinute() { return minute; } }
然后我们写一个实例代码,来看看日出日落的效果。
注意:下例中所有模型,都来源于JME3自带的例子。
package org.pstale.utils; import com.jme3.app.SimpleApplication; import com.jme3.font.BitmapFont; import com.jme3.font.BitmapText; import com.jme3.light.AmbientLight; import com.jme3.light.DirectionalLight; import com.jme3.material.Material; import com.jme3.math.ColorRGBA; import com.jme3.math.FastMath; import com.jme3.math.Quaternion; import com.jme3.math.Vector2f; import com.jme3.math.Vector3f; import com.jme3.renderer.queue.RenderQueue; import com.jme3.renderer.queue.RenderQueue.ShadowMode; import com.jme3.scene.Geometry; import com.jme3.scene.Node; import com.jme3.scene.Spatial; import com.jme3.scene.shape.Box; import com.jme3.scene.shape.Cylinder; import com.jme3.shadow.BasicShadowRenderer; import com.jme3.shadow.DirectionalLightShadowRenderer; import com.jme3.texture.Texture.WrapMode; import com.jme3.util.TangentBinormalGenerator; /** * 游戏时间以及昼夜系统测试 * @author yanmaoyuan * */ public class TestGameDate extends SimpleApplication { private GameDate gameDate;// 游戏时间 private DirectionalLight sunLight;// 太阳光 private BitmapText gui;// 用来显示游戏时间 private Geometry sunBox;// 用一个小方块来模拟代表太阳 @Override public void simpleInitApp() { // 初始化游戏时间 gameDate = new GameDate(); // 初始化镜头 cam.setLocation(new Vector3f(27.492603f, 29.138166f, -13.232513f)); cam.setRotation(new Quaternion(0.25168246f, -0.10547892f, 0.02760565f, 0.96164864f)); flyCam.setMoveSpeed(30); // 我们创建一个红色小方块,用来代表太阳,它会根据时间的变化而移动。 Box box = new Box(5,5,5); sunBox = new Geometry("Box", box); Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); mat.setColor("Color", ColorRGBA.Red); sunBox.setMaterial(mat); sunBox.setShadowMode(ShadowMode.Off); rootNode.attachChild(sunBox); setupGui(); setupLighting(); setupFloor(); setupSignpost(); } /** * 创建gui,用来显示时间 */ public void setupGui() { BitmapFont guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt"); gui = new BitmapText(guiFont, false); gui.setText("00:00"); guiNode.attachChild(gui); // 把gui放在屏幕顶部居中 float width = (settings.getWidth() - gui.getLineWidth())/2; float height = settings.getHeight(); gui.setLocalTranslation(width, height, 0); } /** * 创建光源 */ public void setupLighting() { // 阳光 sunLight = new DirectionalLight(); sunLight.setColor(ColorRGBA.White.clone()); sunLight.setDirection(gameDate.getSunDirection()); rootNode.addLight(sunLight); // 设置一个很淡的环境光 AmbientLight al = new AmbientLight(); al.setColor(ColorRGBA.White.mult(0.3f)); rootNode.addLight(al); rootNode.setShadowMode(ShadowMode.CastAndReceive); // 阳光产生影子全靠这玩意了! DirectionalLightShadowRenderer dlsr = new DirectionalLightShadowRenderer(assetManager, 512, 4); dlsr.setLight(sunLight); viewPort.addProcessor(dlsr); } /** * 创建一个地板,这样我们才能看见影子。 */ public void setupFloor() { Material mat = assetManager.loadMaterial("Textures/Terrain/Pond/Pond.j3m"); mat.getTextureParam("DiffuseMap").getTextureValue().setWrap(WrapMode.Repeat); mat.getTextureParam("NormalMap").getTextureValue().setWrap(WrapMode.Repeat); mat.setBoolean("UseMaterialColors", true); mat.setColor("Diffuse", ColorRGBA.White.clone()); mat.setColor("Ambient", ColorRGBA.White.clone()); // mat.setColor("Specular", ColorRGBA.White.clone()); // mat.getTextureParam("ParallaxMap").getTextureValue().setWrap(WrapMode.Repeat); mat.setFloat("Shininess", 0); // mat.setBoolean("VertexLighting", true); Box floor = new Box(100, 1f, 100); TangentBinormalGenerator.generate(floor); floor.scaleTextureCoordinates(new Vector2f(5, 5)); Geometry floorGeom = new Geometry("Floor", floor); floorGeom.setMaterial(mat); floorGeom.setShadowMode(ShadowMode.Receive);// 地板只接受影子,不产生影子。 rootNode.attachChild(floorGeom); } /** * 创建一个sign,我们可以看到它在阳光下的影子。 */ public void setupSignpost() { Spatial signpost = assetManager.loadModel("Models/Sign Post/Sign Post.mesh.xml"); Material mat = assetManager.loadMaterial("Models/Sign Post/Sign Post.j3m"); signpost.setMaterial(mat); signpost.rotate(0, FastMath.HALF_PI, 0); signpost.setLocalTranslation(0, 3.5f, 0); signpost.setLocalScale(4); signpost.setShadowMode(ShadowMode.CastAndReceive); TangentBinormalGenerator.generate(signpost); rootNode.attachChild(signpost); } @Override public void simpleUpdate(float tpf) { // 更新游戏时间 gameDate.update(); // 更新gui,显示当前时间 gui.setText(String.format("%02d:%02d", gameDate.getHour(), gameDate.getMinute())); // 更新阳光亮度 float power = gameDate.getLightPower(); sunLight.setColor(ColorRGBA.White.clone().mult(power)); // 更新光照角度 sunLight.setDirection(gameDate.getSunDirection()); sunBox.setLocalTranslation(gameDate.getSunDirection().mult(-100f)); } public static void main(String[] args) { TestGameDate app = new TestGameDate(); app.start(); } }