diff --git a/build/demo.html b/build/demo.html index 489e6f4..4af9aab 100644 --- a/build/demo.html +++ b/build/demo.html @@ -11,11 +11,23 @@ \ No newline at end of file diff --git a/src/Drawable.js b/src/Drawable.js new file mode 100644 index 0000000..740f1ae --- /dev/null +++ b/src/Drawable.js @@ -0,0 +1,150 @@ +var twgl = require('twgl.js'); + +function Drawable(renderer, gl) { + this._id = Drawable._nextDrawable++; + Drawable._allDrawables[this._id] = this; + + this._renderer = renderer; + this._gl = gl; + + // TODO: double-buffer uniforms + this._uniforms = { + u_texture: null, + u_mvp: twgl.m4.identity(), + u_brightness_shift: 0, + u_hue_shift: 0, + u_whirl_radians: 0 + }; + + this._position = twgl.v3.create(0, 0); + this._scale = 100; + this._direction = 90; + this._dimensions = twgl.v3.create(0, 0); + this._transformDirty = true; + this._costumeResolution = 2; // TODO: only for bitmaps + + this.setSkin(this._DEFAULT_SKIN); +} + +module.exports = Drawable; + +/** + * The ID to be assigned next time the Drawable constructor is called. + * @type {number} + * @private + */ +Drawable._nextDrawable = 0; + +/** + * All current Drawables, by ID. + * @type {Object.} + * @private + */ +Drawable._allDrawables = {}; + +/** + * Fetch a Drawable by its ID number. + * @param drawableID {int} The ID of the Drawable to fetch. + * @returns {?Drawable} The specified Drawable if found, otherwise null. + */ +Drawable.getDrawableByID = function (drawableID) { + return Drawable._allDrawables[drawableID]; +}; + +Drawable.dirtyAllTransforms = function () { + for (var drawableID in Drawable._allDrawables) { + if (Drawable._allDrawables.hasOwnProperty(drawableID)) { + var drawable = Drawable._allDrawables[drawableID]; + drawable.setTransformDirty(); + } + } +}; + +// TODO: fall back on a built-in skin to protect against network problems +Drawable.prototype._DEFAULT_SKIN = { + squirrel: '7e24c99c1b853e52f8e7f9004416fa34.png', + bus: '66895930177178ea01d9e610917f8acf.png' +}.squirrel; + +Drawable.prototype.dispose = function () { + this.setSkin(null); + if (this._id >= 0) { + delete Drawable[this._id]; + } +}; + +Drawable.prototype.setTransformDirty = function () { + this._transformDirty = true; +}; + +Drawable.prototype.getID = function () { + return this._id; +}; + +Drawable.prototype.setSkin = function (skin_md5ext) { + // TODO: share Skins across Drawables - see also destroy() + if (this._uniforms.u_texture) { + this._gl.deleteTexture(this._uniforms); + } + if (skin_md5ext) { + var url = + 'https://cdn.assets.scratch.mit.edu/internalapi/asset/' + + skin_md5ext + + '/get/'; + var instance = this; + this._uniforms.u_texture = + twgl.createTexture(this._gl, { + auto: true, + src: url + }, function (err, texture, source) { + if (!err) { + instance._dimensions[0] = source.width; + instance._dimensions[1] = source.height; + instance.setTransformDirty(); + } + }); + } + else { + this._uniforms.u_texture = null; + } +}; + +Drawable.prototype.getUniforms = function () { + if (this._transformDirty) { + this._calculateTransform(); + } + return this._uniforms; +}; + +Drawable.prototype.setPosition = function (x, y) { + if (this._position[0] != x || this._position[1] != y) { + this._position[0] = x; + this._position[1] = y; + this.setTransformDirty(); + } +}; + +Drawable.prototype.setDirection = function (directionDegrees) { + if (this._direction != directionDegrees) { + this._direction = directionDegrees; + this.setTransformDirty(); + } +}; + +Drawable.prototype.setScale = function (scalePercent) { + if(this._scale != scalePercent) { + this._scale = scalePercent; + this.setTransformDirty(); + } +}; + +Drawable.prototype._calculateTransform = function () { + var rotation = (270 - this._direction) * Math.PI / 180; + var scale = this._scale / 100 / this._costumeResolution; + var projection = this._renderer.getProjectionMatrix(); + var mvp = this._uniforms.u_mvp; + twgl.m4.translate(projection, this._position, mvp); + twgl.m4.rotateZ(mvp, rotation, mvp); + twgl.m4.scale(mvp, twgl.v3.mulScalar(this._dimensions, scale), mvp); + this._transformDirty = false; +}; diff --git a/src/index.js b/src/index.js index c3a71f7..4abe8cd 100644 --- a/src/index.js +++ b/src/index.js @@ -2,6 +2,8 @@ var EventEmitter = require('events'); var twgl = require('twgl.js'); var util = require('util'); +var Drawable = require('./drawable'); + /** * Create a renderer for drawing Scratch sprites to a canvas using WebGL. * Optionally, specify the logical and/or physical size of the Scratch stage. @@ -24,9 +26,12 @@ function RenderWebGL( // Bind event emitter and runtime to VM instance EventEmitter.call(this); + // TODO: remove? + twgl.setDefaults({crossOrigin: true}); + this._gl = twgl.getWebGLContext(canvas); - this._drawables = {}; - this._uniforms = {}; + this._drawables = []; + this._projection = twgl.m4.identity(); this._createPrograms(); this._createGeometry(); @@ -61,8 +66,8 @@ RenderWebGL.prototype.setStageSize = function (xLeft, xRight, yBottom, yTop) { this._xRight = xRight; this._yBottom = yBottom; this._yTop = yTop; - this._uniforms.u_projection = - twgl.m4.ortho(xLeft, xRight, yBottom, yTop, -1, 1); + this._projection = twgl.m4.ortho(xLeft, xRight, yBottom, yTop, -1, 1); + Drawable.dirtyAllTransforms(); }; /** @@ -87,20 +92,72 @@ RenderWebGL.prototype.draw = function () { gl.clearColor(1, 0, 1, 1); gl.clear(gl.COLOR_BUFFER_BIT); + gl.enable(gl.BLEND); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + gl.useProgram(this._programInfo.program); twgl.setBuffersAndAttributes(gl, this._programInfo, this._bufferInfo); - twgl.setUniforms(this._programInfo, this._uniforms); - - for (var id in this._drawables) { - if (this._drawables.hasOwnProperty(id)) { - var drawable = this._drawables[id]; - twgl.setUniforms(this._programInfo, drawable); - twgl.drawBufferInfo(gl, gl.TRIANGLES, this._bufferInfo); - } + var numDrawables = this._drawables.length; + for (var drawableIndex = 0; drawableIndex < numDrawables; ++drawableIndex) { + var drawableID = this._drawables[drawableIndex]; + var drawable = Drawable.getDrawableByID(drawableID); + twgl.setUniforms(this._programInfo, drawable.getUniforms()); + twgl.drawBufferInfo(gl, gl.TRIANGLES, this._bufferInfo); } }; +/** + * Create a new Drawable and add it to the scene. + * @returns {int} The ID of the new Drawable. + */ +RenderWebGL.prototype.createDrawable = function () { + var drawable = new Drawable(this, this._gl); + var drawableID = drawable.getID(); + this._drawables.push(drawableID); + return drawableID; +}; + +/** + * Destroy a Drawable, removing it from the scene. + * @param {int} drawableID The ID of the Drawable to remove. + * @returns {boolean} True iff the drawable was found and removed. + */ +RenderWebGL.prototype.destroyDrawable = function (drawableID) { + var index = this._drawables.indexOf(drawableID); + if (index >= 0) { + Drawable.getDrawableByID(drawableID).dispose(); + this._drawables.splice(index, 1); + return true; + } + return false; +}; + +RenderWebGL.prototype.setDrawablePosition = function (drawableID, x, y) { + var drawable = Drawable.getDrawableByID(drawableID); + if (drawable) { + drawable.setPosition(x, y); + } +}; + +RenderWebGL.prototype.setDrawableDirection = function (drawableID, directionDegrees) { + var drawable = Drawable.getDrawableByID(drawableID); + if (drawable) { + drawable.setDirection(directionDegrees); + } +}; + +RenderWebGL.prototype.setDrawableScale = function (drawableID, scalePercent) { + var drawable = Drawable.getDrawableByID(drawableID); + if (drawable) { + drawable.setScale(scalePercent); + } +}; + +RenderWebGL.prototype.getProjectionMatrix = function () { + return this._projection; +}; + /** * Build shaders. * @private @@ -118,14 +175,28 @@ RenderWebGL.prototype._createPrograms = function () { */ RenderWebGL.prototype._createGeometry = function () { var quad = { - position: [ - -0.5, -0.5, 0, - 0.5, -0.5, 0, - -0.5, 0.5, 0, - -0.5, 0.5, 0, - 0.5, -0.5, 0, - 0.5, 0.5, 0 - ] + a_position: { + numComponents: 2, + data: [ + -0.5, -0.5, + 0.5, -0.5, + -0.5, 0.5, + -0.5, 0.5, + 0.5, -0.5, + 0.5, 0.5 + ] + }, + a_texCoord: { + numComponents: 2, + data: [ + 1, 0, + 0, 0, + 1, 1, + 1, 1, + 0, 0, + 0, 1 + ] + } }; this._bufferInfo = twgl.createBufferInfoFromArrays(this._gl, quad); }; diff --git a/src/shaders/sprite.frag b/src/shaders/sprite.frag index aa52cb7..aaf105e 100644 --- a/src/shaders/sprite.frag +++ b/src/shaders/sprite.frag @@ -1,13 +1,12 @@ precision mediump float; -uniform sampler2D u_image; -varying vec2 v_texCoord; - -uniform float u_hue_shift; +uniform sampler2D u_skin; uniform float u_brightness_shift; - +uniform float u_hue_shift; uniform float u_whirl_radians; +varying vec2 v_texCoord; + vec3 convertRGB2HSV(vec3 rgb) { float maxRGB = max(max(rgb.r, rgb.g), rgb.b); @@ -98,7 +97,7 @@ void main() } } - gl_FragColor = texture2D(u_image, texcoord0); + gl_FragColor = texture2D(u_skin, texcoord0); // TODO: See if we can/should use actual alpha test. // Does bgfx offer a way to set u_alphaRef? Would that help? diff --git a/src/shaders/sprite.vert b/src/shaders/sprite.vert index 8074c76..03e074a 100644 --- a/src/shaders/sprite.vert +++ b/src/shaders/sprite.vert @@ -1,12 +1,11 @@ -attribute vec4 position; +uniform mat4 u_mvp; + +attribute vec2 a_position; +attribute vec2 a_texCoord; + varying vec2 v_texCoord; -uniform mat4 u_transform; -uniform mat4 u_projection; - void main() { - gl_Position = u_projection * u_transform * position; - - // Map clipspace coordinates to texture coordinates - v_texCoord = (position.xy * vec2(1.0, -1.0)) + vec2(0.5); + gl_Position = u_mvp * vec4(a_position, 0, 1); + v_texCoord = a_texCoord; }