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/
10 var yT
= this.yT
|| require("./yT");
11 var yf
= this.yf
|| require("./yf");
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
19 // https://www.khronos.org/registry/webgl/sdk/demos/google/shiny-teapot/index.html
21 // With DPR = 1 this presumably still happens but the negative
22 // effect is lessened.
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
28 var dpr
= yuu
.DPR
= !yuu
.isSafari()
29 ? (this.devicePixelRatio
|| 1)
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
41 Values from https://www.khronos.org/registry/webgl/specs/1.0/.
43 See also notes in pre on Safari's typed array problems.
46 yuu
.uniform = function (location
, value
) {
47 /** Set a uniform in the active program
49 The type of the uniform is automatically determined from
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
57 * Sequences of other lengths throw a TypeError
59 It is not possible to call uniformMatrix2fv via this
62 switch (value
.constructor) {
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");
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");
91 function isShaderSource (src
) {
92 return src
.indexOf("\n") >= 0 || yf
.last(src
.trim()) === ";";
95 var FRAGMENT_SHADER
= 0x8B30;
96 var VERTEX_SHADER
= 0x8B31;
98 EXTS
[FRAGMENT_SHADER
] = "frag";
99 EXTS
[VERTEX_SHADER
] = "vert";
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
]));
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
);
116 "Shader compile error:\n\n" + src
+ "\n\n" + log
);
122 yuu
.ShaderProgram
= yT({
123 constructor: function (vs
, fs
) {
124 /** A linked program of vertex and fragment shaders
126 vs and fs are arrays of vertex and fragment shader source
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
);
139 if (!gl
.getProgramParameter(id
, gl
.LINK_STATUS
))
140 throw new Error("Shader link error: "
141 + gl
.getProgramInfoLog(id
));
143 }).catch(function (exc
) {
145 this.id
= yuu
.ShaderProgram
.DEFAULT
.id
;
146 this.attribs
= yuu
.ShaderProgram
.DEFAULT
.attribs
;
147 this.uniforms
= yuu
.ShaderProgram
.DEFAULT
.uniforms
;
149 }.bind(this)).then(function (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
));
163 setUniforms: function () {
164 /** Set the values of program uniforms
166 The arguments are any number of objects mapping
167 uniform names to values (floats, vec3s, etc.).
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
]);
174 setAttribPointers: function (buffer
) {
175 /** Bind the contents of a vertex buffer to attributes
177 `buffer` is (or is like) a yuu.VertexBuffer instance.
179 for (var name
in this.attribs
)
180 gl
.vertexAttribPointer(
182 buffer
.spec
.attribs
[name
].elements
,
183 buffer
.spec
.attribs
[name
].View
.prototype.GL_TYPE
,
184 false, 0, buffer
.arrays
[name
].byteOffset
);
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()];
194 function glScopedEnum (scope
, gl
, name
) {
195 var value
= glEnum(gl
, scope
+ "_" + name
);
196 if (value
=== undefined)
197 value
= glEnum(gl
, name
);
201 var glTextureEnum
= glScopedEnum
.bind(null, "texture");
203 yuu
.Texture
= yuu
.Caching(yT({
204 constructor: function (path
, overrideOptions
) {
207 The texture is set to a 1x1 white texture until it is
208 loaded (or if loading fails).
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
|| {});
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
);
225 gl
.TEXTURE_2D
, 0, gl
.RGBA
, 1, 1, 0,
226 gl
.RGBA
, gl
.UNSIGNED_BYTE
, data
);
228 gl
.TEXTURE_2D
, gl
.TEXTURE_MAG_FILTER
, gl
.NEAREST
);
230 gl
.TEXTURE_2D
, gl
.TEXTURE_MIN_FILTER
, gl
.NEAREST
);
231 gl
.bindTexture(gl
.TEXTURE_2D
, null);
232 this.ready
= Promise
.resolve(this);
236 path
= yuu
.resourcePath(path
, "images", "png");
237 this.id
= yuu
.Texture
.DEFAULT
.id
;
239 this.width
= yuu
.Texture
.DEFAULT
.width
;
240 this.height
= yuu
.Texture
.DEFAULT
.height
;
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
]);
250 gl
.TEXTURE_2D
, 0, gl
.RGBA
,
251 gl
.RGBA
, gl
.UNSIGNED_BYTE
, img
);
252 gl
.bindTexture(gl
.TEXTURE_2D
, null);
254 this.width
= img
.width
;
255 this.height
= img
.height
;
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);
267 var TEXTURE_DEFAULTS
= yuu
.Texture
.DEFAULTS
= {
270 wrapS
: "clampToEdge",
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
|| {};
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
);
294 disable: function () {
295 /** Disable this material */
296 gl
.bindTexture(gl
.TEXTURE_2D
, null);
298 for (var attrib
in this.program
.attribs
)
299 gl
.disableVertexAttribArray(this.program
.attribs
[attrib
]);
303 yuu
.VertexAttribSpec = function (spec
) {
304 /** Ordering and types for vertex buffer layout
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.
312 spec
.forEach(function (a
) {
314 var elements
= a
.elements
;
315 var View
= a
.View
|| Float32Array
;
316 this.attribs
[name
] = { elements
: elements
,
317 byteOffset
: byteOffset
,
319 byteOffset
+= elements
* View
.BYTES_PER_ELEMENT
;
321 this.bytesPerVertex
= byteOffset
;
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 }
331 yuu
.IndexBuffer
= yT({
332 constructor: function (maxIndex
, length
) {
334 this._maxIndex
= maxIndex
;
337 this.length
= length
;
338 this._glBuffer
= gl
.createBuffer();
342 bindBuffer: function () {
343 gl
.bindBuffer(gl
.ELEMENT_ARRAY_BUFFER
, this._glBuffer
);
347 gl
.ELEMENT_ARRAY_BUFFER
, this.buffer
, gl
.DYNAMIC_DRAW
);
351 GL_TYPE
: { alias
: "buffer.GL_TYPE" },
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
);
361 buffer
.set(this.buffer
);
362 this.buffer
= buffer
;
365 this._maxIndex
= maxIndex
;
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
);
376 buffer
.set(this.buffer
);
377 this.buffer
= buffer
;
378 this._capacity
= count
;
381 this._length
= count
;
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))
391 else if (maxIndex
< (1 << 16))
397 yuu
.VertexBuffer
= yT({
398 constructor: function (spec
, vertexCount
) {
399 /** A buffer with a specified vertex format and vertex count
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.
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.
412 this._vertexCapacity
= -1;
415 this.vertexCount
= vertexCount
;
416 this._glBuffer
= gl
.createBuffer();
420 bindBuffer: function () {
421 gl
.bindBuffer(gl
.ARRAY_BUFFER
, this._glBuffer
);
424 gl
.bufferData(gl
.ARRAY_BUFFER
, this.buffer
, gl
.DYNAMIC_DRAW
);
428 subdata: function (begin
, length
) {
429 return new yuu
.VertexBuffer
.SubData(this, begin
, begin
+ length
);
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
);
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
;
451 this._vertexCount
= count
;
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(
470 dirty
: { alias
: "_parent.dirty" }
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
);
482 var max
= Math
.max(r
, g
, b
);
483 var min
= Math
.min(r
, g
, b
);
484 var h
, s
, l
= (max
+ min
) / 2;
490 s
= l
> 0.5 ? d
/ (2 - max
- min
) : d
/ (max
+ min
);
493 h
= (g
- b
) / d
+ (g
< b
? 6 : 0);
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
);
520 function hToC (p
, q
, t
) {
526 return p
+ (q
- p
) * 6 * t
;
530 return p
+ (q
- p
) * (2/3 - t
) * 6;
538 var q
= l
< 0.5 ? l
* (1 + s
) : l
+ s
- l
* s
;
540 r
= hToC(p
, q
, h
+ 1 / 3);
542 b
= hToC(p
, q
, h
- 1 / 3);
549 var deviceFromCanvas
= yuu
.deviceFromCanvas
= yf
.argcd(
550 /** Convert a point from client to normalized device space
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.)
559 return deviceFromCanvas(p
.x
|| p
.pageX
|| p
[0] || 0,
560 p
.y
|| p
.pageY
|| p
[1] || 0);
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.
571 // xy now in canvas buffer space.
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.
583 inside
: Math
.abs(x
) <= 1 && Math
.abs(y
) <= 1
588 yuu
.viewport
= new yuu
.AABB();
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");
596 canvas
.width
= canvas
.clientWidth
* dpr
;
597 canvas
.height
= canvas
.clientHeight
* dpr
;
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
;
607 vh
= vw
/ aspectRatio
;
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
);
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
621 window
.requestAnimationFrame(function () {
626 yuu
.registerInitHook(function (options
) {
627 var bgColor
= options
.backgroundColor
|| [0.0, 0.0, 0.0, 0.0];
629 canvas
= this.canvas
= document
.getElementById("yuu-canvas");
631 alpha
: options
.hasOwnProperty("alpha")
632 ? options
.alpha
: bgColor
[3] !== 1.0,
633 antialias
: options
.hasOwnProperty("antialias")
634 ? options
.antialias
: true
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
);
641 throw new Error("WebGL isn't supported.");
645 window
.addEventListener('resize', onresize
);
648 this.ShaderProgram
.DEFAULT
= new this.ShaderProgram();
649 this.Texture
.DEFAULT
= new this.Texture();
650 this.Material
.DEFAULT
= new this.Material();
652 gl
.clearColor
.apply(gl
, bgColor
);
653 gl
.disable(gl
.DEPTH_TEST
);
654 gl
.clear(gl
.COLOR_BUFFER_BIT
);
656 gl
.blendEquationSeparate(gl
.FUNC_ADD
, gl
.FUNC_ADD
);
657 gl
.blendFuncSeparate(gl
.ONE
, gl
.ONE_MINUS_SRC_ALPHA
, gl
.ONE
, gl
.ONE
);
660 var gui
= yuu
.require("nw.gui");
661 yT
.defineProperty(yuu
, "fullscreen", {
664 ? gui
.Window
.get().isFullscreen
665 : !!(document
.fullscreenElement
666 || document
.mozFullScreenElement
);
670 gui
.Window
.get().isFullscreen
= !!v
;
672 document
.body
.requestFullscreen();
674 document
.exitFullscreen();
678 }).call(typeof exports
=== "undefined" ? this : exports
,
679 typeof exports
=== "undefined"
680 ? this.yuu
: (module
.exports
= require('./core')));