今天和你们分享的是webgl渲染树形结构的流程。用过threejs,babylonjs的同窗都知道,一个大模型都是由n个子模型拼装而成的,那么如何依次渲染子模型,以及渲染每一个子模型在原生webgl中的流程是怎样的呢,我就以osg框架为本来,为同窗们展现出来。html
首先介绍osg框架,该框架是基于openGL的几何引擎框架,目前个人工做是将其翻译成为webgl的几何引擎,在这个过程当中学习webgl原生架构的原理和工程构造方式,踩了各类坑,每次爬出坑都以为本身又强大了一点,呵。node
闲话少叙,切入正题,首先咱们要明确一个渲染流程,那就是webgl究竟是怎么将模型绘制到canvas画布中去的,这就牵扯到我以前的一片文章《原生WebGL场景中绘制多个圆锥圆柱》,连接地址https://www.cnblogs.com/ccentry/p/9864847.html,这篇文章讲述的是用原生webgl向canvas中持续绘制多个模型,但这篇文章的局限性在于,他重复使用了同一组shader(顶点shader,片断shader),而且多个模型也不存在父子关系,这就致使了局部坐标系和全局坐标系的紊乱。今天咱们就来弥补这篇文章的不足之处。web
循序渐进,咱们先讨论的是webgl渲染单个模型的过程,首先咱们构造着色器,请看下面着色器代码编程
attribute vec3 position; attribute vec3 normal; attribute vec4 color; uniform mat4 mvpMatrix; uniform mat4 invMatrix; uniform vec3 lightDirection; uniform vec4 ambientColor; varying vec4 vColor; uniform float lightS; void main(void){ vec3 invLight = normalize(invMatrix * vec4(lightDirection, 0)).xyz; float diffuse = clamp(dot(normal, invLight), 0.0, 1.0) * lightS; vColor = color * vec4(vec3(diffuse), 1.0) + ambientColor; gl_Position = mvpMatrix * vec4(position, 1.0); }
顶点着色器canvas
precision mediump float; varying vec4 vColor; void main(void){ gl_FragColor = vColor; }
片断着色器
好了,接下来咱们的模型数据怎么和着色器进行数据连接呢,很简单,咱们首先建立着色器的gl对象,用以js传参,请看代码架构
/** * 生成着色器的函数 */ function create_shader(id){ // 用来保存着色器的变量 var shader; // 根据id从HTML中获取指定的script标签 var scriptElement = document.getElementById(id); // 若是指定的script标签不存在,则返回 if(!scriptElement){return;} // 判断script标签的type属性 switch(scriptElement.type){ // 顶点着色器的时候 case 'x-shader/x-vertex': shader = gl.createShader(gl.VERTEX_SHADER); break; // 片断着色器的时候 case 'x-shader/x-fragment': shader = gl.createShader(gl.FRAGMENT_SHADER); break; default : return; } // 将标签中的代码分配给生成的着色器 gl.shaderSource(shader, scriptElement.text); // 编译着色器 gl.compileShader(shader); // 判断一下着色器是否编译成功 if(gl.getShaderParameter(shader, gl.COMPILE_STATUS)){ // 编译成功,则返回着色器 return shader; }else{ // 编译失败,弹出错误消息 alert(gl.getShaderInfoLog(shader)); } }
这个函数生成的既能够是顶点着色器,也能够是片断着色器,在此不加赘述,有了着色器的gl对象,咱们就能向着色器里传attribute和uniform参数了吗,显然不行,那么接下来咱们就要构造一个能够向着色器对象传参数的程序对象gl.program,这也是难点之一,先看代码框架
/** * 程序对象的生成和着色器链接的函数 */ function create_program(vs, fs){ // 程序对象的生成 var program = gl.createProgram(); // 向程序对象里分配着色器 gl.attachShader(program, vs); gl.attachShader(program, fs); // 将着色器链接 gl.linkProgram(program); // 判断着色器的链接是否成功 if(gl.getProgramParameter(program, gl.LINK_STATUS)){ // 成功的话,将程序对象设置为有效 gl.useProgram(program); // 返回程序对象 return program; }else{ // 若是失败,弹出错误信息 alert(gl.getProgramInfoLog(program)); } }
咱们看到这个函数作了两件事,第一gl.attachShader将咱们刚刚生成的着色器对象绑定到gl.program编程对象上,第二件事就是gl.useProgram激活绑定好着色器对象的编程对象,固然第二件事有待商榷,那就是若是咱们有多个gl.program是否是每次建立绑定好着色器都要激活,这个要看具体使用场景,再次说明,这里这种绑定当即激活的方式不建议使用,一半都是绑定完成后等到要使用时才激活。为了这个还被lasoy老师批评了,哈哈,再次膜拜lasoy老师,阿里大佬。函数
好了,如今咱们有了gl.program编程对象,就可以安心的向shader里传attribute和uniform参数了,具体传参方法不是咱们这篇文章讨论的重点,请参考个人上一篇博客《原生WebGL场景中绘制多个圆锥圆柱》,连接地址https://www.cnblogs.com/ccentry/p/9864847.html。post
接下来咱们进入正题,持续绘制多个模型进一个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都有本身的树结构,因此我也模仿两者本身构造了个人树,请看下面代码
/** * 坐标系 * */ let Section = require('./Section'); let Operation = require('./Operation'); let Geometry = require('../core/Geometry'); let MatrixTransform = require('../core/MatrixTransform'); let StateSet = require('../core/StateSet'); let StateAttribute = require('../core/StateAttribute'); let BufferArray = require('../core/BufferArray'); let DrawElements = require('../core/DrawElements'); let Primitives = require('../core/Primitives'); let Depth = require('../core/Depth'); let LineWidth = require('../core/LineWidth'); let Material = require('../core/Material'); let BlendFunc = require('../core/BlendFunc'); let Algorithm = require('../util/Algorithm'); let BoundingBox = require('../util/BoundingBox'); let Vec3 = require('../util/Vec3'); let Vec4 = require('../util/Vec4'); let Plane = require('../util/Plane'); let Quat = require('../util/Quat'); let Mat4 = require('../util/Mat4'); let Utils = require('../util/Utils'); let ShaderFactory = require('../render/ShaderFactory'); let PolyhedronGeometry = require('../model/polyhedron'); let Group = require('../core/Group'); let CoordinateSection = function(viewer){ Section.call(this, viewer); //坐标系模型的空间位置和姿态矩阵 this.position = Mat4.new(); this._coordRoot = undefined; this._scale = Vec3.create(1, 1, 1); this._translate = Vec3.new(); this._rotate = Quat.new(); this._scaleMatrix = Mat4.new(); this._translateMatrix = Mat4.new(); this._rotateMatrix = Mat4.new(); }; //继承Section类 CoordinateSection.prototype = Object.create(Section.prototype); CoordinateSection.prototype.constructor = CoordinateSection; CoordinateSection.prototype = { /** * 建立坐标系模型 * root:scene根节点 * */ create : function(root){ //初始化坐标系根节点 this._coordRoot = new MatrixTransform(); //几何 let polyhedronGeo = new PolyhedronGeometry(); //构造单位尺寸的模型 polyhedronGeo.getCone(0.2, 0.5, 16); let geoms = polyhedronGeo.vertices; let array = new Float32Array(geoms); let vertexBuffer = new BufferArray(BufferArray.ARRAY_BUFFER, array, 3); //面索引绘制方式 let indices = []; indices = polyhedronGeo.faces; //几何体类实例 let geom = new Geometry(); geom.setBufferArray('Vertex', vertexBuffer); let index = new Int8Array(indices); let indexBuffer = new BufferArray(BufferArray.ELEMENT_ARRAY_BUFFER, index, index.length); let prim = new DrawElements(Primitives.TRIANGLES, indexBuffer); geom.setPrimitive(prim); //将几何对象加入坐标系根节点 this._coordRoot.addChild(geom); //渲染组件 let stateSet = new StateSet(); //使用ColorDefaultProgram这组着色器 stateSet.addAttribute(ShaderFactory.createColorDefault.call(this)); stateSet.addAttribute(new Material([1, 0.5, 0, 1])); stateSet.addAttribute(new BlendFunc(BlendFunc.SRC_ALPHA, BlendFunc.ONE_MINUS_SRC_ALPHA)); stateSet.addAttribute(new Depth(Depth.LESS, 0.1, 0.9, false));//深度值在中间 this._coordRoot.setStateSet(stateSet); //将坐标系根节点加入场景根节点 root.addChild(this._coordRoot); }, /** * 调整坐标轴尺寸姿态 * boundingBox:scene场景的包围盒 * vec3Translate:场景平移向量 * vec4Rotate:场景旋转四元数 * vec3Scale:场景缩放向量 * mat4Scale:场景缩放矩阵 * mat4Translate:场景平移矩阵 * mat4Rotate:场景旋转矩阵 * worldMatrix:当前场景的世界坐标 * */ update: function (boundingBox, vec3Scale, vec3Translate, vec4Rotate, mat4Scale, mat4Translate, mat4Rotate, worldMatrix) { if(boundingBox instanceof BoundingBox){//先保证boundingBox是BoundingBox类的实例 let vecSRaw = Vec3.new(); Vec3.copy(vecSRaw, vec3Scale);//克隆缩放向量,防止污染场景缩放向量 let vecS = Vec3.new(); this.computeScale(vecS, vecSRaw);//取场景缩放最长边的1/4做为坐标系模型缩放比例 let vecT = Vec3.new(); Vec3.copy(vecT, vec3Translate);//克隆平移向量,防止污染场景平移向量 let vecR = Vec4.new(); Vec4.copy(vecR, vec4Rotate);//克隆旋转向量,防止污染场景旋转向量 if (boundingBox.valid()) {//场景模型存在的话 let min = boundingBox.getMin(); let max = boundingBox.getMax(); boundingBox.getCenter(vec3Translate); Vec3.sub(this._scale, max, min); } let matW = Mat4.new(); Mat4.copy(matW, worldMatrix); //克隆一个世界坐标系矩阵,防止修改场景包围盒的矩阵 let matS = Mat4.new(); Mat4.copy(matS, mat4Scale); //克隆一个缩放矩阵,防止污染场景包围盒的缩放矩阵 let matT = Mat4.new(); Mat4.copy(matT, mat4Translate); //克隆一个平移矩阵,防止污染场景包围盒的平移矩阵 let matR = Mat4.new(); Mat4.copy(matR, mat4Rotate); //克隆一个旋转矩阵,防止污染场景包围盒的旋转矩阵 Mat4.fromScaling(matS, vecS); Mat4.fromTranslation(matT, vecT); Mat4.fromQuat(matR, vecR); Mat4.mul(matW, matT, matR); Mat4.mul(matW, matW, matS); this._coordRoot._matrix = matW; } }, //计算坐标系缩放比例 computeScale : function(newScale, boundingBoxScale){ //取场景模型包围盒最长一边的1/4 var scale = boundingBoxScale[0] > boundingBoxScale[1] ? boundingBoxScale[0] : boundingBoxScale[1]; scale = scale > boundingBoxScale[2] ? scale : boundingBoxScale[2]; scale *= 1/4; newScale[0] = scale; newScale[1] = scale; newScale[2] = scale; } }; module.exports = CoordinateSection;
在这个构造类中,我将坐标系模型作成了一个根节点coordRoot,这个根节点下挂载了一个子模型(圆锥),该子模型下又挂载了三个子节点,1、geometry几何特征;2、transformMatrix千万注意是相对于他的父节点的空间变换矩阵,不是相对于世界坐标系的空间变换矩阵,千万注意;3、stateSet着色器相关对象,就是实现shader,program,传参attribute,uniform,空间变换,drawElement相关的配置和操做对象。这样作的好处就显而易见了,遍历整棵模型树,我既能将树上每个节点都绑定不一样的shader绘制出来,又能知道子节点相对于父节点的空间变换矩阵,就不会出现刚才那种错位的事了。 同窗们看到这里应该明白树形结构加载多个子模型的好处了,因为此次的代码并不完整,osg也须要nodejs的运行环境,因此事先说明,贴出的代码只是为了帮助说明观点,本文代码只是局部关键部位,并不能运行,若有问题,能够交流。引用本文请注明出处https://www.cnblogs.com/ccentry/p/9903166.html