经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 软件/图像 » WebGL » 查看文章
WebGL简易教程(十二):包围球与投影
来源:cnblogs  作者:charlee44  时间:2019/11/11 8:41:57  对本文有异议

1. 概述

在之前的教程中,都是通过物体的包围盒来设置模型视图投影矩阵(MVP矩阵),来确定物体合适的位置的。但是在很多情况下,使用包围盒并不方便计算,可以利用包围盒再生成一个包围球,利用包围球来设置MVP矩阵。

《WebGL简易教程(十):光照》中,给地形赋予了固定方向的平行光。这篇教程的例子就是想模拟在平行光的视角下地形的情况。对于点光源光,可以用透视投影来实现渲染的效果;而平行光就需要通过正射投影来模拟。并且,这种正射并不是垂直到达地面,而是附带一定角度[1]
image

在这种情况下使用包围盒来计算合适的位置有点难度,使用包围球来设置MVP矩阵更加方便。

2. 实现详解

包围球是利用包围盒生成的,所以首先需要定义一个球体对象:

  1. //定义一个球体
  2. function Sphere(cuboid) {
  3. this.centerX = cuboid.CenterX();
  4. this.centerY = cuboid.CenterY();
  5. this.centerZ = cuboid.CenterZ();
  6. this.radius = Math.max(Math.max(cuboid.LengthX(), cuboid.LengthY()), cuboid.LengthZ()) / 2.0;
  7. }
  8. Sphere.prototype = {
  9. constructor: Sphere
  10. }

这个球体对象的构造函数传入了一个包围盒对象,以包围盒的中心为球体的中心,包围盒长、宽、高的最大值作为包围球的直径。在构造出包围盒之后,利用包围盒参数构造出包围球,将其保存在自定义的Terrain对象中:

  1. var terrain = new Terrain();
  2. //....
  3. terrain.cuboid = new Cuboid(minX, maxX, minY, maxY, minZ, maxZ);
  4. terrain.sphere = new Sphere(terrain.cuboid);

接下来就是改进设置MVP矩阵的函数setMVPMatrix()了。如果仍然想像之前那样进行透视投影,几乎可以不用做改动:

  1. //设置MVP矩阵
  2. function setMVPMatrix(gl, canvas, sphere, lightDirection) {
  3. //...
  4. //投影矩阵
  5. var fovy = 60;
  6. var projMatrix = new Matrix4();
  7. projMatrix.setPerspective(fovy, canvas.width / canvas.height, 1, 10000);
  8. //计算lookAt()函数初始视点的高度
  9. var angle = fovy / 2 * Math.PI / 180.0;
  10. var eyeHight = (sphere.radius * 2 * 1.1) / 2.0 / angle;
  11. //视图矩阵
  12. var viewMatrix = new Matrix4(); // View matrix
  13. viewMatrix.lookAt(0, 0, eyeHight, 0, 0, 0, 0, 1, 0);
  14. //...
  15. }

之前是通过透视变换的张角和包围盒的Y方向长度来计算合适的视野高度,现在只不过将包围盒的Y方向长度换成包围球的直径。这样的写法兼容性更高,因为包围球的直径是包围盒XYZ三个方向的最大长度。这个时候的初始渲染状态为:
image
最后实现下特定角度平行光视角下的地形渲染情况。前面说到过这种情况下是需要设置正射投影的,具体设置过程如下:

  1. //设置MVP矩阵
  2. function setMVPMatrix(gl, canvas, sphere, lightDirection) {
  3. //...
  4. //模型矩阵
  5. var modelMatrix = new Matrix4();
  6. modelMatrix.scale(curScale, curScale, curScale);
  7. modelMatrix.rotate(currentAngle[0], 1.0, 0.0, 0.0); // Rotation around x-axis
  8. modelMatrix.rotate(currentAngle[1], 0.0, 1.0, 0.0); // Rotation around y-axis
  9. modelMatrix.translate(-sphere.centerX, -sphere.centerY, -sphere.centerZ);
  10. //视图矩阵
  11. var viewMatrix = new Matrix4();
  12. var r = sphere.radius + 10;
  13. viewMatrix.lookAt(lightDirection.elements[0] * r, lightDirection.elements[1] * r, lightDirection.elements[2] * r, 0, 0, 0, 0, 1, 0);
  14. //投影矩阵
  15. var projMatrix = new Matrix4();
  16. var diameter = sphere.radius * 2.1;
  17. var ratioWH = canvas.width / canvas.height;
  18. var nearHeight = diameter;
  19. var nearWidth = nearHeight * ratioWH;
  20. projMatrix.setOrtho(-nearWidth / 2, nearWidth / 2, -nearHeight / 2, nearHeight / 2, 1, 10000);
  21. //...
  22. }
  1. 通过模型变换,将世界坐标系的中心平移到包围球的中心。
  2. 设置视图矩阵的时候将观察点放到这个(0,0,0),也就是这个包围球中心;由于视野的方向也就是光线的方向知道,那么可以通过这个方向将视点位置设在与(0,0,0)相距比包围球半径远一点点的位置,就可以保证这个地形都能够被看见。
  3. 通过包围球的直径,来计算正射投影的盒装可视空间的最小范围。

这个时候的初始渲染状态为:
image

3. 具体代码

具体实现代码如下:

  1. // 顶点着色器程序
  2. var VSHADER_SOURCE =
  3. 'attribute vec4 a_Position;\n' + //位置
  4. 'attribute vec4 a_Color;\n' + //颜色
  5. 'attribute vec4 a_Normal;\n' + //法向量
  6. 'uniform mat4 u_MvpMatrix;\n' +
  7. 'varying vec4 v_Color;\n' +
  8. 'varying vec4 v_Normal;\n' +
  9. 'void main() {\n' +
  10. ' gl_Position = u_MvpMatrix * a_Position;\n' + //设置顶点的坐标
  11. ' v_Color = a_Color;\n' +
  12. ' v_Normal = a_Normal;\n' +
  13. '}\n';
  14. // 片元着色器程序
  15. var FSHADER_SOURCE =
  16. 'precision mediump float;\n' +
  17. 'uniform vec3 u_DiffuseLight;\n' + // 漫反射光颜色
  18. 'uniform vec3 u_LightDirection;\n' + // 漫反射光的方向
  19. 'uniform vec3 u_AmbientLight;\n' + // 环境光颜色
  20. 'varying vec4 v_Color;\n' +
  21. 'varying vec4 v_Normal;\n' +
  22. 'void main() {\n' +
  23. //对法向量归一化
  24. ' vec3 normal = normalize(v_Normal.xyz);\n' +
  25. //计算光线向量与法向量的点积
  26. ' float nDotL = max(dot(u_LightDirection, normal), 0.0);\n' +
  27. //计算漫发射光的颜色
  28. ' vec3 diffuse = u_DiffuseLight * v_Color.rgb * nDotL;\n' +
  29. //计算环境光的颜色
  30. ' vec3 ambient = u_AmbientLight * v_Color.rgb;\n' +
  31. ' gl_FragColor = vec4(diffuse+ambient, v_Color.a);\n' +
  32. '}\n';
  33. //定义一个矩形体:混合构造函数原型模式
  34. function Cuboid(minX, maxX, minY, maxY, minZ, maxZ) {
  35. this.minX = minX;
  36. this.maxX = maxX;
  37. this.minY = minY;
  38. this.maxY = maxY;
  39. this.minZ = minZ;
  40. this.maxZ = maxZ;
  41. }
  42. Cuboid.prototype = {
  43. constructor: Cuboid,
  44. CenterX: function () {
  45. return (this.minX + this.maxX) / 2.0;
  46. },
  47. CenterY: function () {
  48. return (this.minY + this.maxY) / 2.0;
  49. },
  50. CenterZ: function () {
  51. return (this.minZ + this.maxZ) / 2.0;
  52. },
  53. LengthX: function () {
  54. return (this.maxX - this.minX);
  55. },
  56. LengthY: function () {
  57. return (this.maxY - this.minY);
  58. },
  59. LengthZ: function () {
  60. return (this.maxZ - this.minZ);
  61. }
  62. }
  63. //定义一个球体
  64. function Sphere(cuboid) {
  65. this.centerX = cuboid.CenterX();
  66. this.centerY = cuboid.CenterY();
  67. this.centerZ = cuboid.CenterZ();
  68. this.radius = Math.max(Math.max(cuboid.LengthX(), cuboid.LengthY()), cuboid.LengthZ()) / 2.0;
  69. }
  70. Sphere.prototype = {
  71. constructor: Sphere
  72. }
  73. //定义DEM
  74. function Terrain() { }
  75. Terrain.prototype = {
  76. constructor: Terrain,
  77. setWH: function (col, row) {
  78. this.col = col;
  79. this.row = row;
  80. }
  81. }
  82. var currentAngle = [0.0, 0.0]; // 绕X轴Y轴的旋转角度 ([x-axis, y-axis])
  83. var curScale = 1.0; //当前的缩放比例
  84. function main() {
  85. var demFile = document.getElementById('demFile');
  86. if (!demFile) {
  87. console.log("Failed to get demFile element!");
  88. return;
  89. }
  90. demFile.addEventListener("change", function (event) {
  91. //判断浏览器是否支持FileReader接口
  92. if (typeof FileReader == 'undefined') {
  93. console.log("你的浏览器不支持FileReader接口!");
  94. return;
  95. }
  96. var input = event.target;
  97. var reader = new FileReader();
  98. reader.onload = function () {
  99. if (reader.result) {
  100. //读取
  101. var terrain = new Terrain();
  102. if (!readDEMFile(reader.result, terrain)) {
  103. console.log("文件格式有误,不能读取该文件!");
  104. }
  105. //绘制
  106. onDraw(gl, canvas, terrain);
  107. }
  108. }
  109. reader.readAsText(input.files[0]);
  110. });
  111. // 获取 <canvas> 元素
  112. var canvas = document.getElementById('webgl');
  113. // 获取WebGL渲染上下文
  114. var gl = getWebGLContext(canvas);
  115. if (!gl) {
  116. console.log('Failed to get the rendering context for WebGL');
  117. return;
  118. }
  119. // 初始化着色器
  120. if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
  121. console.log('Failed to intialize shaders.');
  122. return;
  123. }
  124. // 指定清空<canvas>的颜色
  125. gl.clearColor(0.0, 0.0, 0.0, 1.0);
  126. // 开启深度测试
  127. gl.enable(gl.DEPTH_TEST);
  128. //清空颜色和深度缓冲区
  129. gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  130. }
  131. //绘制函数
  132. function onDraw(gl, canvas, terrain) {
  133. // 设置顶点位置
  134. var n = initVertexBuffers(gl, terrain);
  135. if (n < 0) {
  136. console.log('Failed to set the positions of the vertices');
  137. return;
  138. }
  139. //注册鼠标事件
  140. initEventHandlers(canvas);
  141. //设置灯光
  142. var lightDirection = setLight(gl);
  143. //绘制函数
  144. var tick = function () {
  145. //设置MVP矩阵
  146. setMVPMatrix(gl, canvas, terrain.sphere, lightDirection);
  147. //清空颜色和深度缓冲区
  148. gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  149. //绘制矩形体
  150. gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_SHORT, 0);
  151. //请求浏览器调用tick
  152. requestAnimationFrame(tick);
  153. };
  154. //开始绘制
  155. tick();
  156. }
  157. //设置灯光
  158. function setLight(gl) {
  159. var u_AmbientLight = gl.getUniformLocation(gl.program, 'u_AmbientLight');
  160. var u_DiffuseLight = gl.getUniformLocation(gl.program, 'u_DiffuseLight');
  161. var u_LightDirection = gl.getUniformLocation(gl.program, 'u_LightDirection');
  162. if (!u_DiffuseLight || !u_LightDirection || !u_AmbientLight) {
  163. console.log('Failed to get the storage location');
  164. return;
  165. }
  166. //设置漫反射光
  167. gl.uniform3f(u_DiffuseLight, 1.0, 1.0, 1.0);
  168. // 设置光线方向(世界坐标系下的)
  169. var solarAltitude = 45.0;
  170. var solarAzimuth = 315.0;
  171. var fAltitude = solarAltitude * Math.PI / 180; //光源高度角
  172. var fAzimuth = solarAzimuth * Math.PI / 180; //光源方位角
  173. var arrayvectorX = Math.cos(fAltitude) * Math.cos(fAzimuth);
  174. var arrayvectorY = Math.cos(fAltitude) * Math.sin(fAzimuth);
  175. var arrayvectorZ = Math.sin(fAltitude);
  176. var lightDirection = new Vector3([arrayvectorX, arrayvectorY, arrayvectorZ]);
  177. lightDirection.normalize(); // Normalize
  178. gl.uniform3fv(u_LightDirection, lightDirection.elements);
  179. //设置环境光
  180. gl.uniform3f(u_AmbientLight, 0.2, 0.2, 0.2);
  181. return lightDirection;
  182. }
  183. //读取DEM函数
  184. function readDEMFile(result, terrain) {
  185. var stringlines = result.split("\n");
  186. if (!stringlines || stringlines.length <= 0) {
  187. return false;
  188. }
  189. //读取头信息
  190. var subline = stringlines[0].split("\t");
  191. if (subline.length != 6) {
  192. return false;
  193. }
  194. var col = parseInt(subline[4]); //DEM宽
  195. var row = parseInt(subline[5]); //DEM高
  196. var verticeNum = col * row;
  197. if (verticeNum + 1 > stringlines.length) {
  198. return false;
  199. }
  200. terrain.setWH(col, row);
  201. //读取点信息
  202. var ci = 0;
  203. var pSize = 9;
  204. terrain.verticesColors = new Float32Array(verticeNum * pSize);
  205. for (var i = 1; i < stringlines.length; i++) {
  206. if (!stringlines[i]) {
  207. continue;
  208. }
  209. var subline = stringlines[i].split(',');
  210. if (subline.length != pSize) {
  211. continue;
  212. }
  213. for (var j = 0; j < pSize; j++) {
  214. terrain.verticesColors[ci] = parseFloat(subline[j]);
  215. ci++;
  216. }
  217. }
  218. if (ci !== verticeNum * pSize) {
  219. return false;
  220. }
  221. //包围盒
  222. var minX = terrain.verticesColors[0];
  223. var maxX = terrain.verticesColors[0];
  224. var minY = terrain.verticesColors[1];
  225. var maxY = terrain.verticesColors[1];
  226. var minZ = terrain.verticesColors[2];
  227. var maxZ = terrain.verticesColors[2];
  228. for (var i = 0; i < verticeNum; i++) {
  229. minX = Math.min(minX, terrain.verticesColors[i * pSize]);
  230. maxX = Math.max(maxX, terrain.verticesColors[i * pSize]);
  231. minY = Math.min(minY, terrain.verticesColors[i * pSize + 1]);
  232. maxY = Math.max(maxY, terrain.verticesColors[i * pSize + 1]);
  233. minZ = Math.min(minZ, terrain.verticesColors[i * pSize + 2]);
  234. maxZ = Math.max(maxZ, terrain.verticesColors[i * pSize + 2]);
  235. }
  236. terrain.cuboid = new Cuboid(minX, maxX, minY, maxY, minZ, maxZ);
  237. terrain.sphere = new Sphere(terrain.cuboid);
  238. return true;
  239. }
  240. //注册鼠标事件
  241. function initEventHandlers(canvas) {
  242. var dragging = false; // Dragging or not
  243. var lastX = -1,
  244. lastY = -1; // Last position of the mouse
  245. //鼠标按下
  246. canvas.onmousedown = function (ev) {
  247. var x = ev.clientX;
  248. var y = ev.clientY;
  249. // Start dragging if a moue is in <canvas>
  250. var rect = ev.target.getBoundingClientRect();
  251. if (rect.left <= x && x < rect.right && rect.top <= y && y < rect.bottom) {
  252. lastX = x;
  253. lastY = y;
  254. dragging = true;
  255. }
  256. };
  257. //鼠标离开时
  258. canvas.onmouseleave = function (ev) {
  259. dragging = false;
  260. };
  261. //鼠标释放
  262. canvas.onmouseup = function (ev) {
  263. dragging = false;
  264. };
  265. //鼠标移动
  266. canvas.onmousemove = function (ev) {
  267. var x = ev.clientX;
  268. var y = ev.clientY;
  269. if (dragging) {
  270. var factor = 100 / canvas.height; // The rotation ratio
  271. var dx = factor * (x - lastX);
  272. var dy = factor * (y - lastY);
  273. currentAngle[0] = currentAngle[0] + dy;
  274. currentAngle[1] = currentAngle[1] + dx;
  275. }
  276. lastX = x, lastY = y;
  277. };
  278. //鼠标缩放
  279. canvas.onmousewheel = function (event) {
  280. if (event.wheelDelta > 0) {
  281. curScale = curScale * 1.1;
  282. } else {
  283. curScale = curScale * 0.9;
  284. }
  285. };
  286. }
  287. //设置MVP矩阵
  288. function setMVPMatrix(gl, canvas, sphere, lightDirection) {
  289. // Get the storage location of u_MvpMatrix
  290. var u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');
  291. if (!u_MvpMatrix) {
  292. console.log('Failed to get the storage location of u_MvpMatrix');
  293. return;
  294. }
  295. //模型矩阵
  296. var modelMatrix = new Matrix4();
  297. modelMatrix.scale(curScale, curScale, curScale);
  298. modelMatrix.rotate(currentAngle[0], 1.0, 0.0, 0.0); // Rotation around x-axis
  299. modelMatrix.rotate(currentAngle[1], 0.0, 1.0, 0.0); // Rotation around y-axis
  300. modelMatrix.translate(-sphere.centerX, -sphere.centerY, -sphere.centerZ);
  301. /*
  302. //----------------------透视---------------------
  303. //投影矩阵
  304. var fovy = 60;
  305. var projMatrix = new Matrix4();
  306. projMatrix.setPerspective(fovy, canvas.width / canvas.height, 1, 10000);
  307. //计算lookAt()函数初始视点的高度
  308. var angle = fovy / 2 * Math.PI / 180.0;
  309. var eyeHight = (sphere.radius * 2 * 1.1) / 2.0 / angle;
  310. //视图矩阵
  311. var viewMatrix = new Matrix4(); // View matrix
  312. viewMatrix.lookAt(0, 0, eyeHight, 0, 0, 0, 0, 1, 0);
  313. //----------------------透视---------------------
  314. */
  315. //----------------------正射---------------------
  316. //视图矩阵
  317. var viewMatrix = new Matrix4();
  318. var r = sphere.radius + 10;
  319. viewMatrix.lookAt(lightDirection.elements[0] * r, lightDirection.elements[1] * r, lightDirection.elements[2] * r, 0, 0, 0, 0, 1, 0);
  320. //投影矩阵
  321. var projMatrix = new Matrix4();
  322. var diameter = sphere.radius * 2.1;
  323. var ratioWH = canvas.width / canvas.height;
  324. var nearHeight = diameter;
  325. var nearWidth = nearHeight * ratioWH;
  326. projMatrix.setOrtho(-nearWidth / 2, nearWidth / 2, -nearHeight / 2, nearHeight / 2, 1, 10000);
  327. //----------------------正射---------------------
  328. //MVP矩阵
  329. var mvpMatrix = new Matrix4();
  330. mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);
  331. //将MVP矩阵传输到着色器的uniform变量u_MvpMatrix
  332. gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);
  333. }
  334. //
  335. function initVertexBuffers(gl, terrain) {
  336. //DEM的一个网格是由两个三角形组成的
  337. // 0------1 1
  338. // | |
  339. // | |
  340. // col col------col+1
  341. var col = terrain.col;
  342. var row = terrain.row;
  343. var indices = new Uint16Array((row - 1) * (col - 1) * 6);
  344. var ci = 0;
  345. for (var yi = 0; yi < row - 1; yi++) {
  346. //for (var yi = 0; yi < 10; yi++) {
  347. for (var xi = 0; xi < col - 1; xi++) {
  348. indices[ci * 6] = yi * col + xi;
  349. indices[ci * 6 + 1] = (yi + 1) * col + xi;
  350. indices[ci * 6 + 2] = yi * col + xi + 1;
  351. indices[ci * 6 + 3] = (yi + 1) * col + xi;
  352. indices[ci * 6 + 4] = (yi + 1) * col + xi + 1;
  353. indices[ci * 6 + 5] = yi * col + xi + 1;
  354. ci++;
  355. }
  356. }
  357. //
  358. var verticesColors = terrain.verticesColors;
  359. var FSIZE = verticesColors.BYTES_PER_ELEMENT; //数组中每个元素的字节数
  360. // 创建缓冲区对象
  361. var vertexColorBuffer = gl.createBuffer();
  362. var indexBuffer = gl.createBuffer();
  363. if (!vertexColorBuffer || !indexBuffer) {
  364. console.log('Failed to create the buffer object');
  365. return -1;
  366. }
  367. // 将缓冲区对象绑定到目标
  368. gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);
  369. // 向缓冲区对象写入数据
  370. gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);
  371. //获取着色器中attribute变量a_Position的地址
  372. var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
  373. if (a_Position < 0) {
  374. console.log('Failed to get the storage location of a_Position');
  375. return -1;
  376. }
  377. // 将缓冲区对象分配给a_Position变量
  378. gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 9, 0);
  379. // 连接a_Position变量与分配给它的缓冲区对象
  380. gl.enableVertexAttribArray(a_Position);
  381. //获取着色器中attribute变量a_Color的地址
  382. var a_Color = gl.getAttribLocation(gl.program, 'a_Color');
  383. if (a_Color < 0) {
  384. console.log('Failed to get the storage location of a_Color');
  385. return -1;
  386. }
  387. // 将缓冲区对象分配给a_Color变量
  388. gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 9, FSIZE * 3);
  389. // 连接a_Color变量与分配给它的缓冲区对象
  390. gl.enableVertexAttribArray(a_Color);
  391. // 向缓冲区对象分配a_Normal变量,传入的这个变量要在着色器使用才行
  392. var a_Normal = gl.getAttribLocation(gl.program, 'a_Normal');
  393. if (a_Normal < 0) {
  394. console.log('Failed to get the storage location of a_Normal');
  395. return -1;
  396. }
  397. gl.vertexAttribPointer(a_Normal, 3, gl.FLOAT, false, FSIZE * 9, FSIZE * 6);
  398. //开启a_Normal变量
  399. gl.enableVertexAttribArray(a_Normal);
  400. // 将顶点索引写入到缓冲区对象
  401. gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
  402. gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
  403. return indices.length;
  404. }

4. 参考

本来部分代码和插图来自《WebGL编程指南》,源代码链接:地址 。会在此共享目录中持续更新后续的内容。

[1] Directx11教程三十一之ShadowMap(阴影贴图)之平行光成影

原文链接:http://www.cnblogs.com/charlee44/p/11828120.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号