1 /**
  2  * Hilo
  3  * Copyright 2015 alibaba.com
  4  * Licensed under the MIT License
  5  */
  6 
  7 /**
  8  * Heavily inspired by PIXI's SpriteRenderer:
  9  * https://github.com/pixijs/pixi.js/blob/v3.0.9/src/core/sprites/webgl/SpriteRenderer.js
 10  */
 11 
 12 var DEG2RAD = Math.PI / 180;
 13 /**
 14  * @class webgl画布渲染器。所有可视对象将渲染在canvas画布上。
 15  * @augments Renderer
 16  * @param {Object} properties 创建对象的属性参数。可包含此类所有可写属性。
 17  * @module hilo/renderer/WebGLRenderer
 18  * @requires hilo/core/Class
 19  * @requires hilo/core/Hilo
 20  * @requires hilo/renderer/Renderer
 21  * @requires  hilo/geom/Matrix
 22  * @property {WebGLRenderingContext} gl webgl上下文。只读属性。
 23  */
 24 var WebGLRenderer = Class.create(/** @lends WebGLRenderer.prototype */{
 25     Extends: Renderer,
 26     Statics:/** @lends WebGLRenderer */{
 27         /**
 28          * 最大批渲染数量。
 29          * @type {Number}
 30          */
 31         MAX_BATCH_NUM:2000,
 32         /**
 33          * 顶点属性数。只读属性。
 34          * @type {Number}
 35          */
 36         ATTRIBUTE_NUM:5,
 37         /**
 38          * 是否支持WebGL。只读属性。
 39          * @type {Boolean}
 40          */
 41         isSupport:function(){
 42             if(this._isSupported == undefined){
 43                 var canvas = document.createElement('canvas');
 44                 if(canvas.getContext && (canvas.getContext('webgl')||canvas.getContext('experimental-webgl'))){
 45                     this._isSupported = true;
 46                 }
 47                 else{
 48                     this._isSupported = false;
 49                 }
 50             }
 51             return this._isSupported;
 52         }
 53     },
 54     renderType:'webgl',
 55     gl:null,
 56     _isContextLost:false,
 57     _cacheTexture:{},
 58     constructor: function(properties){
 59         WebGLRenderer.superclass.constructor.call(this, properties);
 60         var that = this;
 61         this.gl = this.canvas.getContext("webgl")||this.canvas.getContext('experimental-webgl');
 62 
 63         this.maxBatchNum = WebGLRenderer.MAX_BATCH_NUM;
 64         this.positionStride = WebGLRenderer.ATTRIBUTE_NUM * 4;
 65         var vertexNum = this.maxBatchNum * WebGLRenderer.ATTRIBUTE_NUM * 4;
 66         var indexNum = this.maxBatchNum * 6;
 67         this.arrayBuffer = new ArrayBuffer(vertexNum * 4);
 68         this.float32Array = new Float32Array(this.arrayBuffer);
 69         this.uint32Array = new Uint32Array(this.arrayBuffer);
 70         this.indexs = new Uint16Array(indexNum);
 71         for (var i=0, j=0; i < indexNum; i += 6, j += 4)
 72         {
 73             this.indexs[i + 0] = j + 0;
 74             this.indexs[i + 1] = j + 1;
 75             this.indexs[i + 2] = j + 2;
 76             this.indexs[i + 3] = j + 1;
 77             this.indexs[i + 4] = j + 2;
 78             this.indexs[i + 5] = j + 3;
 79         }
 80         this.batchIndex = 0;
 81         this.sprites = [];
 82 
 83         this.canvas.addEventListener('webglcontextlost', function(e){
 84             that._isContextLost = true;
 85             e.preventDefault();
 86         }, false);
 87 
 88         this.canvas.addEventListener('webglcontextrestored', function(e){
 89             that._isContextLost = false;
 90             that.setupWebGLStateAndResource();
 91         }, false);
 92 
 93         this.setupWebGLStateAndResource();
 94     },
 95     setupWebGLStateAndResource:function(){
 96         var gl = this.gl;
 97         gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
 98         gl.clearColor(0, 0, 0, 0);
 99         gl.disable(gl.DEPTH_TEST);
100         gl.disable(gl.CULL_FACE);
101         gl.enable(gl.BLEND);
102 
103         this._cacheTexture = {};
104         this._initShaders();
105         this.defaultShader.active();
106 
107         this.positionBuffer = gl.createBuffer();
108         this.indexBuffer = gl.createBuffer();
109 
110         gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
111         gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, this.indexs, gl.STATIC_DRAW);
112 
113         gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
114         gl.bufferData(gl.ARRAY_BUFFER, this.arrayBuffer, gl.DYNAMIC_DRAW);
115 
116         gl.vertexAttribPointer(this.a_position, 2, gl.FLOAT, false, this.positionStride, 0);//x, y
117         gl.vertexAttribPointer(this.a_TexCoord, 2, gl.FLOAT, false, this.positionStride, 2 * 4);//x, y
118         gl.vertexAttribPointer(this.a_tint, 4, gl.UNSIGNED_BYTE, true, this.positionStride, 4 * 4);//alpha
119     },
120 
121     context: null,
122 
123     /**
124      * @private
125      * @see Renderer#startDraw
126      */
127     startDraw: function(target){
128         if(target.visible && target.alpha > 0){
129             if(target === this.stage){
130                 this.clear();
131             }
132             return true;
133         }
134         return false;
135     },
136     /**
137      * @private
138      * @see Renderer#draw
139      */
140     draw: function(target){
141         var w = target.width,
142             h = target.height;
143 
144         //TODO:draw background
145         var bg = target.background; // jshint ignore:line
146 
147         //draw image
148         var drawable = target.drawable, image = drawable && drawable.image;
149         if(image){
150             var rect = drawable.rect, sw = rect[2], sh = rect[3];
151             if(!w && !h){
152                 //fix width/height TODO: how to get rid of this?
153                 w = target.width = sw;
154                 h = target.height = sh;
155             }
156 
157             if(this.batchIndex >= this.maxBatchNum){
158                 this._renderBatches();
159             }
160 
161             var vertexs = this._createVertexs(image, rect[0], rect[1], sw, sh, 0, 0, w, h);
162             var index = this.batchIndex * this.positionStride;
163             var float32Array = this.float32Array;
164             var uint32Array = this.uint32Array;
165 
166             var tint = (target.tint >> 16) + (target.tint & 0xff00) + ((target.tint & 0xff) << 16) + (target.__webglRenderAlpha * 255 << 24);
167 
168             float32Array[index + 0] = vertexs[0];//x
169             float32Array[index + 1] = vertexs[1];//y
170             float32Array[index + 2] = vertexs[2];//uvx
171             float32Array[index + 3] = vertexs[3];//uvy
172             uint32Array[index + 4] = tint;//tint
173 
174             float32Array[index + 5] = vertexs[4];
175             float32Array[index + 6] = vertexs[5];
176             float32Array[index + 7] = vertexs[6];
177             float32Array[index + 8] = vertexs[7];
178             uint32Array[index + 9] = tint;
179 
180             float32Array[index + 10] = vertexs[8];
181             float32Array[index + 11] = vertexs[9];
182             float32Array[index + 12] = vertexs[10];
183             float32Array[index + 13] = vertexs[11];
184             uint32Array[index + 14] = tint;
185 
186             float32Array[index + 15] = vertexs[12];
187             float32Array[index + 16] = vertexs[13];
188             float32Array[index + 17] = vertexs[14];
189             float32Array[index + 18] = vertexs[15];
190             uint32Array[index + 19] = tint;
191 
192             var matrix = target.__webglWorldMatrix;
193             for(var i = 0;i < 4;i ++){
194                 var x = float32Array[index + i*5];
195                 var y = float32Array[index + i*5 + 1];
196 
197                 float32Array[index + i*5] = matrix.a*x + matrix.c*y + matrix.tx;
198                 float32Array[index + i*5 + 1] = matrix.b*x + matrix.d*y + matrix.ty;
199             }
200 
201             target.__textureImage = image;
202             this.sprites[this.batchIndex++] = target;
203         }
204     },
205 
206     /**
207      * @private
208      * @see Renderer#endDraw
209      */
210     endDraw: function(target){
211         if(target === this.stage){
212             this._renderBatches();
213         }
214     },
215     /**
216      * @private
217      * @see Renderer#transform
218      */
219     transform: function(target){
220         var drawable = target.drawable;
221         if(drawable && drawable.domElement){
222             Hilo.setElementStyleByView(target);
223             return;
224         }
225 
226         var scaleX = target.scaleX,
227             scaleY = target.scaleY;
228 
229         if(target === this.stage){
230             var style = this.canvas.style,
231                 oldScaleX = target._scaleX,
232                 oldScaleY = target._scaleY,
233                 isStyleChange = false;
234 
235             if((!oldScaleX && scaleX != 1) || (oldScaleX && oldScaleX != scaleX)){
236                 target._scaleX = scaleX;
237                 style.width = scaleX * target.width + "px";
238                 isStyleChange = true;
239             }
240             if((!oldScaleY && scaleY != 1) || (oldScaleY && oldScaleY != scaleY)){
241                 target._scaleY = scaleY;
242                 style.height = scaleY * target.height + "px";
243                 isStyleChange = true;
244             }
245             if(isStyleChange){
246                 target.updateViewport();
247             }
248             target.__webglWorldMatrix = target.__webglWorldMatrix||new Matrix(1, 0, 0, 1, 0, 0);
249         }
250         else if(target.parent){
251             target.__webglWorldMatrix = target.__webglWorldMatrix||new Matrix(1, 0, 0, 1, 0, 0);
252             this._setConcatenatedMatrix(target, target.parent);
253         }
254 
255         if(target.alpha > 0) {
256             if(target.parent && target.parent.__webglRenderAlpha){
257                 target.__webglRenderAlpha = target.alpha * target.parent.__webglRenderAlpha;
258             }
259             else{
260                 target.__webglRenderAlpha = target.alpha;
261             }
262         }
263     },
264 
265     /**
266      * @private
267      * @see Renderer#remove
268      */
269     remove: function(target){
270         var drawable = target.drawable;
271         var elem = drawable && drawable.domElement;
272 
273         if(elem){
274             var parentElem = elem.parentNode;
275             if(parentElem){
276                 parentElem.removeChild(elem);
277             }
278         }
279     },
280 
281     /**
282      * @private
283      * @see Renderer#clear
284      */
285     clear: function(x, y, width, height){
286         this.gl.clear(this.gl.COLOR_BUFFER_BIT);
287     },
288 
289     /**
290      * @private
291      * @see Renderer#resize
292      */
293     resize: function(width, height){
294         if(this.width !== width || this.height !== height){
295             var canvas = this.canvas;
296             var stage = this.stage;
297             var style = canvas.style;
298 
299             this.width = canvas.width = width;
300             this.height = canvas.height = height;
301 
302             style.width = stage.width * stage.scaleX + 'px';
303             style.height = stage.height * stage.scaleY + 'px';
304 
305             this.gl.viewport(0, 0, width, height);
306 
307             this.canvasHalfWidth = width * .5;
308             this.canvasHalfHeight = height * .5;
309 
310             this._uploadProjectionTransform(true);
311         }
312     },
313     _renderBatches:function(){
314         var gl = this.gl;
315         gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.uint32Array.subarray(0, this.batchIndex * this.positionStride));
316         var startIndex = 0;
317         var batchNum = 0;
318         var preTextureImage = null;
319         for(var i = 0;i < this.batchIndex;i ++){
320             var sprite = this.sprites[i];
321             if(preTextureImage && preTextureImage !== sprite.__textureImage){
322                 this._renderBatch(startIndex, i);
323                 startIndex = i;
324                 batchNum = 1;
325             }
326             preTextureImage = sprite.__textureImage;
327         }
328         this._renderBatch(startIndex, this.batchIndex);
329         this.batchIndex = 0;
330     },
331     _renderBatch:function(start, end){
332         var gl = this.gl;
333         var num = end - start;
334         if(num > 0){
335             gl.bindTexture(gl.TEXTURE_2D, this._getTexture(this.sprites[start]));
336             gl.drawElements(gl.TRIANGLES, num * 6, gl.UNSIGNED_SHORT, start * 6 * 2);
337         }
338     },
339     _uploadProjectionTransform:function(force){
340         if(!this._projectionTransformElements||force){
341             this._projectionTransformElements = new Float32Array([
342                 1/this.canvasHalfWidth, 0, 0,
343                 0, -1/this.canvasHalfHeight, 0,
344                 -1, 1, 1,
345             ]);
346         }
347 
348         this.gl.uniformMatrix3fv(this.u_projectionTransform, false, this._projectionTransformElements);
349     },
350     _initShaders:function(){
351         var VSHADER_SOURCE ='\
352             attribute vec2 a_position;\n\
353             attribute vec2 a_TexCoord;\n\
354             attribute vec4 a_tint;\n\
355             uniform mat3 u_projectionTransform;\n\
356             varying vec2 v_TexCoord;\n\
357             varying vec4 v_tint;\n\
358             void main(){\n\
359                 gl_Position =  vec4((u_projectionTransform * vec3(a_position, 1.0)).xy, 1.0, 1.0);\n\
360                 v_TexCoord = a_TexCoord;\n\
361                 v_tint = vec4(a_tint.rgb * a_tint.a, a_tint.a);\n\
362             }\n\
363         ';
364 
365         var FSHADER_SOURCE = '\n\
366             precision mediump float;\n\
367             uniform sampler2D u_Sampler;\n\
368             varying vec2 v_TexCoord;\n\
369             varying vec4 v_tint;\n\
370             void main(){\n\
371                 gl_FragColor = texture2D(u_Sampler, v_TexCoord) * v_tint;\n\
372             }\n\
373         ';
374 
375         this.defaultShader = new Shader(this, {
376             v:VSHADER_SOURCE,
377             f:FSHADER_SOURCE
378         },{
379             attributes:["a_position", "a_TexCoord", "a_tint"],
380             uniforms:["u_projectionTransform", "u_Sampler"]
381         });
382     },
383     _createVertexs:function(img, tx, ty, tw, th, x, y, w, h){
384         var tempVertexs = this.__tempVertexs||[];
385         var width = img.width;
386         var height = img.height;
387 
388         tw = tw/width;
389         th = th/height;
390         tx = tx/width;
391         ty = ty/height;
392 
393         w = w;
394         h = h;
395         x = x;
396         y = y;
397 
398         if(tw + tx > 1){
399             tw = 1 - tx;
400         }
401 
402         if(th + ty > 1){
403             th = 1 - ty;
404         }
405 
406         var index = 0;
407         tempVertexs[index++] = x; tempVertexs[index++] = y; tempVertexs[index++] = tx; tempVertexs[index++] = ty;
408         tempVertexs[index++] = x+w;tempVertexs[index++] = y; tempVertexs[index++] = tx+tw; tempVertexs[index++] = ty;
409         tempVertexs[index++] = x; tempVertexs[index++] = y+h; tempVertexs[index++] = tx;tempVertexs[index++] = ty+th;
410         tempVertexs[index++] = x+w;tempVertexs[index++] = y+h;tempVertexs[index++] = tx+tw;tempVertexs[index++] = ty+th;
411 
412         return tempVertexs;
413     },
414     _setConcatenatedMatrix:function(view, ancestor){
415         var mtx = view.__webglWorldMatrix;
416         var cos = 1, sin = 0,
417             rotation = view.rotation % 360,
418             pivotX = view.pivotX, pivotY = view.pivotY,
419             scaleX = view.scaleX, scaleY = view.scaleY;
420 
421         if(rotation){
422             var r = rotation * DEG2RAD;
423             cos = Math.cos(r);
424             sin = Math.sin(r);
425         }
426 
427         var pos = view.getAlignPosition();
428         mtx.a = cos*scaleX;
429         mtx.b = sin*scaleX;
430         mtx.c = -sin*scaleY;
431         mtx.d = cos*scaleY;
432         mtx.tx =  pos.x - mtx.a * pivotX - mtx.c * pivotY;
433         mtx.ty =  pos.y - mtx.b * pivotX - mtx.d * pivotY;
434 
435         mtx.concat(ancestor.__webglWorldMatrix);
436     },
437     _getTexture:function(sprite){
438         var image = sprite.__textureImage;
439         var texture = this._cacheTexture[image.src];
440         if(!texture){
441             texture = this.activeShader.uploadTexture(image);
442         }
443         return texture;
444     }
445 });
446 
447 /**
448  * shader
449  * @param {WebGLRenderer} renderer [description]
450  * @param {Object} source
451  * @param {String} source.v 顶点shader
452  * @param {String} source.f 片段shader
453  * @param {Object} attr
454  * @param {Array} attr.attributes attribute数组
455  * @param {Array} attr.uniforms uniform数组
456  */
457 var Shader = function(renderer, source, attr){
458     this.renderer = renderer;
459     this.gl = renderer.gl;
460     this.program = this._createProgram(this.gl, source.v, source.f);
461 
462     attr = attr||{};
463     this.attributes = attr.attributes||[];
464     this.uniforms = attr.uniforms||[];
465 };
466 
467 Shader.prototype = {
468     active:function(){
469         var that = this;
470         var renderer = that.renderer;
471         var gl = that.gl;
472         var program = that.program;
473 
474         if(program && gl){
475             renderer.activeShader = that;
476             gl.useProgram(program);
477             that.attributes.forEach(function(attribute){
478                 renderer[attribute] = gl.getAttribLocation(program, attribute);
479                 gl.enableVertexAttribArray(renderer[attribute]);
480             });
481 
482             that.uniforms.forEach(function(uniform){
483                 renderer[uniform] = gl.getUniformLocation(program, uniform);
484             });
485 
486             if(that.width !== renderer.width || that.height !== renderer.height){
487                 that.width = renderer.width;
488                 that.height = renderer.height;
489                 renderer._uploadProjectionTransform();
490             }
491         }
492     },
493     uploadTexture:function(image){
494         var gl = this.gl;
495         var renderer = this.renderer;
496         var texture = gl.createTexture();
497         var u_Sampler = renderer.u_Sampler;
498 
499         gl.activeTexture(gl.TEXTURE0);
500         gl.bindTexture(gl.TEXTURE_2D, texture);
501 
502         // gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
503         gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 1);
504         gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
505 
506         gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
507         gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
508         gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
509         gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
510         gl.uniform1i(u_Sampler, 0);
511         gl.bindTexture(gl.TEXTURE_2D, null);
512 
513         this.renderer._cacheTexture[image.src] = texture;
514         return texture;
515     },
516     _createProgram:function(gl, vshader, fshader){
517         var vertexShader = this._createShader(gl, gl.VERTEX_SHADER, vshader);
518         var fragmentShader = this._createShader(gl, gl.FRAGMENT_SHADER, fshader);
519         if (!vertexShader || !fragmentShader) {
520             return null;
521         }
522 
523         var program = gl.createProgram();
524         if (program) {
525             gl.attachShader(program, vertexShader);
526             gl.attachShader(program, fragmentShader);
527 
528             gl.linkProgram(program);
529 
530             gl.deleteShader(fragmentShader);
531             gl.deleteShader(vertexShader);
532             var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
533             if (!linked) {
534                 var error = gl.getProgramInfoLog(program);
535                 console.log('Failed to link program: ' + error);
536                 gl.deleteProgram(program);
537                 return null;
538             }
539         }
540         return program;
541     },
542     _createShader:function(gl, type, source){
543         var shader = gl.createShader(type);
544         if(shader){
545             gl.shaderSource(shader, source);
546             gl.compileShader(shader);
547 
548             var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
549             if (!compiled) {
550                 var error = gl.getShaderInfoLog(shader);
551                 console.log('Failed to compile shader: ' + error);
552                 gl.deleteShader(shader);
553                 return null;
554             }
555         }
556         return shader;
557     }
558 };