今天和大家分享的是webgl渲染树形结构的流程。用过threejs,babylonjs的同学都知道,一个大模型都是由n个子模型拼装而成的,那么如何依次渲染子模型,以及渲染每个子模型在原生webgl中的流程是怎样的呢,我就以osg框架为原本,为同学们展示出来。

  首先介绍osg框架,该框架是基于openGL的几何引擎框架,目前我的工作是将其翻译成为webgl的几何引擎,在这个过程中学习webgl原生架构的原理和工程构造方式,踩了各种坑,每次爬出坑都觉得自己又强大了一点,呵。

  闲话少叙,切入正题,首先我们要明确一个渲染流程,那就是webgl到底是怎么将模型绘制到canvas画布中去的,这就牵扯到我之前的一片文章《原生WebGL场景中绘制多个圆锥圆柱》,链接地址https://www.cnblogs.com/ccentry/p/9864847.html,这篇文章讲述的是用原生webgl向canvas中持续绘制多个模型,但这篇文章的局限性在于,他重复使用了同一组shader(顶点shader,片段shader),并且多个模型也不存在父子关系,这就导致了局部坐标系和全局坐标系的紊乱。今天我们就来弥补这篇文章的不足之处。

  按部就班,我们先讨论的是webgl渲染单个模型的过程,首先我们构造着色器,请看下面着色器代码

  1. attribute vec3 position;
  2. attribute vec3 normal;
  3. attribute vec4 color;
  4. uniform mat4 mvpMatrix;
  5. uniform mat4 invMatrix;
  6. uniform vec3 lightDirection;
  7. uniform vec4 ambientColor;
  8. varying vec4 vColor;
  9. uniform float lightS;
  10.  
  11. void main(void){
  12. vec3 invLight = normalize(invMatrix * vec4(lightDirection, )).xyz;
  13. float diffuse = clamp(dot(normal, invLight), 0.0, 1.0) * lightS;
  14. vColor = color * vec4(vec3(diffuse), 1.0) + ambientColor;
  15. gl_Position = mvpMatrix * vec4(position, 1.0);
  16. }

顶点着色器

  1. precision mediump float;
  2. varying vec4 vColor;
  3. void main(void){
  4. gl_FragColor = vColor;
  5. }

片段着色器
  好了,接下来我们的模型数据怎么和着色器进行数据链接呢,很简单,我们首先创建着色器的gl对象,用以js传参,请看代码

  1. /**
  2. * 生成着色器的函数
  3. */
  4. function create_shader(id){
  5. // 用来保存着色器的变量
  6. var shader;
  7.  
  8. // 根据id从HTML中获取指定的script标签
  9. var scriptElement = document.getElementById(id);
  10.  
  11. // 如果指定的script标签不存在,则返回
  12. if(!scriptElement){return;}
  13.  
  14. // 判断script标签的type属性
  15. switch(scriptElement.type){
  16.  
  17. // 顶点着色器的时候
  18. case 'x-shader/x-vertex':
  19. shader = gl.createShader(gl.VERTEX_SHADER);
  20. break;
  21.  
  22. // 片段着色器的时候
  23. case 'x-shader/x-fragment':
  24. shader = gl.createShader(gl.FRAGMENT_SHADER);
  25. break;
  26. default :
  27. return;
  28. }
  29.  
  30. // 将标签中的代码分配给生成的着色器
  31. gl.shaderSource(shader, scriptElement.text);
  32.  
  33. // 编译着色器
  34. gl.compileShader(shader);
  35.  
  36. // 判断一下着色器是否编译成功
  37. if(gl.getShaderParameter(shader, gl.COMPILE_STATUS)){
  38.  
  39. // 编译成功,则返回着色器
  40. return shader;
  41. }else{
  42.  
  43. // 编译失败,弹出错误消息
  44. alert(gl.getShaderInfoLog(shader));
  45. }
  46. }

这个函数生成的既可以是顶点着色器,也可以是片段着色器,在此不加赘述,有了着色器的gl对象,我们就能向着色器里传attribute和uniform参数了吗,显然不行,那么接下来我们就要构造一个可以向着色器对象传参数的程序对象gl.program,这也是难点之一,先看代码

  1. /**
  2. * 程序对象的生成和着色器连接的函数
  3. */
  4. function create_program(vs, fs){
  5. // 程序对象的生成
  6. var program = gl.createProgram();
  7.  
  8. // 向程序对象里分配着色器
  9. gl.attachShader(program, vs);
  10. gl.attachShader(program, fs);
  11.  
  12. // 将着色器连接
  13. gl.linkProgram(program);
  14.  
  15. // 判断着色器的连接是否成功
  16. if(gl.getProgramParameter(program, gl.LINK_STATUS)){
  17.  
  18. // 成功的话,将程序对象设置为有效
  19. gl.useProgram(program);
  20.  
  21. // 返回程序对象
  22. return program;
  23. }else{
  24.  
  25. // 如果失败,弹出错误信息
  26. alert(gl.getProgramInfoLog(program));
  27. }
  28. }

我们看到这个函数做了两件事,第一gl.attachShader将我们刚刚生成的着色器对象绑定到gl.program编程对象上,第二件事就是gl.useProgram激活绑定好着色器对象的编程对象,当然第二件事有待商榷,那就是如果我们有多个gl.program是不是每次创建绑定好着色器都要激活,这个要看具体使用场景,再次说明,这里这种绑定立即激活的方式不建议使用,一半都是绑定完成后等到要使用时才激活。为了这个还被lasoy老师批评了,哈哈,再次膜拜lasoy老师,阿里大佬。

好了,现在我们有了gl.program编程对象,就能够安心的向shader里传attribute和uniform参数了,具体传参方法不是我们这篇文章讨论的重点,请参考我的上一篇博客《原生WebGL场景中绘制多个圆锥圆柱》,链接地址https://www.cnblogs.com/ccentry/p/9864847.html。

  接下来我们进入正题,持续绘制多个模型进一个canvas场景。也许同学们要说,这很简单啊,每次要绘制一个模型进入场景,就重复上述过程,先构造着色器对象gl.createShader(v-shader1),gl.createShader(f-shader1),然后绑定到程序对象gl.createProgram(program1)上,激活一下gl.useProgram(program1),接下来该穿attribute/uniform就传参,直接gl.drawElement()不就行了嘛,要绘制多少不同的模型就调用这个过程多少次,不就可以了嘛,哪来那么多废话,是不是。对于这种论调,我只能说,逻辑上是完全正确的,也能够正确无误地将多个长相各异的模型持续绘制进同一个canvas场景,没毛病。同学们就要喷了,那你bb了半天,想说啥呢?好,我就来说一说这么做的坏处是什么。请看下面场景

场景中的红色圆锥和红色圆柱的绘制方式就是类似刚才那种思想,不断重复构造着色器对象v-shader1 = gl.createShader(),f-shader1 = gl.createShader(),绑定编程program1 = gl.createProgram(v-shader1,f-shader1),激活编程对象gl.useProgram(program1),然后传attribute/uniform参数给着色器,空间位置姿态变换,gl.drawElement(),从而绘制出圆锥;再来就是重复这个过程绘制出圆柱,唯一稍有区别的是,我偷懒没重新构造shader对象,重新绑定program对象,而是重复利用同一套shader和program,只不过每次绘制传参attribute重新传了一次,覆盖前一次的attribute而已,原理其实一模一样,大家不要学我偷懒。有的同学看到结果以后更来劲了,你看看,这不是挺好吗,持续绘制多个不同模型成功了呀,有啥问题呀?那么我就说说问题在哪里。首先会发生交互的全局坐标系紊乱,请看下图

我们看到,整个模型错位了,原因就是空间变换矩阵并不能和每个模型相对于世界坐标系的相对局部坐标系矩阵正确相乘,这就是零散绘制多模型的坑。解决这个问题的方法就是采用树结构绘制子模型。这也是本文的核心论点,接下来我们就来看看如何采用树结构绘制,树的每个节点存储的又是什么对象。

  由于osg和threejs都有自己的树结构,所以我也模仿二者自己构造了我的树,请看下面代码

  1. /**
  2. * 坐标系
  3. * */
  4. let Section = require('./Section');
  5. let Operation = require('./Operation');
  6.  
  7. let Geometry = require('../core/Geometry');
  8. let MatrixTransform = require('../core/MatrixTransform');
  9. let StateSet = require('../core/StateSet');
  10. let StateAttribute = require('../core/StateAttribute');
  11. let BufferArray = require('../core/BufferArray');
  12. let DrawElements = require('../core/DrawElements');
  13. let Primitives = require('../core/Primitives');
  14. let Depth = require('../core/Depth');
  15. let LineWidth = require('../core/LineWidth');
  16. let Material = require('../core/Material');
  17. let BlendFunc = require('../core/BlendFunc');
  18. let Algorithm = require('../util/Algorithm');
  19. let BoundingBox = require('../util/BoundingBox');
  20. let Vec3 = require('../util/Vec3');
  21. let Vec4 = require('../util/Vec4');
  22. let Plane = require('../util/Plane');
  23. let Quat = require('../util/Quat');
  24. let Mat4 = require('../util/Mat4');
  25. let Utils = require('../util/Utils');
  26. let ShaderFactory = require('../render/ShaderFactory');
  27. let PolyhedronGeometry = require('../model/polyhedron');
  28. let Group = require('../core/Group');
  29.  
  30. let CoordinateSection = function(viewer){
  31. Section.call(this, viewer);
  32. //坐标系模型的空间位置和姿态矩阵
  33. this.position = Mat4.new();
  34. this._coordRoot = undefined;
  35.  
  36. this._scale = Vec3.create(, , );
  37. this._translate = Vec3.new();
  38. this._rotate = Quat.new();
  39.  
  40. this._scaleMatrix = Mat4.new();
  41. this._translateMatrix = Mat4.new();
  42. this._rotateMatrix = Mat4.new();
  43. };
  44. //继承Section类
  45. CoordinateSection.prototype = Object.create(Section.prototype);
  46. CoordinateSection.prototype.constructor = CoordinateSection;
  47.  
  48. CoordinateSection.prototype = {
  49.  
  50. /**
  51. * 创建坐标系模型
  52. * root:scene根节点
  53. * */
  54. create : function(root){
  55. //初始化坐标系根节点
  56. this._coordRoot = new MatrixTransform();
  57. //几何
  58. let polyhedronGeo = new PolyhedronGeometry();
  59. //构造单位尺寸的模型
  60. polyhedronGeo.getCone(0.2, 0.5, );
  61. let geoms = polyhedronGeo.vertices;
  62. let array = new Float32Array(geoms);
  63. let vertexBuffer = new BufferArray(BufferArray.ARRAY_BUFFER, array, );
  64. //面索引绘制方式
  65. let indices = [];
  66. indices = polyhedronGeo.faces;
  67. //几何体类实例
  68. let geom = new Geometry();
  69. geom.setBufferArray('Vertex', vertexBuffer);
  70. let index = new Int8Array(indices);
  71. let indexBuffer = new BufferArray(BufferArray.ELEMENT_ARRAY_BUFFER, index, index.length);
  72. let prim = new DrawElements(Primitives.TRIANGLES, indexBuffer);
  73. geom.setPrimitive(prim);
  74. //将几何对象加入坐标系根节点
  75. this._coordRoot.addChild(geom);
  76. //渲染组件
  77. let stateSet = new StateSet();
  78. //使用ColorDefaultProgram这组着色器
  79. stateSet.addAttribute(ShaderFactory.createColorDefault.call(this));
  80. stateSet.addAttribute(new Material([, 0.5, , ]));
  81. stateSet.addAttribute(new BlendFunc(BlendFunc.SRC_ALPHA, BlendFunc.ONE_MINUS_SRC_ALPHA));
  82. stateSet.addAttribute(new Depth(Depth.LESS, 0.1, 0.9, false));//深度值在中间
  83. this._coordRoot.setStateSet(stateSet);
  84. //将坐标系根节点加入场景根节点
  85. root.addChild(this._coordRoot);
  86. },
  87. /**
  88. * 调整坐标轴尺寸姿态
  89. * boundingBox:scene场景的包围盒
  90. * vec3Translate:场景平移向量
  91. * vec4Rotate:场景旋转四元数
  92. * vec3Scale:场景缩放向量
  93. * mat4Scale:场景缩放矩阵
  94. * mat4Translate:场景平移矩阵
  95. * mat4Rotate:场景旋转矩阵
  96. * worldMatrix:当前场景的世界坐标
  97. * */
  98. update: function (boundingBox, vec3Scale, vec3Translate, vec4Rotate, mat4Scale, mat4Translate, mat4Rotate, worldMatrix) {
  99. if(boundingBox instanceof BoundingBox){//先保证boundingBox是BoundingBox类的实例
  100. let vecSRaw = Vec3.new();
  101. Vec3.copy(vecSRaw, vec3Scale);//克隆缩放向量,防止污染场景缩放向量
  102. let vecS = Vec3.new();
  103. this.computeScale(vecS, vecSRaw);//取场景缩放最长边的1/4作为坐标系模型缩放比例
  104. let vecT = Vec3.new();
  105. Vec3.copy(vecT, vec3Translate);//克隆平移向量,防止污染场景平移向量
  106. let vecR = Vec4.new();
  107. Vec4.copy(vecR, vec4Rotate);//克隆旋转向量,防止污染场景旋转向量
  108. if (boundingBox.valid()) {//场景模型存在的话
  109. let min = boundingBox.getMin();
  110. let max = boundingBox.getMax();
  111. boundingBox.getCenter(vec3Translate);
  112. Vec3.sub(this._scale, max, min);
  113. }
  114. let matW = Mat4.new();
  115. Mat4.copy(matW, worldMatrix); //克隆一个世界坐标系矩阵,防止修改场景包围盒的矩阵
  116. let matS = Mat4.new();
  117. Mat4.copy(matS, mat4Scale); //克隆一个缩放矩阵,防止污染场景包围盒的缩放矩阵
  118. let matT = Mat4.new();
  119. Mat4.copy(matT, mat4Translate); //克隆一个平移矩阵,防止污染场景包围盒的平移矩阵
  120. let matR = Mat4.new();
  121. Mat4.copy(matR, mat4Rotate); //克隆一个旋转矩阵,防止污染场景包围盒的旋转矩阵
  122. Mat4.fromScaling(matS, vecS);
  123. Mat4.fromTranslation(matT, vecT);
  124. Mat4.fromQuat(matR, vecR);
  125.  
  126. Mat4.mul(matW, matT, matR);
  127. Mat4.mul(matW, matW, matS);
  128.  
  129. this._coordRoot._matrix = matW;
  130. }
  131. },
  132. //计算坐标系缩放比例
  133. computeScale : function(newScale, boundingBoxScale){
  134. //取场景模型包围盒最长一边的1/4
  135. var scale = boundingBoxScale[] > boundingBoxScale[] ? boundingBoxScale[] : boundingBoxScale[];
  136. scale = scale > boundingBoxScale[] ? scale : boundingBoxScale[];
  137. scale *= /;
  138. newScale[] = scale;
  139. newScale[] = scale;
  140. newScale[] = scale;
  141. }
  142. };
  143.  
  144. module.exports = CoordinateSection;

在这个构造类中,我将坐标系模型做成了一个根节点coordRoot,这个根节点下挂载了一个子模型(圆锥),该子模型下又挂载了三个子节点,一、geometry几何特征;二、transformMatrix千万注意是相对于他的父节点的空间变换矩阵,不是相对于世界坐标系的空间变换矩阵,千万注意;三、stateSet着色器相关对象,就是实现shader,program,传参attribute,uniform,空间变换,drawElement相关的配置和操作对象。这样做的好处就显而易见了,遍历整棵模型树,我既能将树上每一个节点都绑定不同的shader绘制出来,又能知道子节点相对于父节点的空间变换矩阵,就不会出现刚才那种错位的事了。
  同学们看到这里应该明白树形结构加载多个子模型的好处了,由于这次的代码并不完整,osg也需要nodejs的运行环境,所以事先说明,贴出的代码只是为了帮助说明观点,本文代码只是局部关键部位,并不能运行,如有问题,可以交流。引用本文请注明出处https://www.cnblogs.com/ccentry/p/9903166.html

WebGL树形结构的模型渲染流程的更多相关文章

  1. 树形结构的数据渲染(element-ui&VUE)

    在最开始学习的时候,渲染树形数据没有好好理解. 在实际的运用开发中,彻底的走了一遍树形数据,渲染角色权限的业务逻辑. 首先先发送请求获取全部权限树形结构, 其次发送请求获取当前用户的权限, 最后,通过 ...

  2. ejs模版实现递归树形结构渲染

    使用过前端模板的同学们,尤其是使用过nodejs写后台服务的同学们,应该对ejs模板和jade模板都不陌生.对与ejs模板和jade模板孰强孰弱,载各大论坛中一直争论不休,有说ejs更直观的,也有说j ...

  3. 在NLP中深度学习模型何时需要树形结构?

    在NLP中深度学习模型何时需要树形结构? 前段时间阅读了Jiwei Li等人[1]在EMNLP2015上发表的论文<When Are Tree Structures Necessary for ...

  4. 后台返回平铺数据,如何转换成树形json并渲染树形结构,ant tree 异步加载

    如何后台返回对象数组(平铺式) 1.根据字段标识(板块)获取根节点 ### initTreeData(dataOrg){ var resultArr=dataOrg[0] var secArr=[]; ...

  5. 【Stage3D学习笔记续】山寨Starling(三):Starling核心渲染流程

    这篇文章我们剔除Starling的Touch事件体系和动画体系,专门来看看Starling中的渲染流程实现,以及其搭建的显示列表结构. 由于Starling是模仿Flash的原生显示列表,所以我们可以 ...

  6. Ogre内部渲染流程分析系列

    come from:http://blog.csdn.net/weiqubo/article/details/6956005 要理解OGRE引擎,就要理解其中占很重要位置的 Renderable接口, ...

  7. 转:Ogre内部渲染流程

    以下是 Ogre 的代码中的详细说明: Renderable是OGRE中所有可渲染对象的抽象接口 这个接口抽象出了在渲染管线中的被分组的离散的可渲染对象基本的方法. 此接口的实现类必须是基于单一的材质 ...

  8. [Swing]树形结构的实现

    一般步骤: 1.建立根节点 private DefaultMutableTreeNode root = new DefaultMutableTreeNode("根节点"); 2.建 ...

  9. 浏览器渲染流程&Composite(渲染层合并)简单总结

    梳理浏览器渲染流程 首先简单了解一下浏览器请求.加载.渲染一个页面的大致过程: DNS 查询 TCP 连接 HTTP 请求即响应 服务器响应 客户端渲染 这里主要将客户端渲染展开梳理一下,从浏览器器内 ...

随机推荐

  1. 移动端h5列表页上拉加载更多

    背景 上星期公司要求做一个回收书籍的h5给安卓用,里面有一个功能是回收记录列表.设计师那边出的稿子是没有要求分页或者是上拉刷新的,但是众所周知,列表页数据很多的情况下,h5加载是很慢的.所以我一开始是 ...

  2. JNI由浅入深_8_JNI缓存字段和方法ID

    获取字段ID和方法ID时,需要用字段.方法的名字和描述符进行一个检索.检索过程相对比较费时,因此本节讨论用缓存技术来减少这个过程带来的消耗.缓存字段ID和方法ID的方法主要有两种.两种区别主要在于缓存 ...

  3. 查询优化百万条数据量的MySQL表

    转自https://www.cnblogs.com/llzhang123/p/9239682.html 1.两种查询引擎查询速度(myIsam 引擎 ) InnoDB 中不保存表的具体行数,也就是说, ...

  4. MySql 5.7.21免安装版本win10下的配置

    1.解压到想要安装的位置,创建my.ini文件 my.ini的内容如下 [mysql] # 设置mysql客户端默认字符集 default-character-set=utf8 [mysqld] #设 ...

  5. tp框架如何处理mysql先排序在分组

    $giModel = M('GroupIntegral'); $gi_table = $giModel->order('id desc')->limit('999')->buildS ...

  6. Linux(CentOS7)设置自动备份数据库到阿里云OSS

    环境:阿里云服务器CentOS7.4 + MySQL5.6 基本思路: 1.编写shell脚本,备份数据库到指定目录下 2.编写Python脚本,把文件上传到OSS 3.把shell脚本和Python ...

  7. Idea项目中常见错误及笔记(Old)

    1.Idea基础设置: File-->settings--> 1>修改字体:Font 2>修改编码格式:File Encodings(全部UTF-8,右下方复选框勾中--防止程 ...

  8. [上架] iOS 上架更新版本号建议

    iOS 上架一個新版本号,就改个版号数字就好,有什么好说的? 是啊~ 如果上架顺利的话,就没什么好说的,如果被退件,再上传更新时,那版号怎么改? 下面说说我的做法(这只是建议,版号随自己喜好,没有固定 ...

  9. Pylint 使用手册(正在努力翻译中)

    本篇文章长期更新 本文翻译自:https://pylint.readthedocs.io/en/latest/ 如果本文有哪里翻译不妥,请在本文下方评论处指出 ^_^ 版权声明:原创作品,允许转载,转 ...

  10. Docker Toolbox下配置国内镜像源-阿里云加速器

    Docker machine安装 Docker Machine是一个简化Docker安装的命令行工具,通过一个简单的命令行即可在相应的平台上安装Docker,比如VirtualBox. Digital ...