实验目的:按照一定规律生成类地行星地表地形区块,并用合理的方式将地形块显示出来

涉及知识:Babylon.js引擎应用、着色器编程、正态分布、数据处理、canvas像素操作

github地址:https://github.com/ljzc002/ljzc002.github.io/tree/master/DataWar

一、在球体网格上显示纹理的传统方法:

1、常见的一种星球表面绘制方法是这样的:

首先用三角形近似的模拟一个球体网格:

使用着色器在WebGL3D场景中呈现行星表面地形-LMLPHP

这个简单场景的代码如下:

 <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>演示球体的绘制</title>
<link href="../../CSS/newland.css" rel="stylesheet">
<link href="../../CSS/stat.css" rel="stylesheet">
<script src="../../JS/LIB/babylon.32.all.max.js"></script>
<script src="../../JS/LIB/stat.js"></script>
</head>
<body>
<div id="div_allbase">
<canvas id="renderCanvas"></canvas>
<div id="fps" style="z-index: 301;"></div>
</div>
</body>
<script>
var canvas,engine,scene,gl;
canvas = document.getElementById("renderCanvas");
engine = new BABYLON.Engine(canvas, true);
gl=engine._gl;//决定在这里结合使用原生OpenGL和Babylon.js;
scene = new BABYLON.Scene(engine);
var divFps = document.getElementById("fps");
//全局对象
var light0//全局光源
,camera0//主相机
;
window.onload=webGLStart;
window.addEventListener("resize", function () {
engine.resize();
});
function webGLStart()
{
gl=engine._gl;
createScene();
MyBeforeRender();
}
var createScene = function (engine) {
camera0 =new BABYLON.FreeCamera("FreeCamera", new BABYLON.Vector3(0, 0, -20), scene);
camera0.attachControl(canvas, true);
camera0.speed=0.5;
camera0.minZ=0.0001;
light0 = new BABYLON.HemisphericLight("Hemi0", new BABYLON.Vector3(0, 1, 0), scene);
sphere1=BABYLON.MeshBuilder.CreateSphere("sphere1",{segments:10,diameter:10.0},scene);
var groundMaterial = new BABYLON.StandardMaterial("groundMat", scene);
groundMaterial.wireframe=true;
sphere1.material=groundMaterial; }
function MyBeforeRender()
{
scene.registerBeforeRender(function() {
if(scene.isReady())
{ }
});
engine.runRenderLoop(function () {
engine.hideLoadingUI();
if (divFps) {
// Fps
divFps.innerHTML = engine.getFps().toFixed() + " fps";
}
scene.render();
}); }
</script>
</html>

然后将一张纹理贴图的纹理坐标对应到球体网格中的每个三角形上,具体原理如下:

使用着色器在WebGL3D场景中呈现行星表面地形-LMLPHP

使用着色器在WebGL3D场景中呈现行星表面地形-LMLPHP

以上内容引用自吴亚峰著《OpenGLES3.x游戏开发》,Babylon.js中的纹理对应规则可以参考https://www.cnblogs.com/ljzc002/p/6884252.html中的代码。

但是这种绘制方式存在以下几个缺点:

a、为了将二维的图片映射到三维的球面上,图片或者纹理坐标必须经过复杂的“拓扑变换”(比如图中的南极洲明显经过了拉伸变换),这导致我们在行星表面点击一个点时,很难直观的将它对应到图片上的某个像素,同时生成适合球面的图片也需要使用专门的工具进行计算。

b、如果把每个三角形作为一个可交互对象,极地区域的可交互对象将过于密集,想象一个回合制战棋游戏,玩家会发现极地区域的格子太密而赤道附近的格子太稀疏。

c、在画面拉近时纹理贴图会因为信息不足出现不受控制的模糊或变形,当然,我们可以在视角拉近时用更多的细节贴图来提供更多的信息,但那就是一个更浩大的工程了。

为了避开上述缺点,我决定采用另一种球体纹理绘制方式。

二、使用自定义着色器绘制自定义纹理:

1、在Babylon.js引擎中使用自定义着色器:

Babylon.js通过“着色器材质”对象提供对自定义着色器的支持:

 var amigaMaterial = new BABYLON.ShaderMaterial("amiga", scene,{
vertexElement: "sh1v4.sh",
fragmentElement: "sh1f4.sh",
},
{
attributes: ["position"],
uniforms: ["worldViewProjection","worldView"]
});
amigaMaterial.setVector4("uColor", new BABYLON.Vector4(0.0,1.0,0.0,1.0));
sphere1.material=amigaMaterial;

其中ShaderMaterial构造方法的第一个参数是材质名称、第二个参数是场景对象、第三个参数标明了顶点着色器和片元着色器的文件名称,参考Babylon.js源码可以看到引擎支持的几种着色器代码对象命名方式:

 Effect.prototype._loadFragmentShader = function (fragment, callback) {
// DOM element ?着色器代码是DOM标签中的内容
if (fragment instanceof HTMLElement) {
var fragmentCode = BABYLON.Tools.GetDOMTextContent(fragment);
callback(fragmentCode);
return;
}
// Base64 encoded ?着色器代码使用了base64编码
if (fragment.substr(0, 7) === "base64:") {
var fragmentBinary = window.atob(fragment.substr(7));
callback(fragmentBinary);
return;
}
// Is in local store ?着色器代码在Babylon.js自带的着色器代码库里
if (Effect.ShadersStore[fragment + "PixelShader"]) {
callback(Effect.ShadersStore[fragment + "PixelShader"]);
return;
}
if (Effect.ShadersStore[fragment + "FragmentShader"]) {
callback(Effect.ShadersStore[fragment + "FragmentShader"]);
return;
}
var fragmentShaderUrl;//着色器代码是一个单独的文件,需要通过Ajax加载
if (fragment[0] === "." || fragment[0] === "/" || fragment.indexOf("http") > -1) {
fragmentShaderUrl = fragment;
}
else {//默认情况下Engine.ShadersRepository = "src/Shaders/";
fragmentShaderUrl = BABYLON.Engine.ShadersRepository + fragment;
}
// Fragment shader
BABYLON.Tools.LoadFile(fragmentShaderUrl + ".fragment.fx", callback);
};

我选择了最后一种方式,将着色器文件放在/src/Shaders/下面,不要忘记给着色器文件添加后缀:

使用着色器在WebGL3D场景中呈现行星表面地形-LMLPHP

第四个参数是需要Babylon.js从内存向显卡传递的“默认”变量,其中attributes里是Babylon.js的各种默认的顶点数据,可以选择Mesh.geometry._vertexBuffers里的以下几种顶点数据传给着色器:

使用着色器在WebGL3D场景中呈现行星表面地形-LMLPHP

这里我只选择了将每个顶点的位置传递给着色器,Babylon.js引擎替我们进行了编译链接着色器程序、绑定缓存等一系列操作(Babylon.js中以“_”开头的变量一般都是在渲染过程中建立的,只有在渲染开始后才有值)。

假设一个网格有1000个顶点,那么这1000个顶点的位置数据将被分别发送到显卡上的1000个顶点着色器中,每个着色器使用收到的顶点数据进行计算。

uniforms里是Babylon.js向显卡发送的默认通用变量,其中world对应网格的变换矩阵,View是相机的变换矩阵,Projection是相机的投影矩阵,worldViewProjection是三个矩阵变换的合并(关于矩阵变换可以参考https://www.cnblogs.com/ljzc002/p/8927221.html中的介绍,或者查看我在B站上传的3D编程入门视频教程https://space.bilibili.com/25346426/#/)

对于所有的着色器uniforms型数据都是通用的,比如上面提到的1000个顶点着色器都会使用相同的"worldViewProjection"和"worldView"变量。attributes和uniforms都属于OpenGL的“存储限定符”。

第九行代码设定了一个非默认的uniforms型变量,第十行将这个材质交给球体网格。

2、WebGL版本选择:

在进行glsl编程之前,一个重要的步骤是选择要使用的WebGL版本:

OpenGL发展历史如下:(源文件地址:https://docs.qq.com/sheet/B8uRgG1gE9T32RzNoW38xEnX2epfOY1cwvqG3)

使用着色器在WebGL3D场景中呈现行星表面地形-LMLPHP

可见WebGL1.0对应早期的OpenGL2.x,WebGL2.0对应较新的OpenGL4.x,显然WebGL2.0的功能更为强大,但考虑到我的笔记本显卡不支持WebGL2.0,只好使用旧的WebGL1.0。本文后面的glsl编程均使用OpenGL2.0的语法,OpenGL2.0存在很多缺陷,所以后面的部分内容也正是为了解决这些缺陷而编写的。

Babylon.js可以自动检测电脑支持的WebGL版本,并优先使用最新版:

 // GL
if (!options.disableWebGL2Support) {
try {
this._gl = (canvas.getContext("webgl2", options) || canvas.getContext("experimental-webgl2", options));
if (this._gl) {
this._webGLVersion = 2.0;
}
}
catch (e) {
// Do nothing
}
}
if (!this._gl) {
if (!canvas) {
throw new Error("The provided canvas is null or undefined.");
}
try {
this._gl = (canvas.getContext("webgl", options) || canvas.getContext("experimental-webgl", options));
}
catch (e) {
throw new Error("WebGL not supported");
}
}
if (!this._gl) {
throw new Error("WebGL not supported");
}

Babylon.js源码里包括很多实用的3D编程工具,即使不使用Babylon.js引擎也可以使用其中的工具简化原生WebGL开发。

3、简单glsl代码:

测试用的顶点着色器代码:

 uniform mat4 worldViewProjection;
uniform mat4 worldView;
attribute vec3 position; varying vec3 vPosition; void main(){
gl_Position=worldViewProjection*vec4(position,1);
vPosition=vec3(worldView*vec4(position,1));
}

这里varying是WebGL1.0中的第三种存储限定符,它表示这个变量是顶点着色器的计算结果,经过插值后传入片元着色器(关于WegGL1.0基础知识,推荐观看我以前发布的3D编程入门教程)

gl_Position是OpenGL的内置变量,表示这个顶点经过各种矩阵变换之后在视口中渲染的位置,vPositon指顶点相对于相机的位置。

需要注意的是:除构造函数外,glsl不支持浮点数和整形数之间的自动转换,浮点数通过“int i=int(f)”转为整形数,整形数通过"float f=float(i)"转换为浮点数,上述代码中的vec4()和vec3()则分别是四元浮点数组和三元浮点数组的构造函数,另外WebGL1.0不具备内置的四舍五入函数,需要使用“floor(f+0.5)”代替四舍五入,并且四舍五入之后仍然是浮点数而非整形数。

除了数组的索引外,着色器中绝大部分的计算都是浮点计算,而将整形计算的结果作为数组索引时也会遇到问题,后面会详细讨论如何处理这一问题。

片元着色器代码:

 precision mediump float;
varying vec3 vPosition;
uniform vec4 uColor;
void main()
{
vec4 tempColor=uColor;
//对2取模,参数必须是浮点型
if(mod((vPosition.x+vPosition.y+vPosition.z),2.0)>1.0)
{
tempColor+=vec4(0.0,-0.4,0.0,0.0);
}
gl_FragColor=tempColor;
}

gl_FragColor是一个内置变量,表示片元的最终颜色,注意glsl中的颜色值从0.0到1.0,而不是html中的0到255。

执行代码效果如下:

使用着色器在WebGL3D场景中呈现行星表面地形-LMLPHP

可见,随着相机的移动,球体的纹理自动发生变化,这类效果是很难用贴图方式实现的。

三、生成并保存简单的棋盘地形

假设行星的周长为40000km,将每个地块设为长宽均为100km的正方形,生成并保存一个包含50000多个地块的棋盘型地面:

1、数据保存:

考虑到每个地块都要具有独立的交互能力,使用文件方式保存效率极低,尝试了html5的本地存储功能,发现Chrome浏览器的本地存储空间只有5M,难以支持计划中的对多个行星数据的保存,最终决定使用h2微型数据库保存地块数据(读者可以自己搜索关于h2数据库的知识,我的视频教程里也提到了部分相关知识)。

a、在数据库中建表:

将行星想象为一个一半在地上一半在地下的建筑,不同的纬度对应了不同的层数,每一层有若干个大小相同的房间

 --建立地区块表
create table tab_dqk (
ID varchar(40) NOT NULL,
planetid varchar(40),
beta double,
pbeta double,
alpha double,
palpha double,
weight varchar(1000)
);
comment on table tab_dqk is '地区块表';
comment on column tab_dqk.id is '主键ID';
comment on column tab_dqk.planetid is '地区块所属的行星id';
comment on column tab_dqk.beta is '地区块的仰角';
comment on column tab_dqk.pbeta is '地区块仰角的区分度';--即这个beta仰角上下pbeta弧度都属于这一层
comment on column tab_dqk.alpha is '地区块水平转角';
comment on column tab_dqk.palpha is '地区块水平转角的区分度';--即这个alpha水平转角左右palpha弧度都属于这个房间
comment on column tab_dqk.weight is '用JSON表示的地形类型id权重'; alter table tab_dqk add column floor int;
alter table tab_dqk add column room int;
alter table tab_dqk add column altitude double; comment on column tab_dqk.floor is '地区块位于第几层';
comment on column tab_dqk.room is '地区块位于这一层的第几个房间';
comment on column tab_dqk.altitude is '地区块的海拔高度';
comment on column tab_dqk.weight is '地区块类型';
 --建立行星表
create table tab_planet
(
id varchar(40) NOT NULL,
name varchar(20) NOT NULL,
coreid varchar(40),
min_floor int NOT NULL,
max_floor int NOT NULL,
width_room int NOT NULL,
radius double,
mass double,
gravity double,
orbit double,
cycle double
);
comment on table tab_planet is '行星表';
comment on column tab_planet.id is '主键ID';
comment on column tab_planet.name is '行星名字';
comment on column tab_planet.coreid is '围绕旋转的主星id';
comment on column tab_planet.min_floor is '最低层数';
comment on column tab_planet.max_floor is '最高层数';
comment on column tab_planet.width_room is '数据宽度';
comment on column tab_planet.radius is '半径(km)';
comment on column tab_planet.mass is '质量(t)';
comment on column tab_planet.gravity is '重力加速度';
comment on column tab_planet.orbit is '同步轨道高度';
comment on column tab_planet.cycle is '自转周期'; alter table tab_planet add column perimeter int; comment on column tab_planet.perimeter is '行星周长';

b、dao实现

传统MVC思想认为浏览器端的安全没有保证,必须在浏览器和数据库之间加入一种“后台程序”来提高安全性,这种程序通常由JAVA、C#实现,近些年也出现了许多由python和JavaScript实现的后台程序。这些后台程序一般包括三层:负责接收浏览器访问的service层、负责将特定访问参数和数据关联起来的application层,负责访问数据库的dao层。

但是我认为这个实验中的所有参与者都是可信任的,所以为了程序的简洁要尝试去掉后台程序,我发现h2数据库服务支持http协议通信,通过使用Fiddler对h2的网页控制台进行抓包,编写了直接用浏览器和数据库通信的代码:(代码在Linkh2.js中)

 /**
* Created by lz on 2018/5/15.
*/
var jsessionid="";
var Url="";
var UrlHead="http://127.0.0.1:8082/";
var H2State="offline";
var H2LoginCallback;//回调函数对象
function H2Login(func)
{
H2LoginCallback=func;
Url=UrlHead+"";
Argv="";
Request(xmlHttp,"POST",Url,true,Argv,"application/x-www-form-urlencoded",H2LoginCallBack,0);
}
function H2LoginCallBack()
{
if(xmlHttp.readyState==4) {
if(isTimout=="1")
{
alert("登陆验证请求超时!!");
clearTimeout(timer);
xmlHttp.abort();
}
else {
if (xmlHttp.status == 200) {
clearTimeout(timer);//停止定时器
try
{
var str_id=xmlHttp.responseText;
xmlHttp.abort();
jsessionid=str_id.substr(str_id.search(/jsessionid/)+11,32) ;//从h2获取一个jsessionid
console.log(jsessionid);
H2Login2();
}catch(e)
{
alert(e);
console.error(e)
xmlHttp.abort();
}
}
}
}
}
function H2Login2()
{
Url=UrlHead+"login.do?jsessionid="+jsessionid;//用这个jsessionid登录
Argv="language=en&setting=Generic H2 (Embedded)&name=Generic H2 (Embedded)" +
"&driver=org.h2.Driver&url=jdbc:h2:tcp://127.0.0.1/../../datawar" +
"&user=datawar&password=datawar";
Request(xmlHttp,"POST",Url,true,Argv,"application/x-www-form-urlencoded",H2Login2CallBack,0);
}
function H2Login2CallBack()
{
if(xmlHttp.readyState==4) {
if(isTimout=="1")
{
alert("登陆验证请求超时!!");
clearTimeout(timer);
xmlHttp.abort();
}
else {
if (xmlHttp.status == 200) {
clearTimeout(timer);//停止定时器
try
{
var str_logres=xmlHttp.responseText;//这时已经在h2服务端建立登录状态
xmlHttp.abort();
console.log("完成h2数据库登录");
H2State="online";
//Query();
//CreateChess();//测试时将运算启动放在这里,实际使用时,通过渲染循环检测H2State标志来启动运算
H2LoginCallback();//这样可以执行函数对象吗????《-可以
}catch(e)
{
alert(e);
console.error(e)
xmlHttp.abort();
}
}
}
}
}

其中“Request”是一个Ajax请求函数,内容如下:

 /**
* Created by Administrator on 2015/1/28.
* Update by Administrator on 2015/09/17.
*/
//Ajax通信页面
var xmlHttp=createXMLHttpRequest();
var isTimout="0";//0表示未超时
var timer;//用来存定时器
function createXMLHttpRequest()
{
var xhr=false;
if(window.XMLHttpRequest)
{
try
{
netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");
}
catch(e)
{ }
xhr=new XMLHttpRequest();
}
else if(window.ActiveXObject)
{
try
{
xhr=new window.ActiveXObject("Msxm12.XMLHTTP"); }
catch(e)
{
try
{
xhr=new window.ActiveXObject("Microsoft.XMLHTTP");
}
catch(e)
{ alert("您的浏览器不支持ajax!"); }
}
}
return xhr;
}
//目前的版本为单线程的ajax访问,不能支持多个ajax同时访问
Request=function(xhr,method,src,isajax,argv,content_type,recallfunc,timeout)
{//连接对象、连接方式、连接目的地址、是否异步、提交内容、表单的内容类型、回调函数、 超时时间
xmlHttp.open(method,src,isajax)//第三个参数为true为异步方式
if(method=="POST")
xmlHttp.setRequestHeader("Content-Type",content_type);
xmlHttp.setRequestHeader("Access-Control-Allow-Origin","*");
xmlHttp.onreadystatechange=recallfunc;
xmlHttp.send(argv);
isTimout="0";
if(timeout==1) {//0表示不检测超时,1表示检测超时,好像不好使??
timer = window.setTimeout("dualTimeout();", timeout);
}
}

可见浏览器直接使用明文传递了数据库用户名和密码

2、数据生成

代码如下:(testchess.html)

 //根据极坐标和变化的半径生成颜色交错的地区块
function CreateChess()
{
var size_dqk=100;//每个地区块的长宽都是100km
var perimeter_planet=40000;//这个行星的周长是40000km
var r_planet=perimeter_planet/(2*Math.PI);//行星的半径 //我们将行星的表面想象成一个一半在地下一半在地上的建筑,len_beta就是根据周长算得的地上或地下的层数
var len_beta=sswr(((perimeter_planet/2)/size_dqk)/2);//通过长度而不是弧度来分层!!
var pbeta=(Math.PI/4)/len_beta; //对于每一层,
for(var i=-len_beta;i<=len_beta;i++)
{
var rad_beta=(Math.PI/2)*(i/len_beta);
var r_floor=Math.cos(rad_beta)*r_planet;//这一层的半径
var len_alpha_floor=sswr((r_floor*2*Math.PI)/size_dqk);//根据这一层的周长算出这一层有多少个房间
var palpha=Math.PI/len_alpha_floor;//每一个地区块的角度边界,在这个边界范围内即属于这个地区块
var beta=i*pbeta;
console.log(i+"/"+len_beta+">"+len_alpha_floor);
//对于圆环上的每一个片
for(var j=0;j<len_alpha_floor;j++)
{
var obj={};
obj.palpha=palpha;
obj.alpha=j*palpha;
obj.pbeta=pbeta;
obj.beta=beta;
obj.weight={};
obj.floor=i;
obj.room=j;
if((Math.floor(i/3)%2)==(Math.floor(j/3)%2))//棋盘形,但试验时并没有生成想要的棋盘形,没有调试是为什么
{
obj.weight="{land_textblack:1}";//在数据库中直接使用字符串形式保存
}
else
{
obj.weight="{land_textyellow:1}";
} //先尝试单独推入每一个地区块
PushChess(obj);//将这个地区块数据写入数据库
}
}
}
function PushChess(obj)
{
Url="http://127.0.0.1:8082/query.do?jsessionid="+jsessionid;
Argv="sql=insert into tab_dqk values(uuid(),'test',"+obj.beta+","+obj.pbeta+","+obj.alpha+","+obj.palpha+",'"
+obj.weight+"',"+obj.floor+","+obj.room+")";
//使用同步Ajax请求保证进度同步,在连续使用同步Ajax时不需要xmlHttp.abort()!!!!
Request(xmlHttp,"POST",Url,false,Argv,"application/x-www-form-urlencoded",PushChessCallBack,0);
}
function PushChessCallBack()
{//空方法 }

半径计算图解如下:

使用着色器在WebGL3D场景中呈现行星表面地形-LMLPHP

代码中sswr是自己编写的一个四舍五入方法:

 //四舍五入,目标浮点数、取整方式,小数点后精度
function sswr(float,type,accuracy)
{
var float2=float;
if(type==null||type==0)
{ }
else if(type==1)//向下取整
{
float2+=-0.5;
}
else if(type==2)//向上取整
{
float2+=0.5;
}
var acc=Math.pow(10,accuracy?accuracy:0);//用于保留小数点后精度,保留小数点后一位时,acc为10
var int_res=div(Math.round(float2*acc),acc);
return int_res;
}

为了编程简单,生成每个地区块后立刻用同步方式将数据存入数据库,等保存完毕后再生成下一个地区块,这个简易的Ajax也只支持单线程工作。奇怪的是这次测试时使用post方式成功保存数据,但后面编程时post方式则无Ajax返回值,改用get方式传递参数后又保存成功,因为时间有限没有详细研究,读者如果感兴趣可以好好调试一下,告诉我为什么会这样。

最终在数据库中插入了50930条数据。

3、数据显示

数据查询和显示代码如下:(testdatatexture.html)

a、查询行星信息:

 function CreateChess()//读取地区块的一些整体信息,从planet表获取更合理?
{
Url="http://127.0.0.1:8082/query.do?jsessionid="+jsessionid;
Argv="sql=select min_floor,max_floor,width_room from tab_planet where id='test'";//查找这个行星的地区块层数
//使用同步Ajax请求保证进度同步
Request(xmlHttp,"POST",Url,true,Argv,"application/x-www-form-urlencoded",CreateChessCallBack,0);
}
//地区块计算使用的变量
var min_floor= 0,max_floor= 0,width_room= 0,height_floor=0;//最低层数,最高层数
var can_temp=document.createElement("canvas");//用来做像素处理的隐形canvas
var context_temp;
var imagedata_temp;//=context_temp.getImageData(0,0,400,199);
var strsrc_dqk="";

数据中包含了每个地区块的“层数”和“房间号”信息,我的思路是把这些信息传入显卡,由片元着色器程序算出每个片元所属的房间,然后在传入的数据中找到这个房间的颜色,这个思路类似OpenGL中的3D纹理。

一种将数万条数据传入显卡的合理方式是将数据转化为一张图片,交由显卡的纹理采样器进行处理,我把这种用来传输数据的纹理命名为“数据纹理”。恰好我希望传入的数据正是房间对应的地形颜色,与图片的数据结构相符。

这里建立了一个隐形的canvas用来把每个房间的颜色放入数据纹理的对应像素。

b、处理获得的行星信息,据此获取每个地区块的数据:(这段代码的执行效率较低)

                var str_res=xmlHttp.responseText;//从h2数据库获取到的是一个dom文档,要从中找到所需要的字段
xmlHttp.abort();
var div=document.createElement("div");
//div.innerHTML=str_res;//这样做会报错,因为加载整个dom时,标签中的引用和代码都会被执行!!!!
div.innerHTML=str_res.substring(str_res.search(/<table/),str_res.search(/<\/table>/)+8);
div.style.display=false;
//document.getElementById("div_allbase").appendChild(div);不需要这一句
var tr=div.getElementsByTagName("tr")[1];//从返回的数据表里提取出数据部分
var tds=tr.getElementsByTagName("td");
min_floor=parseInt(tds[0].innerHTML);
max_floor=parseInt(tds[1].innerHTML);
width_room=parseInt(tds[2].innerHTML);//房间最多的一层有多少房间
height_floor=min_floor+max_floor+1;//总共有多少层
var int_wh=width_room>height_floor?width_room:height_floor;//取宽高中较大的值
var int_size=newland.FindPower(int_wh);//找到大于size的最小的2的整数次幂,注意!!!!
can_temp.width=int_size;
can_temp.height=int_size;
context_temp=can_temp.getContext("2d");
context_temp.fillStyle="rgba(0,0,255,1)";
context_temp.fillRect(0,0,int_size,int_size);
imagedata_temp=context_temp.getImageData(0,0,int_size,int_size);//取canvas的像素数据 arr_floorroom=[];
pbeta=0;//认为每一层的弧度区分度是一定的
arr_palpha=[];//每一层内的房间的弧度区分度不同
var arr_temp=JSON.parse(localStorage.getItem("arr_floorroom"));//读取本地存储 if(arr_temp&&arr_temp.length>0)//如果有本地持久化存储就使用本地的存储,节省时间
{//经测试Chrome浏览器的5M空间正好可以放下一个行星的数据
strsrc_dqk=JSON.parse(localStorage.getItem("strsrc_dqk"));
arr_floorroom=arr_temp;
arr_palpha=JSON.parse(localStorage.getItem("arr_palpha"));
pbeta=JSON.parse(localStorage.getItem("pbeta")); }
else
{
for(var i=min_floor;i<=max_floor;i++)//读取每一层的数据
{
Url="http://127.0.0.1:8082/query.do?jsessionid="+jsessionid;
Argv="sql=select id,beta,pbeta,alpha,palpha,weight,floor,room from tab_dqk" +
" where planetid='test' and floor="+i;//查找这个行星的地区块层数
//使用同步Ajax请求保证进度同步
Request(xmlHttp,"POST",Url,false,Argv,"application/x-www-form-urlencoded",QueryCallBack,0);
}
}
localStorage.setItem("arr_floorroom",JSON.stringify(arr_floorroom));
localStorage.setItem("arr_palpha",JSON.stringify(arr_palpha));
localStorage.setItem("pbeta",JSON.stringify(pbeta));
//大小超过了本地存储限制
xmlHttp.abort();
DrawPlanet();//绘制行星

其中FindPower是我编写的一个寻找2的整数次幂的方法:

 //根据正方形边长找大于size的最小的2的整数次幂
newland.FindPower=function(size)
{
var int1=Math.ceil(Math.log2(size));
return Math.pow(2,int1);
}
//根据数据长度找最小的2的整数次幂
newland.FindPower2=function(len)
{
var int1=Math.sqrt(len);
return this.FindPower(int1);
}

借助这一方法,我们把datatexture(数据纹理)设置为边长是2的整数次幂的正方形图片。在原生WebGL中,纹理图片的边长必须是2的整数次幂,否则会发生错误,而Babylon.js虽然支持使用任意尺寸的图片,但非标准图片在传入显卡时会自动进行线性采样模式的拉伸,而导致图片颜色交界处的模糊,这种模糊对于数据纹理准确性的影响是致命的。

您可以自己搜索一下“最近点采样”与“线性纹理采样”的知识,datatexture必须使用最近点采样。

palpha是水平区分度,表示每个房间的中心到房间墙壁的弧度,显然越靠近两极房间越少,水平区分的值也就越高,arr_palpha保存了每一层的水平区分度,是一个长度为199的数组。

c、处理每一层的地区块数据:

                var str_res=xmlHttp.responseText;//这时已经在h2服务端建立登录状态
//xmlHttp.abort();
var div=document.createElement("div");
div.innerHTML=str_res.substring(str_res.search(/<table/),str_res.search(/<\/table>/)+8);
var trs=div.getElementsByTagName("tr");
var len=trs.length;
var int_floor=parseInt(trs[1].getElementsByTagName("td")[6].innerHTML);
if(!arr_floorroom[int_floor])//这里认为只有一个行星,在实际使用时,数组还要加上一维
{
arr_floorroom[int_floor]=[];
}
if(pbeta==0)
{
pbeta=parseFloat(trs[1].getElementsByTagName("td")[2].innerHTML);
}
console.log(int_floor+"/"+max_floor+">"+(len-1));//输出一下处理进度
arr_palpha.push(parseFloat(trs[1].getElementsByTagName("td")[4].innerHTML));
for(var i=1;i<len;i++)//对于这一层中的每个room
{
var tds=trs[i].getElementsByTagName("td");
var int_room=parseInt(tds[7].innerHTML);
var arr_landtype={land_textblack:[0,0,0,255],land_textyellow:[255,255,0,255]}
//保存在内存中的数据结构
//var obj=eval(tds[5].innerHTML);
var obj={};
eval("obj="+tds[5].innerHTML);
arr_floorroom[int_floor][int_room]={id:tds[0],beta:tds[1].innerHTML,pbeta:tds[2].innerHTML,alpha:tds[3].innerHTML
,palpha:tds[4].innerHTML,weight:obj};
//使用何种数据结构传递到显卡?使用一个超长数组arr_set4?《-这是不行的
var num1= 0,num2= 0,num3= 0,num4= 255;
for(key in obj)//将每一种颜色的三个通道加权到一起
{
var num_key=obj[key];
num1+=(num_key*arr_landtype[key][0]);
num2+=(num_key*arr_landtype[key][1]);
num3+=(num_key*arr_landtype[key][2]);
}
var index= (int_floor+max_floor)*4*can_temp.width+(i-1)*4;//每个room由4个元素组成
imagedata_temp.data[index]=num1;//这里存的真的是颜色
imagedata_temp.data[index+1]=num2;
imagedata_temp.data[index+2]=num3;
imagedata_temp.data[index+3]=num4;
}

因为100km*100km的地区块在近距离视角下也是一个很大的区域,下一步计划在拉近视角时,在一个地区块中再生成更多种类的地形,所以所谓“地区块的地形”,其实是其内包含的各种地形的比例分配,在远处看时地区块的颜色是其包含的各种地形的加权。

这段测试里我使用了两种地形:纯黑色的land_textblack和黄色的land_textyellow,而两种地区块的地形则分别是百分之百的land_textblack和land_textyellow。颜色的加权值被放进了canvas的像素中。

d、绘制行星:

 //用glsl和Babylon.js结合的方式绘制行星
function DrawPlanet()
{
var amigaMaterial = new BABYLON.ShaderMaterial("amiga2", scene,{
vertexElement: "sh2v4.sh",
fragmentElement: "sh2f4.sh",
},
{
attributes: ["position"],
uniforms: ["worldViewProjection","worldView"]
});
amigaMaterial.doNotSerialize=true;
sphere1.material=amigaMaterial;
if(strsrc_dqk=="")
{
context_temp.putImageData(imagedata_temp,0,0);
strsrc_dqk=can_temp.toDataURL("image/png");//将canvas转化为dataurl
localStorage.setItem("strsrc_dqk",JSON.stringify(strsrc_dqk));
}
var utexturedqk = new BABYLON.Texture.CreateFromBase64String(strsrc_dqk,"utexturedqk", scene
,false,false,BABYLON.Texture.NEAREST_NEAREST);//将dataurl转化为Babylon.js纹理
amigaMaterial.setTexture("utexturedqk",utexturedqk);//将纹理和显卡采样器关联
amigaMaterial.setFloat("wid_utexturedqk",can_temp.width);//数据纹理宽度,将内存中的变量和显卡中的通用变量关联
amigaMaterial.setFloat("hei_utexturedqk",can_temp.width);
amigaMaterial.setFloat("pbeta",pbeta);//层间区分角度 var size=newland.FindPower2(arr_palpha.length);//注意!!!!
var strsrc_palpha=newland.TranArrToPng1(arr_palpha,size,size);//每一层内的房间区分角度,用4个元素保存一个浮点数
var utexturepalpha = new BABYLON.Texture.CreateFromBase64String(strsrc_palpha,"utexturepalpha", scene
,true,false,BABYLON.Texture.NEAREST_NEAREST);
amigaMaterial.setTexture("utexturepalpha",utexturepalpha);
amigaMaterial.setFloat("wid_utexturepalpha",size);//room区分度的纹理宽度
amigaMaterial.setFloat("hei_utexturepalpha",size); amigaMaterial.setFloat("uarrpalphalen",arr_palpha.length);
amigaMaterial.setFloat("max_floorf",max_floor);//Babylon.js不支持传递整形量??GpenGL中int也是以float形式计算的!!!!
amigaMaterial.setFloat("MathPI",Math.PI); amigaMaterial.onCompiled=function()//Babylon.js文档中写effect是material的一个内容,而material需要一个“编译过程”,编译之后的material才具备effect属性
{//而且对Babylon.js来说,material能传递的变量类型比较少,比如不能传递整形量,而effect则可以传递更多的数据类型
         //amigaMaterial.getEffect().setArray("uarrpalpha",arr);//每一层水平区分度*/effect可以向显卡传递数组
           //console.log(amigaMaterial.getEffect());        
      }
     }

这里我们要考虑使用何种方式把长度为199的浮点型数组arr_palpha传入显卡,事实上OpenGL支持传入数组型通用变量,但对数组的支持分成两种:一种是我们前面看到的vec3、vec4这类向量数组,glsl把整个向量看做一个变量,而另一种类似“uniform float uarrpalpha[500]”的自定义数组则是把数组中的每一个元素都看做一个uniform变量处理!(?)。

根据StackOverFlow上一个外国同行的试验,OpenGL最多只能支持200个左右的uniform变量(?),这意味着我们难以直接用数组的方式传入arr_palpha。另外在WebGL1.0中的glsl不支持不定长度的数组,这意味着我们必须在编写着色器代码前对数组的大小有恰当的估计,或者根据行星的大小临时调节着色器代码。

在glsl中使用长数组的另一个问题是:glsl竟然不支持直接用临时赋值的变量作为数组索引!类似

 int i=;
float f=arr[i+];

这种数组用法是不允许的!!!!

要将计算结果作为数组的索引只能使用if else或者switch case枚举出每一种对应的情况,或者使用:

 float getData500(float data[],int id) {
int len=int(floor(uarrpalphalen+0.5));
for (int i=; i<; i++) {
if(i>=len)//i不能和非常量比较!!只好换个方式限制它
{
return 0.0;
}
if (i == id) return data[i];
}
}

其中id是计算出来的索引,另外glsl的for循环的中段也不支持“i<len”这种写法,i必须小于一个明确的常数。

因此改为使用datatexture传递水平区分度数组

然而使用datatexture时又遇到一个问题,canvas会自动把颜色分量转化为0到255的整数,而这里的水平区分度全是0到1之间的小数,会被自动转为0或1,为解决这一问题把水平区分度数据转化为科学计数法并用像素表示:

 //将一个浮点数组转化为DataTexture,这是浮点数小于1的情况,要注意canvas和webgl对颜色属性的自动处理!!!!
newland.TranArrToPng1=function(arr,width,height)
{
var can_temp=document.createElement("canvas");
can_temp.width=width;
can_temp.height=height;
var context=can_temp.getContext("2d");
context.fillStyle="rgba(0,0,255,1)";//多余的位都是1?
context.fillRect(0,0,width,height);
var imagedata=context.getImageData(0,0,width,height);
var len=arr.length;//小数部分会自动四舍五入!!!!默认palpha必定小于1
for(var i=0;i<len;i+=1)
{
var str_num=arr[i]+"";
//var int_0=str_num.indexOf();
var len_str=str_num.length;
var count_0=0;
for(var j=0;j<len_str;j++)
{
if(str_num[j]=="0"||str_num[j]==".")
{
continue;
}
else
{
count_0=j;//找到第一个非零数
break;
}
}
var num1=parseInt(str_num.substr(count_0,2));
var num2=parseInt(str_num.substr(count_0+2,2));
//var num3=parseInt(str_num.substr(count_0+4,2));
var num4=4+(count_0-2);
imagedata.data[i*4]=num1;//科学计数法:用像素颜色的第一第二个分量保存四位有效数字,用第四个分量保存10的负指数
imagedata.data[i*4+1]=num2;
imagedata.data[i*4+2]=num4;
//imagedata.data[i*4+3]=num4;
}
context.putImageData(imagedata,0,0);
var strsrc_palpha=can_temp.toDataURL("image/png");
//can_temp.dispose();
can_temp=null;
return strsrc_palpha;
}

执行程序,看到运行效果与设计有所偏差:

使用着色器在WebGL3D场景中呈现行星表面地形-LMLPHP

南半球的黑色地区块少了很多行,没有形成预计的棋盘形,时间有限没有详细调试

拉近相机:

使用着色器在WebGL3D场景中呈现行星表面地形-LMLPHP

可以看到每个地区块的边界清晰可见,没有发生模糊。

使用console.log输出用到的两个dataurl作为参考:

使用着色器在WebGL3D场景中呈现行星表面地形-LMLPHP

使用着色器在WebGL3D场景中呈现行星表面地形-LMLPHP

4、着色器代码:

a、顶点着色器:

 uniform mat4 worldViewProjection;
uniform mat4 worldView;
attribute vec3 position; varying vec3 vPosition;
varying vec3 oPosition;//自身坐标系里的位置 void main(){
gl_Position=worldViewProjection*vec4(position,);
vPosition=vec3(worldView*vec4(position,));
oPosition=position;
}

我们前面生成地形的计算都是在行星的自身坐标系里进行的,所以在片元着色器里也需要以行星的自身坐标系为参考确定地区块的层数和房间号,所以把一个不经过任何矩阵变换的顶点位置信息传入片元着色器。

b、片元着色器:

 precision highp float;
//varying vec4 vColor;
varying vec3 vPosition;
varying vec3 oPosition;
//uniform vec4 uColor;
uniform sampler2D utexturedqk;//地区块数据纹理的采样器
uniform float wid_utexturedqk;//数据纹理的宽高
uniform float hei_utexturedqk; uniform sampler2D utexturepalpha;//一个单元里保存了四个元素!!!!
//uniform vec3 uarrdqk[60000];//es3.0之前的glsles不支持隐含数组!!!!
uniform float pbeta;
uniform float wid_utexturepalpha;
uniform float hei_utexturepalpha;
//uniform float uarrpalpha[500];//用来测试的行星只有199层,预设为500层应该够了
uniform float uarrpalphalen;
uniform float max_floorf;
uniform float MathPI; float getDataVec4(vec4 data,int id) {
for (int i=; i<; i++) {
if (i == id) return data[i];
}
}
float getData500(float data[],int id) {
int len=int(floor(uarrpalphalen+0.5));
for (int i=; i<; i++) {
if(i>=len)//i不能和非常量比较!!只好换个方式限制它
{
return 0.0;
}
if (i == id) return data[i];
}
} void main()
{
//vec4 tempColor=uColor;//es3.0之前不支持round!!!!
//glsl事实上以float为计算基准
//int max_floor=int(floor(max_floorf+0.5));
//算层数
float r=sqrt(oPosition.x*oPosition.x+oPosition.y*oPosition.y+oPosition.z*oPosition.z);//这个片元到球心的距离
float beta=asin(oPosition.y/r);//俯仰角
//int int_beta=int(floor((beta/(pbeta*2.0))+0.5));
float count_beta=(beta/(pbeta*2.0));
//int index_beta=int(floor(count_beta+ max_floorf+0.5));
float index_beta=floor(count_beta+ max_floorf+0.5);//第几层
//int roomcount=0;
//使用数据纹理法,取这一层的区分度
//int int1=int(floor(mod(index_beta,4.0)+0.5)); //float float1=(index_beta/4.0);
float floatu=(mod(index_beta,wid_utexturepalpha))/(wid_utexturepalpha)+(0.5/wid_utexturepalpha);//u是x轴坐标,v是y轴坐标
float floatv=(((index_beta)/(wid_utexturepalpha)))/(hei_utexturepalpha)+(0.5/(wid_utexturepalpha*hei_utexturepalpha));
vec2 UVpalpha=vec2(floatu,floatv);//上面计算的uv坐标加了一个偏移量,防止坐标正好落在两个像素的边界上
vec4 vec4palphas=texture2D(utexturepalpha, UVpalpha);//glsl中的颜色为0到1.0,所以要乘以255.0获得传入的科学计数法
float palpha=(vec4palphas[]*255.0*100.0+vec4palphas[]*255.0)/pow(10.0,vec4palphas[]*255.0);
//float palpha=getData500(uarrpalpha,int(floor(index_beta+0.5)));//改为尝试数组法传递数据
//取这一层的转角
float alpha=atan(oPosition.z,oPosition.x);//标准反正切函数有两个参数!!
if(alpha<0.0)
{
alpha+=(MathPI*2.0);
}
//取地区块数据纹理的坐标
float floatu2=(alpha/(palpha*2.0))/wid_utexturedqk;
float floatv2=index_beta/hei_utexturedqk+0.5/hei_utexturedqk;
vec2 UVdqk=vec2(floatu2,floatv2);
gl_FragColor=texture2D(utexturedqk, UVdqk);
//gl_FragColor=vec4palphas;
//gl_FragColor=texelFetch(utexturedqk,ivec2(int(floor(alpha/(palpha*2.0)+0.5)),int(floor(index_beta+0.5))),0);//这个整数像素的方法是WebGL2开始加入的!!!!
//gl_FragColor=vec4(1.0*floatu,1.0*floatv,1.0*floatv2,1.0);//红,绿,蓝结果不是0就是1??!! //int index_dqk=roomcount-1+int(floor((alpha/palpha)+0.5));
//vec4 tempColor=vec4(uarrdqk[index_dqk],1.0); //float float_3=index_beta/(max_floorf*2.0);
//float float_4=oPosition.y/5.0;
//canvas的imagedata用255,255,255,255定义颜色通道,而glsl用1.0,1.0,1.0,1.0定义!!!! }

glsl语言不支持字符串类型,WebGL1.0也不支持从显卡反写数据到内存,一种可行的调试方法是将某个计算结果转化为颜色显示在屏幕上,然后用拾色器提取值。

四、根据规则生成随机的行星表面地形(testarenas.html)

1、生成地区块的基本数据结构:

 function CookDqk()//生成地区块,每一floor的每个room
{
var size_dqk=100;//每个地区块的长宽都是100km
var r_planet=perimeter/(2*Math.PI);//行星的半径
var len_beta=sswr(((perimeter/2)/size_dqk)/2);//通过弧度来分层!!100
pbeta=(Math.PI/4)/len_beta;
//对于每一层,
for(var i=-len_beta;i<=len_beta;i++)
{ var rad_beta=(Math.PI/2)*(i/len_beta);
var r_floor=Math.cos(rad_beta)*r_planet;//这一层的半径
var len_alpha_floor=sswr((r_floor*2*Math.PI)/size_dqk);
var palpha=Math.PI/len_alpha_floor;//每一个地区块的角度边界,在这个边界范围内即属于这个地区块
arr_palpha.push(palpha);
var beta=i*pbeta*2;
//console.log(i+"/"+len_beta+">"+len_alpha_floor);
var arr1=[];
//对于圆环上的每一个片
for(var j=0;j<len_alpha_floor;j++)
{
var obj={};
obj.palpha=palpha;
obj.alpha=j*palpha*2;
obj.pbeta=pbeta;
obj.beta=beta;
//obj.weight={};
obj.floor=i;
obj.room=j;
obj.countcook=0;
obj.altitude=0; arr1.push(obj); }
if(arr1.length>0)
{
arr_floorroom.push(arr1);
} }
CookDqk2();//对生成的数据结构进行 规律随机填充
}

2、使用正态随机数与加和平均确定每个地区块的海拔

 //使用正态随机数和加和平均确定每个地区块的海拔
function CookDqk2()
{
var len=arr_floorroom.length;
//生成初始的随机正态随机海拔
console.log("生成初始的随机正态随机海拔");
for(var i=0;i<len;i++)
{
//console.log(i+" in "+len);
var len2=arr_floorroom[i].length;
for(var j=0;j<len2;j++)
{
var obj=arr_floorroom[i][j];
obj.altitude=dc1.getNumberInNormalDistribution(-10,1000);//平均海拔是-10,常见的海拔在正负1000以内
if(obj.altitude<-10000)
{
obj.altitude=-10000;
}
else if(obj.altitude>10000)
{
obj.altitude=10000;
}
obj.countcook=1;
if(i%2==1)//如果是奇数层,room偏移一个识别范围,这样地形看起来更自然
{
obj.alpha+=obj.palpha;
}
}
}
//使用加和平均方法使海拔趋于连续(高度平滑)
console.log("使用加和平均方法使海拔趋于连续");
for(var i=0;i<len;i++)//将地区块的海拔和周围相邻的所有地区块的海拔相加取平均值,作为这个地区块的海拔
{
console.log(i+" in "+len);
var len2=arr_floorroom[i].length;
for(var j=0;j<len2;j++)
{
var obj=arr_floorroom[i][j];
obj.altitude1=obj.altitude;
if(i>0)//考虑这个room下面的floor
{
//var alpha=obj.alpha;
var len3=arr_floorroom[i-1].length;
for(var k=0;k<len3;k++)//遍历下层的room
{
var subplpha=Math.abs(arr_floorroom[i-1][k].alpha-obj.alpha);
if(subplpha>Math.PI)
{
subplpha=Math.PI*2-subplpha;
}
if(subplpha<=(obj.palpha+arr_floorroom[i-1][k].palpha))
{//对这个地区块有影响
obj.altitude1+=arr_floorroom[i-1][k].altitude;
obj.countcook++;
}
} }
if(i<len-1)//考虑这个room上面的floor
{
var len3=arr_floorroom[i+1].length;
for(var k=0;k<len3;k++)//遍历上层的room
{
var subplpha=Math.abs(arr_floorroom[i+1][k].alpha-obj.alpha);
if(subplpha>Math.PI)
{
subplpha=Math.PI*2-subplpha;
}
if(subplpha<=(obj.palpha+arr_floorroom[i+1][k].palpha))
{//对这个地区块有影响
obj.altitude1+=arr_floorroom[i+1][k].altitude;
obj.countcook++;
}
}
}
//考虑本层的相邻元素
if(j==0)
{
obj.altitude1+=arr_floorroom[i][1].altitude;
obj.altitude1+=arr_floorroom[i][len2-1].altitude;
obj.countcook+=2;
}else if(j==(len2-1))
{
obj.altitude1+=arr_floorroom[i][0].altitude;
obj.altitude1+=arr_floorroom[i][len2-2].altitude;
obj.countcook+=2;
}
else{
obj.altitude1+=arr_floorroom[i][j-1].altitude;
obj.altitude1+=arr_floorroom[i][j+1].altitude;
obj.countcook+=2;
}
}
}
var min_altitude= 0,max_altitude=0;
console.log("去除总权值");
for(var i=0;i<len;i++)
{
console.log(i+" in "+len);
var len2=arr_floorroom[i].length;
for(var j=0;j<len2;j++)
{
var obj=arr_floorroom[i][j];
obj.altitude=obj.altitude1/obj.countcook;
if(obj.altitude<min_altitude)
{
min_altitude=obj.altitude;
}
if(obj.altitude>max_altitude)
{
max_altitude=obj.altitude;
}
//delete obj.altitude1;
}
}
console.log("最低、最高海拔为:"+min_altitude+"、"+max_altitude);
//根据海拔高度与概率规则确定海洋与陆地,根据纬度和高度确定陆地的类型(高度达到一定程度后优于纬度)
CookDqk3();
}

关于正态随机数的知识可以参考这篇文章:https://www.cnblogs.com/zztt/p/4025207.html

3、根据海拔高度和纬度确定地形:

 function CookDqk3()
{
console.log("开始生成地区块级地形");
var len=arr_floorroom.length;
for(var i=0;i<len;i++) {
console.log(i+" in "+len);
var len2 = arr_floorroom[i].length;
for (var j = 0; j < len2; j++)
{
var obj=arr_floorroom[i][j];
getLandtypeDqk(obj);//根据规则确定这个地区块的地形
}
}
//地区块平滑
console.log("地区块平滑");
for(var i=0;i<len;i++)
{
console.log(i+" in "+len);
var len2=arr_floorroom[i].length;
for(var j=0;j<len2;j++)
{
var obj=arr_floorroom[i][j]; if(i>0)//考虑这个room下面的floor
{
//var alpha=obj.alpha;
var len3=arr_floorroom[i-1].length;
for(var k=0;k<len3;k++)//遍历下层的room
{
var obj1=arr_floorroom[i-1][k];
var subplpha=Math.abs(obj1.alpha-obj.alpha);
if(subplpha>Math.PI)
{
subplpha=Math.PI*2-subplpha;
}
if(subplpha<=(obj.palpha+obj1.palpha))
{//对这个地区块有影响
if(!obj.landtypedqk[obj1.type2])
{
obj.landtypedqk[obj1.type2]=obj1.effect;//这一种地形的权重
}
else
{
obj.landtypedqk[obj1.type2]+=obj1.effect;
}
obj.landtypedqkeffect+=obj1.effect;//所有地形的权重
}
} }
if(i<len-1)//考虑这个room上面的floor
{
var len3=arr_floorroom[i+1].length;
for(var k=0;k<len3;k++)//遍历上层的room
{
var obj1=arr_floorroom[i+1][k];
var subplpha=Math.abs(obj1.alpha-obj.alpha);
if(subplpha>Math.PI)
{
subplpha=Math.PI*2-subplpha;
}
if(subplpha<=(obj.palpha+obj1.palpha))
{//对这个地区块有影响
if(!obj.landtypedqk[obj1.type2])
{
obj.landtypedqk[obj1.type2]=obj1.effect;
}
else
{
obj.landtypedqk[obj1.type2]+=obj1.effect;
}
obj.landtypedqkeffect+=obj1.effect;
}
}
}
//考虑本层的相邻元素
if(j==0)
{
var obj1=arr_floorroom[i][1];
if(!obj.landtypedqk[obj1.type2])
{
obj.landtypedqk[obj1.type2]=obj1.effect;
}
else
{
obj.landtypedqk[obj1.type2]+=obj1.effect;
}
obj.landtypedqkeffect+=obj1.effect;
var obj1=arr_floorroom[i][len2-1];
if(!obj.landtypedqk[obj1.type2])
{
obj.landtypedqk[obj1.type2]=obj1.effect;
}
else
{
obj.landtypedqk[obj1.type2]+=obj1.effect;
}
obj.landtypedqkeffect+=obj1.effect;
}
else if(j==(len2-1))
{
var obj1=arr_floorroom[i][0];
if(!obj.landtypedqk[obj1.type2])
{
obj.landtypedqk[obj1.type2]=obj1.effect;
}
else
{
obj.landtypedqk[obj1.type2]+=obj1.effect;
}
obj.landtypedqkeffect+=obj1.effect;
var obj1=arr_floorroom[i][len2-2];
if(!obj.landtypedqk[obj1.type2])
{
obj.landtypedqk[obj1.type2]=obj1.effect;
}
else
{
obj.landtypedqk[obj1.type2]+=obj1.effect;
}
obj.landtypedqkeffect+=obj1.effect;
}
else{
var obj1=arr_floorroom[i][j-1];
if(!obj.landtypedqk[obj1.type2])
{
obj.landtypedqk[obj1.type2]=obj1.effect;
}
else
{
obj.landtypedqk[obj1.type2]+=obj1.effect;
}
obj.landtypedqkeffect+=obj1.effect;
var obj1=arr_floorroom[i][j+1];
if(!obj.landtypedqk[obj1.type2])
{
obj.landtypedqk[obj1.type2]=obj1.effect;
}
else
{
obj.landtypedqk[obj1.type2]+=obj1.effect;
}
obj.landtypedqkeffect+=obj1.effect;
}
}
}
console.log("对每个地区块进行加权并入库");
for(var i=0;i<len;i++)//对每个地区块进行加权,这段代码执行很慢
{
var len2=arr_floorroom[i].length;
console.log(i+" in "+len);
for(var j=0;j<len2;j++)
{ var obj=arr_floorroom[i][j];
obj.altitude=obj.altitude/obj.countcook;
var rate_type1final=Math.random()*obj.landtypedqkeffect;
var rate_type1final_count=0;
obj.type2final="默认dqk";
for(key in obj.landtypedqk)
{
rate_type1final_count+=obj.landtypedqk[key];//这一种地形的权重
if(rate_type1final<rate_type1final_count)//如果随机数小于这种地形的累积权重
{
obj.type2final=key;
break;
}
}
//在这里把这个地区块插入数据库?不知道什么原因POST方法失败了,改用GET方法
/*Url="http://127.0.0.1:8082/query.do?jsessionid="+jsessionid;
Argv="sql=insert into tab_dqk values(uuid(),'test1',"+obj.beta+","+obj.pbeta+","+obj.alpha+","+obj.palpha+",'"
+obj.type2final+"',"+obj.floor+","+obj.room+","+obj.altitude+")";
//使用同步Ajax请求保证进度同步,在连续使用同步Ajax时不需要xmlHttp.abort()!!!!
Request(xmlHttp,"POST",Url,false,Argv,"application/x-www-form-urlencoded",PushChessCallBack,0);*/
Url="http://127.0.0.1:8082/query.do?jsessionid="+jsessionid+"&sql=insert into tab_dqk values(uuid(),'test1',"+obj.beta+","+obj.pbeta+","+obj.alpha+","+obj.palpha+",'"
+obj.type2final+"',"+obj.floor+","+obj.room+","+obj.altitude+")"
Argv="";
Request(xmlHttp,"GET",Url,false,Argv,"application/x-www-form-urlencoded",PushChessCallBack,0);
}//这里改变了一下数据结构,直接用weight字段存储确定了的地区块级地形
}
CookDqk4();//着色
}

getLandtypeDqk是根据海拔和纬度确定地形的方法:(这个方法效率很低)

 function getLandtypeDqk(obj)
{
var height=obj.altitude;//对于这个地区块
var beta=obj.beta;
var rate_land=Math.random();
for(key in tab_landtypedqk)//这个方法并不能保证顺序!!!!//for key和eval占用了大量时间????
{//按顺序查找每一个地区块级地形是否符合条件,
if(eval(tab_landtypedqk[key].eval))//JavaScript语言的一大特点是可以随时把字符串转化为可执行代码,
{//这使得JavaScript语言可以非常灵活,但是会降低执行效率和安全性
obj.type1=key;
var count_rate=0;//用来累加概率
var obj1=tab_landtypedqk[key];
for(key2 in obj1)
{
if(key2!="eval")
{
var rate_type2=Math.random();
count_rate+=obj1[key2].rate;
if(rate_type2<count_rate)
{
obj.type2=key2;
obj.effect=obj1[key2].effect;//对周边地块的影响程度
break;
}
}
}
break;
}
}
if (!obj.type1)//如果这个地区块没有被分配地形
{
obj.type1="未定义";
obj.type2="默认dqk";
obj.effect=0;
}
obj.landtypedqk={};//这三个变量用于对地形进行平滑处理
obj.landtypedqk[obj.type2]=obj.effect;
obj.landtypedqkeffect=obj.effect;
}

在同一海拔和纬度可能有多种地形存在,每一种地形都有一个出现概率,取一个随机数,如果这个随机数小于遍历到这个地形时的概率累积,则将这个地区块设为这种地形。

每一种地形还有一个effect属性,表示这种地形对周边地形的影响能力,比如如果一片热带雨林周围全被沙漠包围,那么这片雨林有很大可能变成沙漠。

海拔和纬度与地形的对应关系设定如下:(tab_datalib.js)

 //地区块地形元素分布表
var beta_2326=((23+26/60)/180)*Math.PI;//南北回归线弧度
var beta_6034=((60+34/60)/180)*Math.PI;//南北极圈弧度
var beta_8=((8)/180)*Math.PI;//赤道附近弧度
var tab_landtypedqk={//rate按从小到大排列生成的随机数小于哪个就定为何种地形,effect在卷积平滑阶段起作用,表示这个地形对周围环境的影响程度
"热带海洋":{eval:"height<0&&rate_land<0.9&&Math.abs(beta)<beta_2326","热带海洋dqk":{rate:1,effect:1}},//eval是判断地区块大类型的判断条件,以后在设计技能效果时也可能要借鉴这里
"温带海洋":{eval:"height<0&&rate_land<0.9&&Math.abs(beta)>beta_2326&&Math.abs(beta)<beta_6034","温带海洋dqk":{rate:1,effect:1}},
"寒带海洋":{eval:"height<0&&rate_land<0.9&&Math.abs(beta)>beta_6034","寒带海洋dqk":{rate:1,effect:1}},
"温带1500米以下":{eval:"rate_land>0.1&&Math.abs(beta)>beta_2326&&Math.abs(beta)<beta_6034&&height<1500","草原dqk":{rate:0.4,effect:1},"森林dqk":{rate:0.4,effect:1},"戈壁dqk":{rate:0.2,effect:2}},
"温带1500米以上":{eval:"rate_land>0.1&&Math.abs(beta)>beta_2326&&Math.abs(beta)<beta_6034&&height>=1500","雪山dqk":{rate:1,effect:1}},
"亚热带3000米以下":{eval:"rate_land>0.1&&Math.abs(beta)>beta_8&&Math.abs(beta)<beta_2326&&height<3000","热带雨林dqk":{rate:0.5,effect:1},"稀树草原dqk":{rate:0.5,effect:1}},
"亚热带3000米以上":{eval:"rate_land>0.1&&Math.abs(beta)>beta_8&&Math.abs(beta)<beta_2326&&height>=3000","雪山dqk":{rate:1,effect:1}},
"热带3000米以下":{eval:"rate_land>0.1&&Math.abs(beta)<beta_8&&height<3000","热带雨林dqk":{rate:0.5,effect:1},"沙漠dqk":{rate:0.5,effect:2}},
"热带3000米以上":{eval:"rate_land>0.1&&Math.abs(beta)<beta_8&&height>=3000","雪山dqk":{rate:1,effect:1}},
"寒带-100米以下":{eval:"rate_land>0.1&&Math.abs(beta)>beta_6034&&height<-100","草原dqk":{rate:0.4,effect:1},"森林dqk":{rate:0.4,effect:1},"戈壁dqk":{rate:0.2,effect:2}},
"寒带-100到200米以内":{eval:"rate_land>0.1&&Math.abs(beta)>beta_6034&&height<200&&height>=-100","寒带森林dqk":{rate:0.6,effect:1},"冰川dqk":{rate:0.4,effect:1}},
"寒带200米以上":{eval:"rate_land>0.1&&Math.abs(beta)>beta_6034&&height>200","冰川dqk":{rate:1,effect:1}}
}
var tab_landtypedqk2={//每一种地区块的远观颜色和内部地貌块(单位块?)占比
"默认dqk":{color:[250,126,126],content:{"红白格dmk":{rate:1,effect:0}}},//完全红白格,这种表示错误和未定义的地貌块不会影响周围
"热带海洋dqk":{color:[15,63,105],content:{"海洋水面dmk":{rate:0.99,effect:1},"雨林dmk":{rate:0.995,effect:0},"沙滩dmk":{rate:1,effect:0}}},
"温带海洋dqk":{color:[15,63,105],content:{"海洋水面dmk":{rate:0.99,effect:1},"森林dmk":{rate:0.995,effect:0},"沙滩dmk":{rate:1,effect:0}}},
"寒带海洋dqk":{color:[15,63,105],content:{"海洋水面dmk":{rate:0.5,effect:1},"冰面dmk":{rate:1,effect:1}}},
"草原dqk":{color:[93, 153, 63],content:{"草地dmk":{rate:0.95,effect:1},"内陆水面dmk":{rate:1,effect:1}}},
"森林dqk":{color:[33,68,44],content:{"森林dmk":{rate:0.95,effect:1},"内陆水面dmk":{rate:1,effect:1}}},
"戈壁dqk":{color:[127, 102, 79],content:{"戈壁dmk":{rate:1,effect:1}}},
"雪山dqk":{color:[220, 221, 220],content:{"雪地dmk":{rate:0.8,effect:1},"岩石dmk":{rate:1,effect:0}}},
"热带雨林dqk":{color:[33,68,44],content:{"雨林dmk":{rate:0.95,effect:1},"内陆水面dmk":{rate:1,effect:1}}},
"稀树草原dqk":{color:[117, 118, 68],content:{"稀树草原dmk":{rate:0.95,effect:1},"内陆水面dmk":{rate:1,effect:1}}},
"沙漠dqk":{color:[175, 117, 68],content:{"沙地dmk":{rate:0.99,effect:1},"内陆水面dmk":{rate:0.995,effect:0},"绿洲dmk":{rate:1,effect:0}}},
"寒带森林dqk":{color:[],content:{"寒带森林dmk":{rate:0.85,effect:1},"雪地dmk":{rate:1,effect:1}}},
"冰川dqk":{color:[201, 216, 220],content:{"冰面dmk":{rate:0.85,effect:1},"雪地dmk":{rate:1,effect:1}}}
}
var tab_landtypedmk={//每一种地貌块的纹理url
"红白格dmk":{eval_effect:"",Url:"../../ASSETS/IMAGE/Texture_landtypedmk/amiga.jpg",color:[250,126,126]},//对单位的影响,纹理Url,纹理的平均颜色
"海洋水面dmk":{eval_effect:"",Url:"../../ASSETS/IMAGE/Texture_landtypedmk/sea.png",color:[15,63,105]},
"雨林dmk":{eval_effect:"",Url:"../../ASSETS/IMAGE/Texture_landtypedmk/yulin.png",color:[33,68,44]},
"沙滩dmk":{eval_effect:"",Url:"../../ASSETS/IMAGE/Texture_landtypedmk/shatan.png",color:[205, 160, 109]},
"森林dmk":{eval_effect:"",Url:"../../ASSETS/IMAGE/Texture_landtypedmk/yulin.png",color:[33,68,44]},//没找到温带森林,暂时用雨林代替
"冰面dmk":{eval_effect:"",Url:"../../ASSETS/IMAGE/Texture_landtypedmk/ice.png",color:[201, 216, 220]},
"草地dmk":{eval_effect:"",Url:"../../ASSETS/IMAGE/Texture_landtypedmk/grass.png",color:[93, 153, 63]},
"内陆水面dmk":{eval_effect:"",Url:"../../ASSETS/IMAGE/Texture_landtypedmk/lake.png",color:[93,143,180]},
"戈壁dmk":{eval_effect:"",Url:"../../ASSETS/IMAGE/Texture_landtypedmk/gebi.png",color:[127, 102, 79]},
"雪地dmk":{eval_effect:"",Url:"../../ASSETS/IMAGE/Texture_landtypedmk/snow.png",color:[220, 221, 220]},
"岩石dmk":{eval_effect:"",Url:"../../ASSETS/IMAGE/Texture_landtypedmk/stone.png",color:[82, 81, 74]},
"稀树草原dmk":{eval_effect:"../../ASSETS/IMAGE/Texture_landtypedmk/xishucaoyuan.png",Url:"",color:[117, 118, 68]},
"沙地dmk":{eval_effect:"",Url:"../../ASSETS/IMAGE/Texture_landtypedmk/sand.png",color:[175, 117, 68]},
"绿洲dmk":{eval_effect:"",Url:"../../ASSETS/IMAGE/Texture_landtypedmk/lvzhou.png",color:[127, 144, 111]},
"寒带森林dmk":{eval_effect:"",Url:"../../ASSETS/IMAGE/Texture_landtypedmk/lvzhou.png",color:[127, 144, 111]} }

这里设定每一个100km*100km的地区块可以由更小的“地貌块”组成,在极近时地貌块的地形显示为实际的纹理图,在较远时地貌块表现为纹理图的平均色,在更远一些时用地区块代替地貌块,地区块的颜色为地貌块的加权。

目前这个切换功能还未编写,因为时间有限实际地形纹理也没有仔细设置,地区块的颜色加权也没做,直接使用了占比较多的地貌块颜色。

提取纹理图平均颜色的代码如下:(testpix.html)

 <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>提取一副固定大小图片的平均颜色color4</title>
</head>
<body>
<div id="div_allbase">
<canvas style="width: 512px;height: 512px" width="512" height="512" id="can_pic"> </canvas>
</div>
</body>
<script>
var canvas=document.getElementById("can_pic");
window.onload=loadImage;
function loadImage()
{
var context=canvas.getContext("2d");
var img=document.createElement("img");
img.src="../../ASSETS/IMAGE/Texture_landtypedmk/lvzhou.png";
img.onload=function()
{//在图片加载完毕后才可以在canvas里绘制
context.drawImage(img,0,0);
var imagedata_temp=context.getImageData(0,0,512,512);//规定地貌块纹理图片的宽高是512
var data=imagedata_temp.data;
var len=data.length;
var color4=[0,0,0,0];
for(var i=0;i<len;i+=4)
{
color4[0]+=data[i];
color4[1]+=data[i+1];
color4[2]+=data[i+2];
color4[3]+=data[i+3];
}
var int=512*512;
color4[0]=Math.round(color4[0]/int);
color4[1]=Math.round(color4[1]/int);
color4[2]=Math.round(color4[2]/int);
color4[3]=Math.round(color4[3]/int);
console.log(color4);
} }
</script>
</html>

4、生成数据纹理

 function CookDqk4()
{
var len=arr_floorroom.length;
//开始生成数据纹理
console.log("开始生成数据纹理");
for(var i=0;i<len;i++) //每一行
{
//console.log(i+" in "+len);
var len2 = arr_floorroom[i].length;
for (var j = 0; j < len2; j++) {
var obj = arr_floorroom[i][j];
var index= (i)*4*can_temp.width+(j-1)*4;//每个room由4个元素组成
var color4=[];
if(tab_landtypedqk2[obj.type2final].color)
{//从地形对象中获取颜色
color4=tab_landtypedqk2[obj.type2final].color;
}
else
{
color4=[250,126,126];//默认纹理远观颜色
}
imagedata_temp.data[index]=color4[0];//这里存的真的是颜色
imagedata_temp.data[index+1]=color4[1];
imagedata_temp.data[index+2]=color4[2];
imagedata_temp.data[index+3]=255; }
} }

然后用和前面类似的方式将数据纹理送入显卡并进行渲染

着色器代码如下:

顶点着色器:

 uniform mat4 worldViewProjection;
uniform mat4 worldView;
attribute vec3 position; varying vec3 vPosition;
varying vec3 oPosition;//自身坐标系里的位置 void main(){
gl_Position=worldViewProjection*vec4(position,);
vPosition=vec3(worldView*vec4(position,));
oPosition=position;
}

片元着色器:

 precision highp float;
//varying vec4 vColor;
varying vec3 vPosition;
varying vec3 oPosition;
//uniform vec4 uColor;
uniform sampler2D utexturedqk;
uniform float wid_utexturedqk;
uniform float hei_utexturedqk; uniform sampler2D utexturepalpha;//一个单元里保存了四个元素!!!!
//uniform vec3 uarrdqk[60000];//es3.0之前的glsles不支持隐含数组!!!!
uniform float pbeta;
uniform float wid_utexturepalpha;
uniform float hei_utexturepalpha;
//uniform float uarrpalpha[500];//用来测试的行星只有199层,预设为500层应该够了
uniform float uarrpalphalen;
uniform float max_floorf;
uniform float MathPI; float getDataVec4(vec4 data,int id) {
for (int i=; i<; i++) {
if (i == id) return data[i];
}
}
float getData500(float data[],int id) {
int len=int(floor(uarrpalphalen+0.5));
for (int i=; i<; i++) {
if(i>=len)//i不能和非常量比较!!只好换个方式限制它
{
return 0.0;
}
if (i == id) return data[i];
}
}
float getOdevity(float a)//判断浮点型整数的奇偶性,偶返回0,奇返回1
{
float b=mod(a,2.0);
float c=0.0;
if(b>=0.5&&b<1.5)
{
c=1.0;
}
return c;
} void main()
{
//vec4 tempColor=uColor;//es3.0之前不支持round!!!!
//glsl事实上以float为计算基准
//int max_floor=int(floor(max_floorf+0.5));
//算层数
float r=sqrt(oPosition.x*oPosition.x+oPosition.y*oPosition.y+oPosition.z*oPosition.z);
float beta=asin(oPosition.y/r);//俯仰角
//int int_beta=int(floor((beta/(pbeta*2.0))+0.5));//层数
float count_beta=(beta/(pbeta*2.0));
//int index_beta=int(floor(count_beta+ max_floorf+0.5));//整数层数索引
float index_beta=floor(count_beta+ max_floorf+0.5);
//int roomcount=0;
//使用数据纹理法,取这一层的区分度
//int int1=int(floor(mod(index_beta,4.0)+0.5));//使用哪个颜色分量
//float float1=(index_beta/4.0);//在纹理采样器中的顺序索引
float floatu=(mod(index_beta,wid_utexturepalpha))/(wid_utexturepalpha)+(0.5/wid_utexturepalpha);//猜测u是x轴
float floatv=(((index_beta)/(wid_utexturepalpha)))/(hei_utexturepalpha)+(0.5/(wid_utexturepalpha*hei_utexturepalpha));
vec2 UVpalpha=vec2(floatu,floatv);
vec4 vec4palphas=texture2D(utexturepalpha, UVpalpha);
float palpha=(vec4palphas[]*255.0*100.0+vec4palphas[]*255.0)/pow(10.0,vec4palphas[]*255.0);
//float palpha=getData500(uarrpalpha,int(floor(index_beta+0.5)));//改为尝试数组法传递数据
//取这一层的转角
float alpha=atan(oPosition.z,oPosition.x);//标准反正切函数有两个参数!! if(getOdevity(index_beta)==1.0)//为了体现交错效果,如果是奇数层alpha要减去palpha
{
alpha-=palpha;
}
if(alpha<0.0)
{
alpha+=(MathPI*2.0);
}
//取地区块数据纹理的索引
float floatu2=(alpha/(palpha*2.0))/wid_utexturedqk;
float floatv2=index_beta/hei_utexturedqk+0.5/hei_utexturedqk;
vec2 UVdqk=vec2(floatu2,floatv2);
gl_FragColor=texture2D(utexturedqk, UVdqk);
//gl_FragColor=vec4palphas;
//gl_FragColor=texelFetch(utexturedqk,ivec2(int(floor(alpha/(palpha*2.0)+0.5)),int(floor(index_beta+0.5))),0);//这个整数像素的方法是WebGL2开始加入的!!!!
//gl_FragColor=vec4(1.0*floatu,1.0*floatv,1.0*floatv2,1.0);//红,绿,蓝结果不是0就是1??!! //int index_dqk=roomcount-1+int(floor((alpha/palpha)+0.5));
//vec4 tempColor=vec4(uarrdqk[index_dqk],1.0); //float float_3=index_beta/(max_floorf*2.0);
//float float_4=oPosition.y/5.0;
//canvas的imagedata用255,255,255,1定义颜色通道,而glsl用1.0,1.0,1.0,1.0定义!!!! }

片元着色器和前面的区别是加了一个判断奇偶层数的方法,另外,在WebGL2.0中加入的texelFetch方法可以直接根据像素的位置对纹理图进行采样,比使用纹理坐标进行采样方便许多。

执行程序效果如下:

使用着色器在WebGL3D场景中呈现行星表面地形-LMLPHP

拉近相机:

使用着色器在WebGL3D场景中呈现行星表面地形-LMLPHP

其中生成地形耗时1分30秒,平滑地形并入库耗时3分10秒,从数据库查询需要约1分钟

五、总结

这个方法耗时比较长,无法实时应用,但可以用来一次性构建地形多次使用,也许可以通过优化代码来提高运行速度。生成的海洋和陆地混杂度比较高,也许可以考虑从种子位置随机生长地形的算法,这样可以使得大陆和海洋区分的更加分明。着色器代码中没有考虑周期性光照效果,也没有考虑极点处的地区块如何处理。

下一步准备给地区块添加鼠标交互,和更细节的地貌块显示。

05-11 09:39