Fix pixelate effect

This implementation of the pixel effect is now very similar to the
PixelBender implementation in Scratch 2.0

I also added a slider to demo.html as a debugging aid: it manipulates a
value that is passed into the shader and used in whatever way helps.
This commit is contained in:
Christopher Willis-Ford 2016-05-20 14:15:37 -07:00
parent 41d3d6f6ba
commit 920271d076
4 changed files with 93 additions and 28 deletions

View File

@ -5,10 +5,29 @@
<title>Scratch WebGL rendering demo</title>
</head>
<body>
<canvas id="scratch-stage" width="10" height="10" style="border:1px dashed black;"></canvas>
<canvas id="scratch-stage" width="10" height="10" style="border:1px dashed black; margin-top: 9px;"></canvas>
<p>
<input type="range" id="fudge" style="width:50%" min="0" max="100" step="any" oninput="onFudgeChanged(this.value)" onchange="onFudgeChanged(this.value)">
</p>
<p>
Min: <input id="fudgeMin" type="number" onchange="onFudgeMinChanged(this.value)">
Max: <input id="fudgeMax" type="number" onchange="onFudgeMaxChanged(this.value)">
</p>
</body>
<script src="render-webgl.js"></script>
<script>
window.fudge = 0;
var fudgeInput = document.getElementById('fudge');
function onFudgeMinChanged(newValue) {
fudgeInput.min = newValue;
}
function onFudgeMaxChanged(newValue) {
fudgeInput.max = newValue;
}
function onFudgeChanged(newValue) {
window.fudge = newValue;
}
var canvas = document.getElementById('scratch-stage');
var renderer = new RenderWebGL(canvas);
var drawableID = renderer.createDrawable();
@ -17,12 +36,14 @@
renderer.draw();
requestAnimationFrame(drawStep);
}
renderer.updateDrawableProperties(drawableID, {
position: [0, 0],
scale: 300,
direction: 90
});
var direction = 90;
var posX = 0;
var posY = 0;
var scale = 100;
function thinkStep() {
direction += 0.1;
//direction += 0.1;
var props = {};
//props.position = [posX, posY];

View File

@ -4,6 +4,7 @@ var xhr = require('xhr');
/**
* An object which can be drawn by the renderer.
* TODO: double-buffer all rendering state (position, skin, shader index, etc.)
* @param renderer The renderer which owns this Drawable.
* @param gl The OpenGL context.
* @constructor
@ -15,24 +16,50 @@ function Drawable(renderer, gl) {
this._renderer = renderer;
this._gl = gl;
// TODO: double-buffer uniforms
/**
* The uniforms to be used by the vertex and pixel shaders.
* Some of these are used by other parts of the renderer as well.
* @type {Object.<string,*>}
* @private
*/
this._uniforms = {
u_texture: null,
u_mvp: twgl.m4.identity()
/**
* The model-view-projection matrix.
* @type {module:twgl/m4.Mat4}
*/
u_mvp: twgl.m4.identity(),
/**
* The scaling factor to use when drawing.
* This is updated at the same time as u_mvp.
* @type {number}
*/
u_scale: 1,
/**
* The nominal (not necessarily current) size of the current skin.
* @type {number[]}
*/
u_skinSize: [0, 0],
/**
* The actual WebGL texture object for the skin.
* @type {WebGLTexture}
*/
u_texture: null
};
// Effect values are uniforms too
var numEffects = Drawable.EFFECTS.length;
for (var index = 0; index < numEffects; ++index) {
var effectName = Drawable.EFFECTS[index];
var converter = Drawable._effectCoverter[effectName];
var converter = Drawable._effectConverter[effectName];
this._uniforms['u_' + effectName] = converter(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._shaderIndex = 0;
this._costumeResolution = 2; // TODO: only for bitmaps
@ -49,7 +76,7 @@ module.exports = Drawable;
* @type {Object.<string,function>}
* @private
*/
Drawable._effectCoverter = {
Drawable._effectConverter = {
color: function(x) {
return (360 * x / 200) % 360;
},
@ -60,9 +87,7 @@ Drawable._effectCoverter = {
return x * Math.PI / 180;
},
pixelate: function(x) {
// TODO: if (targetObj == stagePane) n *= stagePane.scaleX;
// TODO: n = Math.min(n, Math.min(srcWidth, srcHeight));
return Math.abs(x) / 10 + 1;
return Math.abs(x) / 10;
},
mosaic: function(x) {
x = Math.round((Math.abs(x) + 10) / 10);
@ -81,7 +106,7 @@ Drawable._effectCoverter = {
* The name of each supported effect.
* @type {Array}
*/
Drawable.EFFECTS = Object.keys(Drawable._effectCoverter);
Drawable.EFFECTS = Object.keys(Drawable._effectConverter);
/**
* The cache of all shaders compiled so far. These are generated on demand.
@ -131,7 +156,7 @@ Drawable._DEFAULT_SKIN = {
squirrel: '7e24c99c1b853e52f8e7f9004416fa34.png',
bus: '66895930177178ea01d9e610917f8acf.png',
scratch_cat: '09dc888b0b7df19f70d81588ae73420e.svg'
}.scratch_cat;
}.squirrel;
/**
* Dispose of this Drawable. Do not use it after calling this method.
@ -281,7 +306,7 @@ Drawable.prototype._setSkinCore = function (source, costumeResolution) {
if (!err) {
instance._costumeResolution = costumeResolution || 1;
instance._uniforms.u_texture = texture;
instance._setDimensions(source.width, source.height);
instance._setSkinSize(source.width, source.height);
}
};
@ -298,7 +323,7 @@ Drawable.prototype._setSkinCore = function (source, costumeResolution) {
if (willCallCallback) {
if (!this._uniforms.u_texture) {
this._uniforms.u_texture = texture;
this._setDimensions(0, 0);
this._setSkinSize(0, 0);
}
}
else {
@ -353,7 +378,7 @@ Drawable.prototype.updateProperties = function (properties) {
else {
this._shaderIndex &= ~mask;
}
var converter = Drawable._effectCoverter[propertyName];
var converter = Drawable._effectConverter[propertyName];
this._uniforms['u_' + propertyName] = converter(rawValue);
}
}
@ -365,10 +390,11 @@ Drawable.prototype.updateProperties = function (properties) {
* @param {int} height The height of the new skin.
* @private
*/
Drawable.prototype._setDimensions = function (width, height) {
if (this._dimensions[0] != width || this._dimensions[1] != height) {
this._dimensions[0] = width;
this._dimensions[1] = height;
Drawable.prototype._setSkinSize = function (width, height) {
if (this._uniforms.u_skinSize[0] != width
|| this._uniforms.u_skinSize[1] != height) {
this._uniforms.u_skinSize[0] = width;
this._uniforms.u_skinSize[1] = height;
this.setTransformDirty();
}
};
@ -378,12 +404,20 @@ Drawable.prototype._setDimensions = function (width, height) {
* @private
*/
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;
var projection = this._renderer.getProjectionMatrix();
twgl.m4.translate(projection, this._position, mvp);
var rotation = (270 - this._direction) * Math.PI / 180;
twgl.m4.rotateZ(mvp, rotation, mvp);
twgl.m4.scale(mvp, twgl.v3.mulScalar(this._dimensions, scale), mvp);
var scale = this._scale / 100;
scale /= this._costumeResolution;
this._uniforms.u_scale = scale;
var scaledSize = twgl.v3.mulScalar(this._uniforms.u_skinSize, scale);
scaledSize[2] = 0; // was NaN because u_skinSize has only 2 components
twgl.m4.scale(mvp, scaledSize, mvp);
this._transformDirty = false;
};

View File

@ -120,6 +120,7 @@ RenderWebGL.prototype.draw = function () {
twgl.setBuffersAndAttributes(gl, currentShader, this._bufferInfo);
}
twgl.setUniforms(currentShader, drawable.getUniforms());
twgl.setUniforms(currentShader, {u_fudge: window.fudge || 0});
twgl.drawBufferInfo(gl, gl.TRIANGLES, this._bufferInfo);
}
};

View File

@ -1,5 +1,7 @@
precision mediump float;
uniform float u_fudge;
#ifdef ENABLE_color
uniform float u_color;
#endif
@ -11,6 +13,8 @@ uniform float u_whirl;
#endif
#ifdef ENABLE_pixelate
uniform float u_pixelate;
uniform float u_scale;
uniform vec2 u_skinSize;
#endif
#ifdef ENABLE_mosaic
uniform float u_mosaic;
@ -103,7 +107,12 @@ void main()
#endif // ENABLE_mosaic
#ifdef ENABLE_pixelate
texcoord0 = floor(texcoord0 / u_pixelate) * u_pixelate + u_pixelate_half;
{
// TODO: understand why this padding helps clean up "pixel" edges
const vec2 pixelPadding = vec2(0.125, 0.125);
vec2 pixelTexelSize = u_skinSize * u_scale / u_pixelate;
texcoord0 = (floor(texcoord0 * pixelTexelSize + pixelPadding)) / pixelTexelSize;
}
#endif // ENABLE_pixelate
#ifdef ENABLE_whirl