Update BUGS for 1.1 and upcoming 1.2.
[pwl6.git] / src / yuu / gfx.js
1 /* Copyright 2014 Yukkuri Games
2 Licensed under the terms of the GNU GPL v2 or later
3 @license http://www.gnu.org/licenses/gpl-2.0.html
4 @source: http://yukkurigames.com/yuu/
5 */
6
7 (function (yuu) {
8 "use strict";
9
10 var yT = this.yT || require("./yT");
11 var yf = this.yf || require("./yf");
12 var gl;
13 var canvas;
14
15 var dpr = yuu.DPR = this.devicePixelRatio || 1;
16
17 yT.defineProperty(Int8Array.prototype, "GL_TYPE", 0x1400);
18 yT.defineProperty(Uint8Array.prototype, "GL_TYPE", 0x1401);
19 yT.defineProperty(Int16Array.prototype, "GL_TYPE", 0x1402);
20 yT.defineProperty(Uint16Array.prototype, "GL_TYPE", 0x1403);
21 yT.defineProperty(Int32Array.prototype, "GL_TYPE", 0x1404);
22 yT.defineProperty(Uint32Array.prototype, "GL_TYPE", 0x1405);
23 yT.defineProperty(Float32Array.prototype, "GL_TYPE", 0x1406);
24 /** Patch the WebGL type onto arrays for data-driven access later
25
26 Values from https://www.khronos.org/registry/webgl/specs/1.0/.
27
28 See also notes in pre on Safari's typed array problems.
29 */
30
31 yuu.uniform = function (location, value) {
32 /** Set a uniform in the active program
33
34 The type of the uniform is automatically determined from
35 the value:
36
37 * Typed integer arrays of length 1-4 call uniform[1-4]iv
38 * Other sequences of length 1-4 call uniform[1-4]fv
39 * Sequences of length 9 or 16 call uniformMatrix[3-4]fv
40 * Non-sequences call uniform1fv (even if the parameter
41 is a valid integer)
42 * Sequences of other lengths throw a TypeError
43
44 It is not possible to call uniformMatrix2fv via this
45 function.
46 */
47 switch (value.constructor) {
48 case Int8Array:
49 case Uint8Array:
50 case Int16Array:
51 case Uint16Array:
52 case Int32Array:
53 case Uint32Array:
54 switch (value.length) {
55 case 1: return gl.uniform1iv(location, value);
56 case 2: return gl.uniform2iv(location, value);
57 case 3: return gl.uniform3iv(location, value);
58 case 4: return gl.uniform4iv(location, value);
59 default: throw new TypeError("unexpected array length");
60 }
61 break;
62 default:
63 switch (value.length) {
64 case 1: return gl.uniform1fv(location, value);
65 case 2: return gl.uniform2fv(location, value);
66 case 3: return gl.uniform3fv(location, value);
67 case 4: return gl.uniform4fv(location, value);
68 case 9: return gl.uniformMatrix3fv(location, false, value);
69 case 16: return gl.uniformMatrix4fv(location, false, value);
70 case undefined: return gl.uniform1f(location, value);
71 default: throw new TypeError("unexpected array length");
72 }
73 }
74 };
75
76 function isShaderSource (src) {
77 return src.indexOf("\n") >= 0 || yf.last(src.trim()) === ";";
78 }
79
80 var FRAGMENT_SHADER = 0x8B30;
81 var VERTEX_SHADER = 0x8B31;
82 var EXTS = {};
83 EXTS[FRAGMENT_SHADER] = "frag";
84 EXTS[VERTEX_SHADER] = "vert";
85
86 function compile (type, srcs) {
87 function getSource (src) {
88 return isShaderSource(src)
89 ? Promise.resolve(src)
90 : yuu.GET(yuu.resourcePath(src, "shaders", EXTS[type]));
91 }
92 return Promise.all(yf.map(getSource, srcs))
93 .then(function (srcs) {
94 var src = srcs.join("\n");
95 var shader = gl.createShader(type);
96 gl.shaderSource(shader, src);
97 gl.compileShader(shader);
98 if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
99 var log = gl.getShaderInfoLog(shader);
100 throw new Error(
101 "Shader compile error:\n\n" + src + "\n\n" + log);
102 }
103 return shader;
104 });
105 }
106
107 yuu.ShaderProgram = yT({
108 constructor: function (vs, fs) {
109 /** A linked program of vertex and fragment shaders
110
111 vs and fs are arrays of vertex and fragment shader source
112 code or URLs.
113 */
114 fs = fs || ["yuu/@default"];
115 vs = vs || ["yuu/@default"];
116 var id = this.id = gl.createProgram();
117 var attribs = this.attribs = {};
118 var uniforms = this.uniforms = {};
119 this.ready = Promise.all([compile(VERTEX_SHADER, vs),
120 compile(FRAGMENT_SHADER, fs)])
121 .then(function (shaders) {
122 yf.each(gl.attachShader.bind(gl, id), shaders);
123 gl.linkProgram(id);
124 if (!gl.getProgramParameter(id, gl.LINK_STATUS))
125 throw new Error("Shader link error: "
126 + gl.getProgramInfoLog(id));
127 return id;
128 }).catch(function (exc) {
129 yuu.showError(exc);
130 this.id = yuu.ShaderProgram.DEFAULT.id;
131 this.attribs = yuu.ShaderProgram.DEFAULT.attribs;
132 this.uniforms = yuu.ShaderProgram.DEFAULT.uniforms;
133 throw exc;
134 }.bind(this)).then(function (id) {
135 this.id = id;
136 yf.irange(function (i) {
137 var name = gl.getActiveAttrib(id, i).name;
138 attribs[name] = gl.getAttribLocation(id, name);
139 }, gl.getProgramParameter(id, gl.ACTIVE_ATTRIBUTES));
140 yf.irange(function (i) {
141 var name = gl.getActiveUniform(id, i).name;
142 uniforms[name] = gl.getUniformLocation(id, name);
143 }, gl.getProgramParameter(id, gl.ACTIVE_UNIFORMS));
144 return this;
145 }.bind(this));
146 },
147
148 setUniforms: function () {
149 /** Set the values of program uniforms
150
151 The arguments are any number of objects mapping
152 uniform names to values (floats, vec3s, etc.).
153 */
154 for (var i = 0; i < arguments.length; ++i)
155 for (var name in arguments[i])
156 yuu.uniform(this.uniforms[name], arguments[i][name]);
157 },
158
159 setAttribPointers: function (buffer) {
160 /** Bind the contents of a vertex buffer to attributes
161
162 `buffer` is (or is like) a yuu.VertexBuffer instance.
163 */
164 for (var name in this.attribs)
165 gl.vertexAttribPointer(
166 this.attribs[name],
167 buffer.spec.attribs[name].elements,
168 buffer.spec.attribs[name].View.prototype.GL_TYPE,
169 false, 0, buffer.arrays[name].byteOffset);
170 }
171 });
172
173 // This function is easier to read than a giant lookup table
174 // ({ textureWrapS: "TEXTURE_WRAP_S", ... x100 }) but slower.
175 function glEnum (gl, name) {
176 return gl[name.replace(/([A-Z]+)/g, "_$1").toUpperCase()];
177 }
178
179 function glScopedEnum (scope, gl, name) {
180 var value = glEnum(gl, scope + "_" + name);
181 if (value === undefined)
182 value = glEnum(gl, name);
183 return value;
184 }
185
186 var glTextureEnum = glScopedEnum.bind(null, "texture");
187
188 yuu.Texture = yuu.Caching(yT({
189 constructor: function (path, overrideOptions) {
190 /** A 2D texture
191
192 The texture is set to a 1x1 white texture until it is
193 loaded (or if loading fails).
194 */
195 var options = {};
196 yf.ipairs(function (k, v) {
197 options[glTextureEnum(gl, k)] = glEnum(gl, v);
198 }, TEXTURE_DEFAULTS);
199 yf.ipairs(function (k, v) {
200 options[glTextureEnum(gl, k)] = glEnum(gl, v);
201 }, overrideOptions || {});
202
203 if (!path) {
204 var data = new Uint8Array([255, 255, 255, 255]);
205 this.id = gl.createTexture();
206 this.width = this.height = 1;
207 this.src = "default / fallback 1x1 white texture";
208 gl.bindTexture(gl.TEXTURE_2D, this.id);
209 gl.texImage2D(
210 gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0,
211 gl.RGBA, gl.UNSIGNED_BYTE, data);
212 gl.texParameteri(
213 gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
214 gl.texParameteri(
215 gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
216 gl.bindTexture(gl.TEXTURE_2D, null);
217 this.ready = Promise.resolve(this);
218 return;
219 }
220
221 path = yuu.resourcePath(path, "images", "png");
222 this.id = yuu.Texture.DEFAULT.id;
223 this.src = path;
224 this.width = yuu.Texture.DEFAULT.width;
225 this.height = yuu.Texture.DEFAULT.height;
226
227 this.ready = yuu.Image(path).then(function (img) {
228 var id = gl.createTexture();
229 gl.bindTexture(gl.TEXTURE_2D, id);
230 gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
231 gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
232 for (var opt in options)
233 gl.texParameteri(gl.TEXTURE_2D, opt, options[opt]);
234 gl.texImage2D(
235 gl.TEXTURE_2D, 0, gl.RGBA,
236 gl.RGBA, gl.UNSIGNED_BYTE, img);
237 gl.bindTexture(gl.TEXTURE_2D, null);
238 this.id = id;
239 this.width = img.width;
240 this.height = img.height;
241 this.src = img.src;
242 return this;
243 }.bind(this)).catch(function (e) {
244 this.src = "Error loading " + path + ": " + e;
245 yuu.log("errors", this.src);
246 gl.bindTexture(gl.TEXTURE_2D, null);
247 throw e;
248 }.bind(this));
249 }
250 }));
251
252 var TEXTURE_DEFAULTS = yuu.Texture.DEFAULTS = {
253 magFilter: "linear",
254 minFilter: "linear",
255 wrapS: "clampToEdge",
256 wrapT: "clampToEdge"
257 };
258
259 yuu.Material = yuu.Caching(yT({
260 constructor: function (texture, program, uniforms) {
261 /** A material is a combination of a texture and shader program */
262 if (yf.isString(texture))
263 texture = new yuu.Texture(texture);
264 this.texture = texture || yuu.Texture.DEFAULT;
265 this.program = program || yuu.ShaderProgram.DEFAULT;
266 this.ready = yuu.ready([this.texture, this.program], this);
267 this.uniforms = uniforms || {};
268 },
269
270 enable: function (uniforms) {
271 /** Enable this material and its default parameters */
272 gl.bindTexture(gl.TEXTURE_2D, this.texture.id);
273 gl.useProgram(this.program.id);
274 for (var attrib in this.program.attribs)
275 gl.enableVertexAttribArray(this.program.attribs[attrib]);
276 this.program.setUniforms(this.uniforms, uniforms);
277 },
278
279 disable: function () {
280 /** Disable this material */
281 gl.bindTexture(gl.TEXTURE_2D, null);
282 gl.useProgram(null);
283 for (var attrib in this.program.attribs)
284 gl.disableVertexAttribArray(this.program.attribs[attrib]);
285 }
286 }));
287
288 yuu.VertexAttribSpec = function (spec) {
289 /** Ordering and types for vertex buffer layout
290
291 Interleaved vertices (e.g. VTCVTCVTC) are not currently
292 supported, as ArrayBufferViews are not able to manage a buffer
293 with this kind of layout.
294 */
295 var byteOffset = 0;
296 this.attribs = {};
297 spec.forEach(function (a) {
298 var name = a.name;
299 var elements = a.elements;
300 var View = a.View || Float32Array;
301 this.attribs[name] = { elements: elements,
302 byteOffset: byteOffset,
303 View: View };
304 byteOffset += elements * View.BYTES_PER_ELEMENT;
305 }, this);
306 this.bytesPerVertex = byteOffset;
307 };
308
309 yuu.V3T2C4_F = new yuu.VertexAttribSpec([
310 /** vec3 position; vec2 texCoord; vec4 color; */
311 { name: "position", elements: 3 },
312 { name: "texCoord", elements: 2 },
313 { name: "color", elements: 4 }
314 ]);
315
316 yuu.IndexBuffer = yT({
317 constructor: function (maxIndex, length) {
318 this._capacity = -1;
319 this._maxIndex = maxIndex;
320 this.buffer = null;
321 this.type = null;
322 this.length = length;
323 this._glBuffer = gl.createBuffer();
324 this.dirty = true;
325 },
326
327 bindBuffer: function () {
328 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._glBuffer);
329 if (this.dirty) {
330 this.dirty = false;
331 gl.bufferData(
332 gl.ELEMENT_ARRAY_BUFFER, this.buffer, gl.DYNAMIC_DRAW);
333 }
334 },
335
336 GL_TYPE: { alias: "buffer.GL_TYPE" },
337
338 maxIndex: {
339 get: function () { return this._maxIndex; },
340 set: function (maxIndex) {
341 var Array = yuu.IndexBuffer.Array(maxIndex);
342 if (maxIndex > this._maxIndex
343 && Array !== this.buffer.constructor) {
344 var buffer = new Array(this._capacity);
345 if (this.buffer)
346 buffer.set(this.buffer);
347 this.buffer = buffer;
348 this.dirty = true;
349 }
350 this._maxIndex = maxIndex;
351 }
352 },
353
354 length: {
355 get: function () { return this._length; },
356 set: function (count) {
357 if (count > this._capacity) {
358 var Array = yuu.IndexBuffer.Array(this._maxIndex);
359 var buffer = new Array(count);
360 if (this.buffer)
361 buffer.set(this.buffer);
362 this.buffer = buffer;
363 this._capacity = count;
364 this.dirty = true;
365 }
366 this._length = count;
367 }
368 }
369 });
370
371 yuu.IndexBuffer.Array = function (maxIndex) {
372 if (maxIndex < 0 || maxIndex >= (256 * 256 * 256 * 256))
373 throw new Error("invalid maxIndex index: " + maxIndex);
374 else if (maxIndex < (1 << 8))
375 return Uint8Array;
376 else if (maxIndex < (1 << 16))
377 return Uint16Array;
378 else
379 return Uint32Array;
380 };
381
382 yuu.VertexBuffer = yT({
383 constructor: function (spec, vertexCount) {
384 /** A buffer with a specified vertex format and vertex count
385
386 The individual vertex attribute array views from the
387 attribute specification are available via the .arrays
388 property, e.g. v.arrays.position. The underlying
389 buffer is available as v.buffer.
390
391 The vertex count may be changed after creation and the
392 buffer size and views will be adjusted. If you've
393 grown the buffer, you will need to refill all its
394 data. Shrinking it will truncate it.
395 */
396 this.spec = spec;
397 this._vertexCapacity = -1;
398 this.buffer = null;
399 this.arrays = {};
400 this.vertexCount = vertexCount;
401 this._glBuffer = gl.createBuffer();
402 this.dirty = true;
403 },
404
405 bindBuffer: function () {
406 gl.bindBuffer(gl.ARRAY_BUFFER, this._glBuffer);
407 if (this.dirty) {
408 this.dirty = false;
409 gl.bufferData(gl.ARRAY_BUFFER, this.buffer, gl.DYNAMIC_DRAW);
410 }
411 },
412
413 subdata: function (begin, length) {
414 return new yuu.VertexBuffer.SubData(this, begin, begin + length);
415 },
416
417 vertexCount: {
418 get: function () { return this._vertexCount; },
419 set: function (count) {
420 if (count > this._vertexCapacity) {
421 var buffer = new ArrayBuffer(
422 this.spec.bytesPerVertex * count);
423 var arrays = {};
424 yf.ipairs.call(this, function (name, attrib) {
425 arrays[name] = new attrib.View(
426 buffer, attrib.byteOffset * count,
427 attrib.elements * count);
428 if (this.arrays[name])
429 arrays[name].set(this.arrays[name]);
430 }, this.spec.attribs);
431 this.buffer = buffer;
432 this.arrays = arrays;
433 this._vertexCapacity = count;
434 this.dirty = true;
435 }
436 this._vertexCount = count;
437 }
438 }
439 });
440
441 yuu.VertexBuffer.SubData = yT({
442 constructor: function (parent, begin, end) {
443 var arrays = this.arrays = {};
444 this._parent = parent;
445 this.spec = parent.spec;
446 this.buffer = parent.buffer;
447 yT.defineProperty(this, "vertexCount", end - begin);
448 for (var attrib in parent.arrays) {
449 var s = parent.spec.attribs[attrib].elements;
450 arrays[attrib] = parent.arrays[attrib].subarray(
451 begin * s, end * s);
452 }
453 },
454
455 dirty: { alias: "_parent.dirty" }
456 });
457
458 var rgbToHsl = yuu.rgbToHsl = yf.argcd(
459 /** Convert RBG [0, 1] to HSL [0, 1]. */
460 function (rgb) { return rgbToHsl.apply(null, rgb); },
461 function (r, g, b, a) {
462 var hsl = rgbToHsl(r, g, b);
463 hsl[3] = a;
464 return hsl;
465 },
466 function (r, g, b) {
467 var max = Math.max(r, g, b);
468 var min = Math.min(r, g, b);
469 var h, s, l = (max + min) / 2;
470
471 if (max === min) {
472 h = s = 0;
473 } else {
474 var d = max - min;
475 s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
476 switch (max) {
477 case r:
478 h = (g - b) / d + (g < b ? 6 : 0);
479 break;
480 case g:
481 h = (b - r) / d + 2;
482 break;
483 case b:
484 h = (r - g) / d + 4;
485 break;
486 }
487 h /= 6;
488 }
489
490 return [h, s, l];
491 }
492 );
493
494 var hslToRgb = yuu.hslToRgb = yf.argcd(
495 /** Convert HSL [0, 1] to RGB [0, 1]. */
496 function (hsl) { return hslToRgb.apply(null, hsl); },
497 function (h, s, l, a) {
498 var rgb = hslToRgb(h, s, l);
499 rgb[3] = a;
500 return rgb;
501 },
502 function (h, s, l) {
503 var r, g, b;
504
505 function hToC (p, q, t) {
506 if (t < 0)
507 t += 1;
508 if (t > 1)
509 t -= 1;
510 if (t < 1 / 6)
511 return p + (q - p) * 6 * t;
512 else if (t < 1 / 2)
513 return q;
514 else if (t < 2 / 3)
515 return p + (q - p) * (2/3 - t) * 6;
516 else
517 return p;
518 }
519
520 if (s === 0) {
521 r = g = b = l;
522 } else {
523 var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
524 var p = 2 * l - q;
525 r = hToC(p, q, h + 1 / 3);
526 g = hToC(p, q, h);
527 b = hToC(p, q, h - 1 / 3);
528 }
529
530 return [r, g, b];
531 }
532 );
533
534 var deviceFromCanvas = yuu.deviceFromCanvas = yf.argcd(
535 /** Convert a point from client to normalized device space
536
537 Normalized device space ranges from [-1, -1] at the
538 bottom-left of the viewport to [1, 1] at the top-right.
539 (This is the definition of the space, _not_ bounds on the
540 return value, as events can happen outside the viewport or
541 even outside the canvas.)
542 */
543 function (p) {
544 return deviceFromCanvas(p.x || p.pageX || p[0] || 0,
545 p.y || p.pageY || p[1] || 0);
546 },
547 function (x, y) {
548 x -= canvas.offsetLeft;
549 y -= canvas.offsetTop;
550 x /= canvas.clientWidth;
551 y /= canvas.clientHeight;
552 // xy is now in [0, 1] page space.
553
554 x *= canvas.width;
555 y *= canvas.height;
556 // xy now in canvas buffer space.
557
558 var vp = gl.getParameter(gl.VIEWPORT);
559 var hvpw = vp[2] / 2;
560 var hvph = vp[3] / 2;
561 x = (x - vp[0] - hvpw) / hvpw;
562 y = (y - vp[1] - hvph) / -hvph;
563 // xy now in normalized device space.
564
565 return {
566 x: x, 0: x,
567 y: y, 1: y,
568 inside: Math.abs(x) <= 1 && Math.abs(y) <= 1
569 };
570 }
571 );
572
573 yuu.viewport = new yuu.AABB();
574
575 function onresize () {
576 var resize = canvas.getAttribute("data-yuu-resize") !== null;
577 var width = +canvas.getAttribute("data-yuu-width");
578 var height = +canvas.getAttribute("data-yuu-height");
579
580 if (resize) {
581 canvas.width = canvas.clientWidth * dpr;
582 canvas.height = canvas.clientHeight * dpr;
583 }
584
585 var vw = canvas.width;
586 var vh = canvas.height;
587 if (width && height) {
588 var aspectRatio = width / height;
589 if (vw / vh > aspectRatio)
590 vw = vh * aspectRatio;
591 else
592 vh = vw / aspectRatio;
593 }
594 var vx = (canvas.width - vw) / 2;
595 var vy = (canvas.height - vh) / 2;
596 gl.viewport(vx, vy, vw, vh);
597 yuu.viewport = new yuu.AABB(vx, vy, vx + vw / dpr, vy + vh / dpr);
598 }
599
600 yuu.afterAnimationFrame = function (f) {
601 /* DOM class modifications intended to trigger transitions
602 must be delayed for at least one frame after the element is
603 created, i.e. after it has gone through at least one full
604 repaint.
605 */
606 window.requestAnimationFrame(function () {
607 setTimeout(f, 0);
608 });
609 };
610
611 yuu.registerInitHook(function (options) {
612 var bgColor = options.backgroundColor || [0.0, 0.0, 0.0, 0.0];
613
614 canvas = this.canvas = document.getElementById("yuu-canvas");
615 var glOptions = {
616 alpha: options.hasOwnProperty("alpha")
617 ? options.alpha : bgColor[3] !== 1.0,
618 antialias: options.hasOwnProperty("antialias")
619 ? options.antialias : true
620 };
621 if (!window.HTMLCanvasElement)
622 throw new Error("<canvas> isn't supported.");
623 gl = this.gl = canvas.getContext("webgl", glOptions)
624 || canvas.getContext("experimental-webgl", glOptions);
625 if (!gl)
626 throw new Error("WebGL isn't supported.");
627
628 canvas.focus();
629
630 window.addEventListener('resize', onresize);
631 onresize();
632
633 this.ShaderProgram.DEFAULT = new this.ShaderProgram();
634 this.Texture.DEFAULT = new this.Texture();
635 this.Material.DEFAULT = new this.Material();
636
637 gl.clearColor.apply(gl, bgColor);
638 gl.disable(gl.DEPTH_TEST);
639 gl.clear(gl.COLOR_BUFFER_BIT);
640 gl.enable(gl.BLEND);
641 gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD);
642 gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE);
643 });
644
645 var gui = yuu.require("nw.gui");
646 yT.defineProperty(yuu, "fullscreen", {
647 get: function () {
648 return gui
649 ? gui.Window.get().isFullscreen
650 : !!(document.fullscreenElement
651 || document.mozFullScreenElement);
652 },
653 set: function (v) {
654 if (gui)
655 gui.Window.get().isFullscreen = !!v;
656 else if (v)
657 document.body.requestFullscreen();
658 else
659 document.exitFullscreen();
660 }
661 });
662
663 }).call(typeof exports === "undefined" ? this : exports,
664 typeof exports === "undefined"
665 ? this.yuu : (module.exports = require('./core')));