Framebuffer PenSkin (#319)
* draw pen skin lines and stamps to a framebuffer * skip reading pixels and draw stamp to framebuffer * update silhouette with readPixels * draw pen canvas to buffer when its dirty Composite lines and stamps on browser preferred side (cpu/gpu) until the export texture is needed. Then blend the canvas with the current buffer contents. Updating this way invalidates useProgram optimization and the renderer currently does not have a way to know this. * draw lines on framebuffer through fragment shader * optimize draw regions and pen skin matrix creation * control draw regions * mobile gpus need high precision floats for line drawing * optimize cpu pen line math * sampled pen line caps * sampleless pen skin lines, lint, document pen skin buffer ops * add PenSkin._canvasDirty to constructor * remove DRAW_MODE_line * comment PenSkin reused memory, use memory in drawRectangle * turn draw region id's into optional method handlers A region ID object may have an enter and exit method on it that are used by default when entering and exiting that region. * remove old DRAW_MODE_line precision setting * standardize vert lines on 4 spaces * fixup! turn draw region id's into optional method handlers * do not draw when updating pen skin silhouette * do not premultiply stamp colors by alpha * fixup! do not draw when updating pen skin silhouette * do not premultiply line color * add a small rim around the line for aliasing * variable pen line alias amount * reverse offset pen line on y axis by relative alias amount Reverse the offset to keep small line overlap to a minimum. * fixup! reverse offset pen line on y axis by relative alias amount * medium precision gpu floats
This commit is contained in:
committed by
GitHub
parent
c54a928f0a
commit
4c8bc5d806
479
src/PenSkin.js
479
src/PenSkin.js
@@ -3,6 +3,9 @@ const twgl = require('twgl.js');
|
||||
const RenderConstants = require('./RenderConstants');
|
||||
const Skin = require('./Skin');
|
||||
|
||||
const Rectangle = require('./Rectangle');
|
||||
const ShaderManager = require('./ShaderManager');
|
||||
|
||||
/**
|
||||
* Attributes to use when drawing with the pen
|
||||
* @typedef {object} PenSkin#PenAttributes
|
||||
@@ -23,6 +26,48 @@ const DefaultPenAttributes = {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Reused memory location for projection matrices.
|
||||
* @type {FloatArray}
|
||||
*/
|
||||
const __projectionMatrix = twgl.m4.identity();
|
||||
|
||||
/**
|
||||
* Reused memory location for translation matrix for building a model matrix.
|
||||
* @type {FloatArray}
|
||||
*/
|
||||
const __modelTranslationMatrix = twgl.m4.identity();
|
||||
|
||||
/**
|
||||
* Reused memory location for rotation matrix for building a model matrix.
|
||||
* @type {FloatArray}
|
||||
*/
|
||||
const __modelRotationMatrix = twgl.m4.identity();
|
||||
|
||||
/**
|
||||
* Reused memory location for scaling matrix for building a model matrix.
|
||||
* @type {FloatArray}
|
||||
*/
|
||||
const __modelScalingMatrix = twgl.m4.identity();
|
||||
|
||||
/**
|
||||
* Reused memory location for a model matrix.
|
||||
* @type {FloatArray}
|
||||
*/
|
||||
const __modelMatrix = twgl.m4.identity();
|
||||
|
||||
/**
|
||||
* Reused memory location for a vector to create a translation matrix from.
|
||||
* @type {FloatArray}
|
||||
*/
|
||||
const __modelTranslationVector = twgl.v3.create();
|
||||
|
||||
/**
|
||||
* Reused memory location for a vector to create a scaling matrix from.
|
||||
* @type {FloatArray}
|
||||
*/
|
||||
const __modelScalingVector = twgl.v3.create();
|
||||
|
||||
class PenSkin extends Skin {
|
||||
/**
|
||||
* Create a Skin which implements a Scratch pen layer.
|
||||
@@ -43,15 +88,48 @@ class PenSkin extends Skin {
|
||||
/** @type {HTMLCanvasElement} */
|
||||
this._canvas = document.createElement('canvas');
|
||||
|
||||
/** @type {boolean} */
|
||||
this._canvasDirty = false;
|
||||
|
||||
/** @type {WebGLTexture} */
|
||||
this._texture = null;
|
||||
|
||||
/** @type {WebGLTexture} */
|
||||
this._exportTexture = null;
|
||||
|
||||
/** @type {WebGLFramebuffer} */
|
||||
this._framebuffer = null;
|
||||
|
||||
/** @type {WebGLFramebuffer} */
|
||||
this._silhouetteBuffer = null;
|
||||
|
||||
/** @type {boolean} */
|
||||
this._canvasDirty = false;
|
||||
|
||||
/** @type {boolean} */
|
||||
this._silhouetteDirty = false;
|
||||
|
||||
/** @type {object} */
|
||||
this._lineOnBufferDrawRegionId = {
|
||||
enter: () => this._enterDrawLineOnBuffer(),
|
||||
exit: () => this._exitDrawLineOnBuffer()
|
||||
};
|
||||
|
||||
/** @type {object} */
|
||||
this._toBufferDrawRegionId = {
|
||||
enter: () => this._enterDrawToBuffer(),
|
||||
exit: () => this._exitDrawToBuffer()
|
||||
};
|
||||
|
||||
/** @type {twgl.BufferInfo} */
|
||||
this._lineBufferInfo = null;
|
||||
|
||||
const NO_EFFECTS = 0;
|
||||
/** @type {twgl.ProgramInfo} */
|
||||
this._stampShader = this._renderer._shaderManager.getShader(ShaderManager.DRAW_MODE.stamp, NO_EFFECTS);
|
||||
|
||||
/** @type {twgl.ProgramInfo} */
|
||||
this._lineShader = this._renderer._shaderManager.getShader(ShaderManager.DRAW_MODE.lineSample, NO_EFFECTS);
|
||||
|
||||
this._createLineGeometry();
|
||||
|
||||
this.onNativeSizeChanged = this.onNativeSizeChanged.bind(this);
|
||||
this._renderer.on(RenderConstants.Events.NativeSizeChanged, this.onNativeSizeChanged);
|
||||
|
||||
@@ -64,6 +142,7 @@ class PenSkin extends Skin {
|
||||
dispose () {
|
||||
this._renderer.removeListener(RenderConstants.Events.NativeSizeChanged, this.onNativeSizeChanged);
|
||||
this._renderer.gl.deleteTexture(this._texture);
|
||||
this._renderer.gl.deleteTexture(this._exportTexture);
|
||||
this._texture = null;
|
||||
super.dispose();
|
||||
}
|
||||
@@ -90,23 +169,25 @@ class PenSkin extends Skin {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
getTexture (pixelsWide, pixelsTall) {
|
||||
if (this._canvasDirty) {
|
||||
this._canvasDirty = false;
|
||||
|
||||
const gl = this._renderer.gl;
|
||||
gl.bindTexture(gl.TEXTURE_2D, this._texture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._canvas);
|
||||
this._drawToBuffer();
|
||||
}
|
||||
|
||||
return this._texture;
|
||||
return this._exportTexture;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the pen layer.
|
||||
*/
|
||||
clear () {
|
||||
const gl = this._renderer.gl;
|
||||
twgl.bindFramebufferInfo(gl, this._framebuffer);
|
||||
|
||||
gl.clearColor(1, 1, 1, 0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
const ctx = this._canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
|
||||
this._canvasDirty = true;
|
||||
|
||||
this._silhouetteDirty = true;
|
||||
}
|
||||
|
||||
@@ -130,17 +211,196 @@ class PenSkin extends Skin {
|
||||
* @param {number} y1 - the Y coordinate of the end of the line.
|
||||
*/
|
||||
drawLine (penAttributes, x0, y0, x1, y1) {
|
||||
const ctx = this._canvas.getContext('2d');
|
||||
this._setAttributes(ctx, penAttributes);
|
||||
|
||||
// Width 1 and 3 lines need to be offset by 0.5.
|
||||
const diameter = penAttributes.diameter || DefaultPenAttributes.diameter;
|
||||
const offset = (Math.max(4 - diameter, 0) % 2) / 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(this._rotationCenter[0] + x0 + offset, this._rotationCenter[1] - y0 + offset);
|
||||
ctx.lineTo(this._rotationCenter[0] + x1 + offset, this._rotationCenter[1] - y1 + offset);
|
||||
ctx.stroke();
|
||||
this._canvasDirty = true;
|
||||
|
||||
this._drawLineOnBuffer(
|
||||
penAttributes,
|
||||
this._rotationCenter[0] + x0 + offset, this._rotationCenter[1] - y0 + offset,
|
||||
this._rotationCenter[0] + x1 + offset, this._rotationCenter[1] - y1 + offset
|
||||
);
|
||||
|
||||
this._silhouetteDirty = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create 2D geometry for drawing lines to a framebuffer.
|
||||
*/
|
||||
_createLineGeometry () {
|
||||
// Create a set of triangulated quads that break up a line into 3 parts:
|
||||
// 2 caps and a body. The y component of these position vertices are
|
||||
// divided to bring a value of 1 down to 0.5 to 0. The large y values
|
||||
// are set so they will still be at least 0.5 after division. The
|
||||
// divisor is scaled based on the length of the line and the lines
|
||||
// width.
|
||||
//
|
||||
// Texture coordinates are based on a "generated" texture whose general
|
||||
// shape is a circle. The line caps set their texture values to define
|
||||
// there roundedness with the texture. The body has all of its texture
|
||||
// valeus set to the center of the texture so its a solid block.
|
||||
const quads = {
|
||||
a_position: {
|
||||
numComponents: 2,
|
||||
data: [
|
||||
-0.5, 1,
|
||||
0.5, 1,
|
||||
-0.5, 100000,
|
||||
|
||||
-0.5, 100000,
|
||||
0.5, 1,
|
||||
0.5, 100000,
|
||||
|
||||
-0.5, 1,
|
||||
0.5, 1,
|
||||
-0.5, -1,
|
||||
|
||||
-0.5, -1,
|
||||
0.5, 1,
|
||||
0.5, -1,
|
||||
|
||||
-0.5, -100000,
|
||||
0.5, -100000,
|
||||
-0.5, -1,
|
||||
|
||||
-0.5, -1,
|
||||
0.5, -100000,
|
||||
0.5, -1
|
||||
]
|
||||
},
|
||||
a_texCoord: {
|
||||
numComponents: 2,
|
||||
data: [
|
||||
1, 0.5,
|
||||
0, 0.5,
|
||||
1, 0,
|
||||
|
||||
1, 0,
|
||||
0, 0.5,
|
||||
0, 0,
|
||||
|
||||
0.5, 0,
|
||||
0.5, 1,
|
||||
0.5, 0,
|
||||
|
||||
0.5, 0,
|
||||
0.5, 1,
|
||||
0.5, 1,
|
||||
|
||||
1, 0,
|
||||
0, 0,
|
||||
1, 0.5,
|
||||
|
||||
1, 0.5,
|
||||
0, 0,
|
||||
0, 0.5
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
this._lineBufferInfo = twgl.createBufferInfoFromArrays(this._renderer.gl, quads);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare to draw lines in the _lineOnBufferDrawRegionId region.
|
||||
*/
|
||||
_enterDrawLineOnBuffer () {
|
||||
const gl = this._renderer.gl;
|
||||
|
||||
const bounds = this._bounds;
|
||||
const currentShader = this._lineShader;
|
||||
const projection = twgl.m4.ortho(0, bounds.width, 0, bounds.height, -1, 1, __projectionMatrix);
|
||||
|
||||
twgl.bindFramebufferInfo(gl, this._framebuffer);
|
||||
|
||||
// Needs a blend function that blends a destination that starts with
|
||||
// no alpha.
|
||||
gl.blendFuncSeparate(
|
||||
gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA,
|
||||
gl.ONE, gl.ONE_MINUS_SRC_ALPHA
|
||||
);
|
||||
|
||||
gl.viewport(0, 0, bounds.width, bounds.height);
|
||||
|
||||
gl.useProgram(currentShader.program);
|
||||
|
||||
twgl.setBuffersAndAttributes(gl, currentShader, this._lineBufferInfo);
|
||||
|
||||
const uniforms = {
|
||||
u_skin: this._texture,
|
||||
u_projectionMatrix: projection,
|
||||
u_fudge: 0
|
||||
};
|
||||
|
||||
twgl.setUniforms(currentShader, uniforms);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return to a base state from _lineOnBufferDrawRegionId.
|
||||
*/
|
||||
_exitDrawLineOnBuffer () {
|
||||
const gl = this._renderer.gl;
|
||||
|
||||
gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ZERO, gl.ONE);
|
||||
|
||||
twgl.bindFramebufferInfo(gl, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a line on the framebuffer.
|
||||
* @param {PenAttributes} penAttributes - how the line should be drawn.
|
||||
* @param {number} x0 - the X coordinate of the beginning of the line.
|
||||
* @param {number} y0 - the Y coordinate of the beginning of the line.
|
||||
* @param {number} x1 - the X coordinate of the end of the line.
|
||||
* @param {number} y1 - the Y coordinate of the end of the line.
|
||||
*/
|
||||
_drawLineOnBuffer (penAttributes, x0, y0, x1, y1) {
|
||||
const gl = this._renderer.gl;
|
||||
|
||||
const currentShader = this._lineShader;
|
||||
|
||||
this._renderer.enterDrawRegion(this._lineOnBufferDrawRegionId);
|
||||
|
||||
const diameter = penAttributes.diameter || DefaultPenAttributes.diameter;
|
||||
const length = Math.hypot(Math.abs(x1 - x0) - 0.001, Math.abs(y1 - y0) - 0.001);
|
||||
const avgX = (x0 + x1) / 2;
|
||||
const avgY = (y0 + y1) / 2;
|
||||
const theta = Math.atan2(y0 - y1, x0 - x1);
|
||||
const alias = x0 === x1 || y0 === y1 ? 0 : 1;
|
||||
|
||||
// The line needs a bit of aliasing to look smooth. Add a small offset
|
||||
// and a small size boost to scaling to give a section to alias.
|
||||
const translationVector = __modelTranslationVector;
|
||||
translationVector[0] = avgX - (alias / 2);
|
||||
translationVector[1] = avgY + (alias / 4);
|
||||
|
||||
const scalingVector = __modelScalingVector;
|
||||
scalingVector[0] = diameter + alias;
|
||||
scalingVector[1] = length + diameter - (alias / 2);
|
||||
|
||||
const radius = diameter / 2;
|
||||
const yScalar = (0.50001 - (radius / (length + diameter)));
|
||||
|
||||
const uniforms = {
|
||||
u_positionScalar: yScalar,
|
||||
u_capScale: diameter,
|
||||
u_aliasAmount: alias,
|
||||
u_modelMatrix: twgl.m4.multiply(
|
||||
twgl.m4.multiply(
|
||||
twgl.m4.translation(translationVector, __modelTranslationMatrix),
|
||||
twgl.m4.rotationZ(theta - (Math.PI / 2), __modelRotationMatrix),
|
||||
__modelMatrix
|
||||
),
|
||||
twgl.m4.scaling(scalingVector, __modelScalingMatrix),
|
||||
__modelMatrix
|
||||
),
|
||||
u_lineColor: penAttributes.color4f || DefaultPenAttributes.color4f
|
||||
};
|
||||
|
||||
twgl.setUniforms(currentShader, uniforms);
|
||||
|
||||
twgl.drawBufferInfo(gl, this._lineBufferInfo, gl.TRIANGLES);
|
||||
|
||||
this._silhouetteDirty = true;
|
||||
}
|
||||
|
||||
@@ -152,11 +412,133 @@ class PenSkin extends Skin {
|
||||
*/
|
||||
drawStamp (stampElement, x, y) {
|
||||
const ctx = this._canvas.getContext('2d');
|
||||
|
||||
ctx.drawImage(stampElement, this._rotationCenter[0] + x, this._rotationCenter[1] - y);
|
||||
|
||||
this._canvasDirty = true;
|
||||
this._silhouetteDirty = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enter a draw region to draw a rectangle.
|
||||
*
|
||||
* Multiple calls with the same regionId skip the callback reducing the
|
||||
* amount of GL state changes.
|
||||
* @param {twgl.ProgramInfo} currentShader - program info to draw rectangle
|
||||
* with
|
||||
* @param {Rectangle} bounds - viewport bounds to draw in
|
||||
* region
|
||||
*/
|
||||
_drawRectangleRegionEnter (currentShader, bounds) {
|
||||
const gl = this._renderer.gl;
|
||||
|
||||
gl.viewport(0, 0, bounds.width, bounds.height);
|
||||
|
||||
gl.useProgram(currentShader.program);
|
||||
twgl.setBuffersAndAttributes(gl, currentShader, this._renderer._bufferInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a rectangle.
|
||||
* @param {twgl.ProgramInfo} currentShader - program info to draw rectangle
|
||||
* with
|
||||
* @param {WebGLTexture} texture - texture to draw
|
||||
* @param {Rectangle} bounds - bounded area to draw in
|
||||
* @param {number} x - centered at x
|
||||
* @param {number} y - centered at y
|
||||
*/
|
||||
_drawRectangle (currentShader, texture, bounds, x = -this._canvas.width / 2, y = this._canvas.height / 2) {
|
||||
const gl = this._renderer.gl;
|
||||
|
||||
const projection = twgl.m4.ortho(
|
||||
bounds.left, bounds.right, bounds.top, bounds.bottom, -1, 1,
|
||||
__projectionMatrix
|
||||
);
|
||||
|
||||
const uniforms = {
|
||||
u_skin: texture,
|
||||
u_projectionMatrix: projection,
|
||||
u_modelMatrix: twgl.m4.multiply(
|
||||
twgl.m4.translation(twgl.v3.create(
|
||||
-x - (bounds.width / 2),
|
||||
-y + (bounds.height / 2),
|
||||
0
|
||||
), __modelTranslationMatrix),
|
||||
twgl.m4.scaling(twgl.v3.create(
|
||||
bounds.width,
|
||||
bounds.height,
|
||||
0
|
||||
), __modelScalingMatrix),
|
||||
__modelMatrix
|
||||
),
|
||||
u_fudge: 0
|
||||
};
|
||||
|
||||
twgl.setTextureParameters(gl, texture, {minMag: gl.NEAREST});
|
||||
twgl.setUniforms(currentShader, uniforms);
|
||||
|
||||
twgl.drawBufferInfo(gl, this._renderer._bufferInfo, gl.TRIANGLES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare to draw a rectangle in the _toBufferDrawRegionId region.
|
||||
*/
|
||||
_enterDrawToBuffer () {
|
||||
const gl = this._renderer.gl;
|
||||
|
||||
twgl.bindFramebufferInfo(gl, this._framebuffer);
|
||||
|
||||
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
this._drawRectangleRegionEnter(this._stampShader, this._bounds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return to a base state from _toBufferDrawRegionId.
|
||||
*/
|
||||
_exitDrawToBuffer () {
|
||||
const gl = this._renderer.gl;
|
||||
|
||||
gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ZERO, gl.ONE);
|
||||
|
||||
twgl.bindFramebufferInfo(gl, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the input texture to the framebuffer.
|
||||
* @param {WebGLTexture} texture - input texture to draw
|
||||
* @param {number} x - texture centered at x
|
||||
* @param {number} y - texture centered at y
|
||||
*/
|
||||
_drawToBuffer (texture = this._texture, x = -this._canvas.width / 2, y = this._canvas.height / 2) {
|
||||
if (texture !== this._texture && this._canvasDirty) {
|
||||
this._drawToBuffer();
|
||||
}
|
||||
|
||||
const gl = this._renderer.gl;
|
||||
|
||||
// If the input texture is the one that represents the pen's canvas
|
||||
// layer, update the texture with the canvas data.
|
||||
if (texture === this._texture) {
|
||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._canvas);
|
||||
|
||||
const ctx = this._canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
|
||||
|
||||
this._canvasDirty = false;
|
||||
}
|
||||
|
||||
const currentShader = this._stampShader;
|
||||
const bounds = this._bounds;
|
||||
|
||||
this._renderer.enterDrawRegion(this._toBufferDrawRegionId);
|
||||
|
||||
this._drawRectangle(currentShader, texture, bounds, x, y);
|
||||
|
||||
this._silhouetteDirty = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* React to a change in the renderer's native size.
|
||||
* @param {object} event - The change event.
|
||||
@@ -174,10 +556,15 @@ class PenSkin extends Skin {
|
||||
const [width, height] = canvasSize;
|
||||
|
||||
const gl = this._renderer.gl;
|
||||
|
||||
this._bounds = new Rectangle();
|
||||
this._bounds.initFromBounds(width / 2, width / -2, height / 2, height / -2);
|
||||
|
||||
this._canvas.width = width;
|
||||
this._canvas.height = height;
|
||||
this._rotationCenter[0] = width / 2;
|
||||
this._rotationCenter[1] = height / 2;
|
||||
|
||||
this._texture = twgl.createTexture(
|
||||
gl,
|
||||
{
|
||||
@@ -188,7 +575,36 @@ class PenSkin extends Skin {
|
||||
src: this._canvas
|
||||
}
|
||||
);
|
||||
this._canvasDirty = true;
|
||||
|
||||
this._exportTexture = twgl.createTexture(
|
||||
gl,
|
||||
{
|
||||
auto: true,
|
||||
mag: gl.NEAREST,
|
||||
min: gl.NEAREST,
|
||||
wrap: gl.CLAMP_TO_EDGE,
|
||||
width,
|
||||
height
|
||||
}
|
||||
);
|
||||
|
||||
const attachments = [
|
||||
{
|
||||
format: gl.RGBA,
|
||||
attachment: this._exportTexture
|
||||
}
|
||||
];
|
||||
if (this._framebuffer) {
|
||||
twgl.resizeFramebufferInfo(gl, this._framebuffer, attachments, width, height);
|
||||
twgl.resizeFramebufferInfo(gl, this._silhouetteBuffer, [{format: gl.RGBA}], width, height);
|
||||
} else {
|
||||
this._framebuffer = twgl.createFramebufferInfo(gl, attachments, width, height);
|
||||
this._silhouetteBuffer = twgl.createFramebufferInfo(gl, [{format: gl.RGBA}], width, height);
|
||||
}
|
||||
|
||||
gl.clearColor(1, 1, 1, 0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
this._silhouetteDirty = true;
|
||||
}
|
||||
|
||||
@@ -220,9 +636,32 @@ class PenSkin extends Skin {
|
||||
updateSilhouette () {
|
||||
if (this._silhouetteDirty) {
|
||||
if (this._canvasDirty) {
|
||||
this.getTexture();
|
||||
this._drawToBuffer();
|
||||
}
|
||||
|
||||
// Render export texture to another framebuffer
|
||||
const gl = this._renderer.gl;
|
||||
|
||||
const bounds = this._bounds;
|
||||
|
||||
this._renderer.enterDrawRegion(this._toBufferDrawRegionId);
|
||||
|
||||
// Sample the framebuffer's pixels into the silhouette instance
|
||||
const skinPixels = new Uint8Array(Math.floor(this._canvas.width * this._canvas.height * 4));
|
||||
gl.readPixels(0, 0, this._canvas.width, this._canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, skinPixels);
|
||||
|
||||
const skinCanvas = this._canvas;
|
||||
skinCanvas.width = bounds.width;
|
||||
skinCanvas.height = bounds.height;
|
||||
|
||||
const skinContext = skinCanvas.getContext('2d');
|
||||
const skinImageData = skinContext.createImageData(bounds.width, bounds.height);
|
||||
skinImageData.data.set(skinPixels);
|
||||
skinContext.putImageData(skinImageData, 0, 0);
|
||||
|
||||
this._silhouette.update(this._canvas);
|
||||
|
||||
this._silhouetteDirty = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,6 +172,12 @@ class RenderWebGL extends EventEmitter {
|
||||
/** @type {HTMLCanvasElement} */
|
||||
this._tempCanvas = document.createElement('canvas');
|
||||
|
||||
/** @type {any} */
|
||||
this._regionId = null;
|
||||
|
||||
/** @type {function} */
|
||||
this._exitRegion = null;
|
||||
|
||||
this._svgTextBubble = new SVGTextBubble();
|
||||
|
||||
this._createGeometry();
|
||||
@@ -573,6 +579,8 @@ class RenderWebGL extends EventEmitter {
|
||||
* Draw all current drawables and present the frame on the canvas.
|
||||
*/
|
||||
draw () {
|
||||
this._doExitDrawRegion();
|
||||
|
||||
const gl = this._gl;
|
||||
|
||||
twgl.bindFramebufferInfo(gl, null);
|
||||
@@ -729,6 +737,8 @@ class RenderWebGL extends EventEmitter {
|
||||
}
|
||||
|
||||
_isTouchingColorGpuStart (drawableID, candidateIDs, bounds, color3b, mask3b) {
|
||||
this._doExitDrawRegion();
|
||||
|
||||
const gl = this._gl;
|
||||
twgl.bindFramebufferInfo(gl, this._queryBufferInfo);
|
||||
|
||||
@@ -997,6 +1007,8 @@ class RenderWebGL extends EventEmitter {
|
||||
* @return {?DrawableExtraction} Data about the picked drawable
|
||||
*/
|
||||
extractDrawable (drawableID, x, y) {
|
||||
this._doExitDrawRegion();
|
||||
|
||||
const drawable = this._allDrawables[drawableID];
|
||||
if (!drawable) return null;
|
||||
|
||||
@@ -1077,6 +1089,8 @@ class RenderWebGL extends EventEmitter {
|
||||
* @return {?ColorExtraction} Data about the picked color
|
||||
*/
|
||||
extractColor (x, y, radius) {
|
||||
this._doExitDrawRegion();
|
||||
|
||||
const scratchX = Math.round(this._nativeSize[0] * ((x / this._gl.canvas.clientWidth) - 0.5));
|
||||
const scratchY = Math.round(-this._nativeSize[1] * ((y / this._gl.canvas.clientHeight) - 0.5));
|
||||
|
||||
@@ -1313,6 +1327,8 @@ class RenderWebGL extends EventEmitter {
|
||||
* @param {int} stampID - the unique ID of the Drawable to use as the stamp.
|
||||
*/
|
||||
penStamp (penSkinID, stampID) {
|
||||
this._doExitDrawRegion();
|
||||
|
||||
const stampDrawable = this._allDrawables[stampID];
|
||||
if (!stampDrawable) {
|
||||
return;
|
||||
@@ -1337,24 +1353,12 @@ class RenderWebGL extends EventEmitter {
|
||||
|
||||
try {
|
||||
gl.disable(gl.BLEND);
|
||||
this._drawThese([stampID], ShaderManager.DRAW_MODE.default, projection, {ignoreVisibility: true});
|
||||
this._drawThese([stampID], ShaderManager.DRAW_MODE.stamp, projection, {ignoreVisibility: true});
|
||||
} finally {
|
||||
gl.enable(gl.BLEND);
|
||||
}
|
||||
|
||||
const stampPixels = new Uint8Array(Math.floor(bounds.width * bounds.height * 4));
|
||||
gl.readPixels(0, 0, bounds.width, bounds.height, gl.RGBA, gl.UNSIGNED_BYTE, stampPixels);
|
||||
|
||||
const stampCanvas = this._tempCanvas;
|
||||
stampCanvas.width = bounds.width;
|
||||
stampCanvas.height = bounds.height;
|
||||
|
||||
const stampContext = stampCanvas.getContext('2d');
|
||||
const stampImageData = stampContext.createImageData(bounds.width, bounds.height);
|
||||
stampImageData.data.set(stampPixels);
|
||||
stampContext.putImageData(stampImageData, 0, 0);
|
||||
|
||||
skin.drawStamp(stampCanvas, bounds.left, bounds.top);
|
||||
skin._drawToBuffer(this._queryBufferInfo.attachments[0], bounds.left, bounds.top);
|
||||
}
|
||||
|
||||
/* ******
|
||||
@@ -1422,6 +1426,41 @@ class RenderWebGL extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enter a draw region.
|
||||
*
|
||||
* A draw region is where multiple draw operations are performed with the
|
||||
* same GL state. WebGL performs poorly when it changes state like blend
|
||||
* mode. Marking a collection of state values as a "region" the renderer
|
||||
* can skip superfluous extra state calls when it is already in that
|
||||
* region. Since one region may be entered from within another a exit
|
||||
* handle can also be registered that is called when a new region is about
|
||||
* to be entered to restore a common inbetween state.
|
||||
*
|
||||
* @param {any} regionId - id of the region to enter
|
||||
* @param {function} enter - handle to call when first entering a region
|
||||
* @param {function} exit - handle to call when leaving a region
|
||||
*/
|
||||
enterDrawRegion (regionId, enter = regionId.enter, exit = regionId.exit) {
|
||||
if (this._regionId !== regionId) {
|
||||
this._doExitDrawRegion();
|
||||
this._regionId = regionId;
|
||||
enter();
|
||||
this._exitRegion = exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forcefully exit the current region returning to a common inbetween GL
|
||||
* state.
|
||||
*/
|
||||
_doExitDrawRegion () {
|
||||
if (this._exitRegion !== null) {
|
||||
this._exitRegion();
|
||||
}
|
||||
this._exitRegion = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a set of Drawables, by drawable ID
|
||||
* @param {Array<int>} drawables The Drawable IDs to draw, possibly this._drawList.
|
||||
@@ -1467,7 +1506,13 @@ class RenderWebGL extends EventEmitter {
|
||||
let effectBits = drawable.getEnabledEffects();
|
||||
effectBits &= opts.hasOwnProperty('effectMask') ? opts.effectMask : effectBits;
|
||||
const newShader = this._shaderManager.getShader(drawMode, effectBits);
|
||||
if (currentShader !== newShader) {
|
||||
|
||||
// Manually perform region check. Do not create functions inside a
|
||||
// loop.
|
||||
if (this._regionId !== newShader) {
|
||||
this._doExitDrawRegion();
|
||||
this._regionId = newShader;
|
||||
|
||||
currentShader = newShader;
|
||||
gl.useProgram(currentShader.program);
|
||||
twgl.setBuffersAndAttributes(gl, currentShader, this._bufferInfo);
|
||||
@@ -1496,6 +1541,8 @@ class RenderWebGL extends EventEmitter {
|
||||
|
||||
twgl.drawBufferInfo(gl, this._bufferInfo, gl.TRIANGLES);
|
||||
}
|
||||
|
||||
this._regionId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -166,7 +166,17 @@ ShaderManager.DRAW_MODE = {
|
||||
/**
|
||||
* Draw only the parts of the drawable which match a particular color.
|
||||
*/
|
||||
colorMask: 'colorMask'
|
||||
colorMask: 'colorMask',
|
||||
|
||||
/**
|
||||
* Sample a "texture" to draw a line with caps.
|
||||
*/
|
||||
lineSample: 'lineSample',
|
||||
|
||||
/**
|
||||
* Draw normally except for pre-multiplied alpha
|
||||
*/
|
||||
stamp: 'stamp'
|
||||
};
|
||||
|
||||
module.exports = ShaderManager;
|
||||
|
||||
@@ -35,6 +35,12 @@ uniform float u_mosaic;
|
||||
uniform float u_ghost;
|
||||
#endif // ENABLE_ghost
|
||||
|
||||
#ifdef DRAW_MODE_lineSample
|
||||
uniform vec4 u_lineColor;
|
||||
uniform float u_capScale;
|
||||
uniform float u_aliasAmount;
|
||||
#endif // DRAW_MODE_lineSample
|
||||
|
||||
uniform sampler2D u_skin;
|
||||
|
||||
varying vec2 v_texCoord;
|
||||
@@ -103,6 +109,7 @@ const vec2 kCenter = vec2(0.5, 0.5);
|
||||
|
||||
void main()
|
||||
{
|
||||
#ifndef DRAW_MODE_lineSample
|
||||
vec2 texcoord0 = v_texCoord;
|
||||
|
||||
#ifdef ENABLE_mosaic
|
||||
@@ -148,12 +155,6 @@ void main()
|
||||
|
||||
gl_FragColor = texture2D(u_skin, texcoord0);
|
||||
|
||||
|
||||
if (gl_FragColor.a == 0.0)
|
||||
{
|
||||
discard;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_ghost
|
||||
gl_FragColor.a *= u_ghost;
|
||||
#endif // ENABLE_ghost
|
||||
@@ -199,7 +200,21 @@ void main()
|
||||
#endif // DRAW_MODE_colorMask
|
||||
|
||||
// WebGL defaults to premultiplied alpha
|
||||
#ifndef DRAW_MODE_stamp
|
||||
gl_FragColor.rgb *= gl_FragColor.a;
|
||||
#endif // DRAW_MODE_stamp
|
||||
|
||||
#endif // DRAW_MODE_silhouette
|
||||
|
||||
#else // DRAW_MODE_lineSample
|
||||
gl_FragColor = u_lineColor;
|
||||
gl_FragColor.a *= clamp(
|
||||
// Scale the capScale a little to have an aliased region.
|
||||
(u_capScale + u_aliasAmount -
|
||||
u_capScale * 2.0 * distance(v_texCoord, vec2(0.5, 0.5))
|
||||
) / (u_aliasAmount + 1.0),
|
||||
0.0,
|
||||
1.0
|
||||
);
|
||||
#endif // DRAW_MODE_lineSample
|
||||
}
|
||||
|
||||
@@ -6,7 +6,17 @@ attribute vec2 a_texCoord;
|
||||
|
||||
varying vec2 v_texCoord;
|
||||
|
||||
#ifdef DRAW_MODE_lineSample
|
||||
uniform float u_positionScalar;
|
||||
#endif
|
||||
|
||||
void main() {
|
||||
#ifdef DRAW_MODE_lineSample
|
||||
vec2 position = a_position;
|
||||
position.y = clamp(position.y * u_positionScalar, -0.5, 0.5);
|
||||
gl_Position = u_projectionMatrix * u_modelMatrix * vec4(position, 0, 1);
|
||||
#else
|
||||
gl_Position = u_projectionMatrix * u_modelMatrix * vec4(a_position, 0, 1);
|
||||
#endif
|
||||
v_texCoord = a_texCoord;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user