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