Skip to content

Commit ed45bcd

Browse files
authored
Optimize pen line drawing using instanced rendering (#11)
1 parent c89c863 commit ed45bcd

File tree

1 file changed

+147
-140
lines changed

1 file changed

+147
-140
lines changed

src/PenSkin.js

+147-140
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,9 @@ const DefaultPenAttributes = {
2424
diameter: 1
2525
};
2626

27-
/**
28-
* Reused memory location for storing a premultiplied pen color.
29-
* @type {FloatArray}
30-
*/
31-
const __premultipliedColor = [0, 0, 0, 0];
32-
33-
const PEN_BUFFER_SIZE_LARGER = 65520;
34-
const PEN_BUFFER_SIZE_SMALLER = 32760;
27+
const PEN_ATTRIBUTE_BUFFER_SIZE = 163800;
28+
const PEN_ATTRIBUTE_STRIDE = 10;
29+
const PEN_ATTRIBUTE_STRIDE_BYTES = PEN_ATTRIBUTE_STRIDE * 4;
3530

3631
class PenSkin extends Skin {
3732
/**
@@ -71,67 +66,90 @@ class PenSkin extends Skin {
7166
exit: () => this._exitUsePenBuffer()
7267
};
7368

69+
/** @type {WebGLRenderingContext} */
70+
const gl = this._renderer.gl;
71+
7472
// tw: renderQuality attribute
7573
this.renderQuality = 1;
7674

7775
// tw: keep track of native size
7876
this._nativeSize = renderer.getNativeSize();
7977

80-
// tw: create the extra data structures needed to buffer pen
81-
this._resetAttributeIndexes();
82-
this.a_lineColor = new Float32Array(PEN_BUFFER_SIZE_LARGER);
83-
this.a_lineThicknessAndLength = new Float32Array(PEN_BUFFER_SIZE_SMALLER);
84-
this.a_penPoints = new Float32Array(PEN_BUFFER_SIZE_LARGER);
85-
this.a_position = new Float32Array(PEN_BUFFER_SIZE_SMALLER);
86-
for (let i = 0; i < this.a_position.length; i += 12) {
87-
this.a_position[i + 0] = 1;
88-
this.a_position[i + 1] = 0;
89-
this.a_position[i + 2] = 0;
90-
this.a_position[i + 3] = 0;
91-
this.a_position[i + 4] = 1;
92-
this.a_position[i + 5] = 1;
93-
this.a_position[i + 6] = 1;
94-
this.a_position[i + 7] = 1;
95-
this.a_position[i + 8] = 0;
96-
this.a_position[i + 9] = 0;
97-
this.a_position[i + 10] = 0;
98-
this.a_position[i + 11] = 1;
99-
}
100-
/** @type {twgl.BufferInfo} */
101-
this._lineBufferInfo = twgl.createBufferInfoFromArrays(this._renderer.gl, {
102-
a_position: {
103-
numComponents: 2,
104-
data: this.a_position
105-
},
106-
a_lineColor: {
107-
numComponents: 4,
108-
drawType: this._renderer.gl.STREAM_DRAW,
109-
data: this.a_lineColor
110-
},
111-
a_lineThicknessAndLength: {
112-
numComponents: 2,
113-
drawType: this._renderer.gl.STREAM_DRAW,
114-
data: this.a_lineThicknessAndLength
115-
},
116-
a_penPoints: {
117-
numComponents: 4,
118-
drawType: this._renderer.gl.STREAM_DRAW,
119-
data: this.a_penPoints
120-
}
121-
});
122-
12378
const NO_EFFECTS = 0;
12479
/** @type {twgl.ProgramInfo} */
12580
this._lineShader = this._renderer._shaderManager.getShader(ShaderManager.DRAW_MODE.line, NO_EFFECTS);
12681

127-
// tw: draw region used to preserve texture when resizing
82+
// Draw region used to preserve texture when resizing
12883
this._drawTextureShader = this._renderer._shaderManager.getShader(ShaderManager.DRAW_MODE.default, NO_EFFECTS);
12984
/** @type {object} */
13085
this._drawTextureRegionId = {
13186
enter: () => this._enterDrawTexture(),
13287
exit: () => this._exitDrawTexture()
13388
};
13489

90+
this.a_position_glbuffer = gl.createBuffer();
91+
this.a_position_loc = gl.getAttribLocation(this._lineShader.program, 'a_position');
92+
93+
this.a_lineColor_loc = gl.getAttribLocation(this._lineShader.program, 'a_lineColor');
94+
this.a_lineThicknessAndLength_loc = gl.getAttribLocation(this._lineShader.program, 'a_lineThicknessAndLength');
95+
this.a_penPoints_loc = gl.getAttribLocation(this._lineShader.program, 'a_penPoints');
96+
97+
this.attribute_glbuffer = gl.createBuffer();
98+
this.attribute_index = 0;
99+
this.attribute_data = new Float32Array(PEN_ATTRIBUTE_BUFFER_SIZE);
100+
gl.bindBuffer(gl.ARRAY_BUFFER, this.attribute_glbuffer);
101+
gl.bufferData(gl.ARRAY_BUFFER, this.attribute_data.length * 4, gl.STREAM_DRAW);
102+
103+
if (gl.drawArraysInstanced) {
104+
// WebGL2 has native instanced rendering
105+
this.instancedRendering = true;
106+
this.glDrawArraysInstanced = gl.drawArraysInstanced.bind(gl);
107+
this.glVertexAttribDivisor = gl.vertexAttribDivisor.bind(gl);
108+
} else {
109+
// WebGL1 may have instanced rendering through the ANGLE_instanced_arrays extension
110+
const instancedArraysExtension = gl.getExtension('ANGLE_instanced_arrays');
111+
if (instancedArraysExtension) {
112+
this.instancedRendering = true;
113+
this.glDrawArraysInstanced = instancedArraysExtension.drawArraysInstancedANGLE.bind(
114+
instancedArraysExtension
115+
);
116+
this.glVertexAttribDivisor = instancedArraysExtension.vertexAttribDivisorANGLE.bind(
117+
instancedArraysExtension
118+
);
119+
} else {
120+
// Inefficient but still supported
121+
this.instancedRendering = false;
122+
}
123+
}
124+
125+
if (this.instancedRendering) {
126+
gl.bindBuffer(gl.ARRAY_BUFFER, this.a_position_glbuffer);
127+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
128+
1, 0,
129+
0, 0,
130+
1, 1,
131+
0, 1
132+
]), gl.STATIC_DRAW);
133+
} else {
134+
const positionBuffer = new Float32Array(PEN_ATTRIBUTE_BUFFER_SIZE / PEN_ATTRIBUTE_STRIDE * 2);
135+
for (let i = 0; i < positionBuffer.length; i += 12) {
136+
positionBuffer[i + 0] = 1;
137+
positionBuffer[i + 1] = 0;
138+
positionBuffer[i + 2] = 0;
139+
positionBuffer[i + 3] = 0;
140+
positionBuffer[i + 4] = 1;
141+
positionBuffer[i + 5] = 1;
142+
positionBuffer[i + 6] = 1;
143+
positionBuffer[i + 7] = 1;
144+
positionBuffer[i + 8] = 0;
145+
positionBuffer[i + 9] = 0;
146+
positionBuffer[i + 10] = 0;
147+
positionBuffer[i + 11] = 1;
148+
}
149+
gl.bindBuffer(gl.ARRAY_BUFFER, this.a_position_glbuffer);
150+
gl.bufferData(gl.ARRAY_BUFFER, positionBuffer, gl.STATIC_DRAW);
151+
}
152+
135153
this.onNativeSizeChanged = this.onNativeSizeChanged.bind(this);
136154
this._renderer.on(RenderConstants.Events.NativeSizeChanged, this.onNativeSizeChanged);
137155

@@ -222,8 +240,6 @@ class PenSkin extends Skin {
222240
* Prepare to draw lines in the _lineOnBufferDrawRegionId region.
223241
*/
224242
_enterDrawLineOnBuffer () {
225-
// tw: reset attributes when starting pen drawing
226-
this._resetAttributeIndexes();
227243
const gl = this._renderer.gl;
228244

229245
twgl.bindFramebufferInfo(gl, this._framebuffer);
@@ -232,22 +248,27 @@ class PenSkin extends Skin {
232248

233249
const currentShader = this._lineShader;
234250
gl.useProgram(currentShader.program);
235-
twgl.setBuffersAndAttributes(gl, currentShader, this._lineBufferInfo);
236251

237252
const uniforms = {
238253
u_skin: this._texture,
239254
u_stageSize: this._size
240255
};
241256

242257
twgl.setUniforms(currentShader, uniforms);
258+
259+
gl.bindBuffer(gl.ARRAY_BUFFER, this.a_position_glbuffer);
260+
gl.enableVertexAttribArray(this.a_position_loc);
261+
gl.vertexAttribPointer(this.a_position_loc, 2, gl.FLOAT, false, 2 * 4, 0);
262+
263+
this.attribute_index = 0;
243264
}
244265

245266
/**
246267
* Return to a base state from _lineOnBufferDrawRegionId.
247268
*/
248269
_exitDrawLineOnBuffer () {
249270
// tw: flush when exiting pen rendering
250-
if (this.a_lineColorIndex) {
271+
if (this.attribute_index) {
251272
this._flushLines();
252273
}
253274

@@ -326,19 +347,15 @@ class PenSkin extends Skin {
326347
_drawLineOnBuffer (penAttributes, x0, y0, x1, y1) {
327348
this._renderer.enterDrawRegion(this._lineOnBufferDrawRegionId);
328349

329-
// tw: flush if this line would overflow buffers
330-
// For some reason, looking up the size of a_lineColor with .length is very slow in some browsers.
331-
// We see measurable performance improvements by comparing to a constant instead.
332-
if (this.a_lineColorIndex + 24 > PEN_BUFFER_SIZE_LARGER) {
350+
const iters = this.instancedRendering ? 1 : 6;
351+
352+
// For some reason, looking up the size of a buffer through .length can be slow,
353+
// so use a constant instead.
354+
if (this.attribute_index + (PEN_ATTRIBUTE_STRIDE * iters) > PEN_ATTRIBUTE_BUFFER_SIZE) {
333355
this._flushLines();
334356
}
335357

336-
// Premultiply pen color by pen transparency
337358
const penColor = penAttributes.color4f || DefaultPenAttributes.color4f;
338-
__premultipliedColor[0] = penColor[0] * penColor[3];
339-
__premultipliedColor[1] = penColor[1] * penColor[3];
340-
__premultipliedColor[2] = penColor[2] * penColor[3];
341-
__premultipliedColor[3] = penColor[3];
342359

343360
// tw: apply renderQuality
344361
x0 *= this.renderQuality;
@@ -357,92 +374,82 @@ class PenSkin extends Skin {
357374

358375
// tw: apply renderQuality
359376
const lineThickness = (penAttributes.diameter || DefaultPenAttributes.diameter) * this.renderQuality;
360-
// tw: write pen draws to buffers where they will be flushed later
361-
for (let i = 0; i < 6; i++) {
362-
this.a_lineColor[this.a_lineColorIndex] = __premultipliedColor[0];
363-
this.a_lineColorIndex++;
364-
this.a_lineColor[this.a_lineColorIndex] = __premultipliedColor[1];
365-
this.a_lineColorIndex++;
366-
this.a_lineColor[this.a_lineColorIndex] = __premultipliedColor[2];
367-
this.a_lineColorIndex++;
368-
this.a_lineColor[this.a_lineColorIndex] = __premultipliedColor[3];
369-
this.a_lineColorIndex++;
370-
371-
this.a_lineThicknessAndLength[this.a_lineThicknessAndLengthIndex] = lineThickness;
372-
this.a_lineThicknessAndLengthIndex++;
373-
374-
this.a_lineThicknessAndLength[this.a_lineThicknessAndLengthIndex] = lineLength;
375-
this.a_lineThicknessAndLengthIndex++;
376-
377-
this.a_penPoints[this.a_penPointsIndex] = x0;
378-
this.a_penPointsIndex++;
379-
this.a_penPoints[this.a_penPointsIndex] = -y0;
380-
this.a_penPointsIndex++;
381-
this.a_penPoints[this.a_penPointsIndex] = lineDiffX;
382-
this.a_penPointsIndex++;
383-
this.a_penPoints[this.a_penPointsIndex] = -lineDiffY;
384-
this.a_penPointsIndex++;
385-
}
386-
}
387377

388-
// tw: resets indexes in the pen drawing buffers
389-
_resetAttributeIndexes () {
390-
this.a_lineColorIndex = 0;
391-
this.a_lineThicknessAndLengthIndex = 0;
392-
this.a_penPointsIndex = 0;
378+
for (let i = 0; i < iters; i++) {
379+
// Pen color sent to the GPU is pre-multiplied by transparency
380+
this.attribute_data[this.attribute_index] = penColor[0] * penColor[3];
381+
this.attribute_index++;
382+
this.attribute_data[this.attribute_index] = penColor[1] * penColor[3];
383+
this.attribute_index++;
384+
this.attribute_data[this.attribute_index] = penColor[2] * penColor[3];
385+
this.attribute_index++;
386+
this.attribute_data[this.attribute_index] = penColor[3];
387+
this.attribute_index++;
388+
389+
this.attribute_data[this.attribute_index] = lineThickness;
390+
this.attribute_index++;
391+
392+
this.attribute_data[this.attribute_index] = lineLength;
393+
this.attribute_index++;
394+
395+
this.attribute_data[this.attribute_index] = x0;
396+
this.attribute_index++;
397+
this.attribute_data[this.attribute_index] = -y0;
398+
this.attribute_index++;
399+
this.attribute_data[this.attribute_index] = lineDiffX;
400+
this.attribute_index++;
401+
this.attribute_data[this.attribute_index] = -lineDiffY;
402+
this.attribute_index++;
403+
}
393404
}
394405

395-
// tw: flushes buffered pen lines to the GPU
396406
_flushLines () {
407+
/** @type {WebGLRenderingContext} */
397408
const gl = this._renderer.gl;
398409

399-
const currentShader = this._lineShader;
410+
gl.bindBuffer(gl.ARRAY_BUFFER, this.attribute_glbuffer);
411+
gl.bufferSubData(gl.ARRAY_BUFFER, 0, new Float32Array(this.attribute_data.buffer, 0, this.attribute_index));
400412

401-
// If only a small amount of data needs to be uploaded, only upload part of the data.
402-
// todo: need to see if this helps and fine tune this number
403-
if (this.a_lineColorIndex < 1000) {
404-
twgl.setAttribInfoBufferFromArray(
405-
gl,
406-
this._lineBufferInfo.attribs.a_lineColor,
407-
new Float32Array(this.a_lineColor.buffer, 0, this.a_lineColorIndex),
408-
0
409-
);
410-
twgl.setAttribInfoBufferFromArray(
411-
gl,
412-
this._lineBufferInfo.attribs.a_penPoints,
413-
new Float32Array(this.a_penPoints.buffer, 0, this.a_penPointsIndex),
414-
0
415-
);
416-
twgl.setAttribInfoBufferFromArray(
417-
gl,
418-
this._lineBufferInfo.attribs.a_lineThicknessAndLength,
419-
new Float32Array(this.a_lineThicknessAndLength.buffer, 0, this.a_lineThicknessAndLengthIndex),
420-
0
413+
gl.enableVertexAttribArray(this.a_lineColor_loc);
414+
gl.vertexAttribPointer(
415+
this.a_lineColor_loc,
416+
4, gl.FLOAT, false,
417+
PEN_ATTRIBUTE_STRIDE_BYTES, 0
418+
);
419+
420+
gl.enableVertexAttribArray(this.a_lineThicknessAndLength_loc);
421+
gl.vertexAttribPointer(
422+
this.a_lineThicknessAndLength_loc,
423+
2, gl.FLOAT, false,
424+
PEN_ATTRIBUTE_STRIDE_BYTES, 4 * 4
425+
);
426+
427+
gl.enableVertexAttribArray(this.a_penPoints_loc);
428+
gl.vertexAttribPointer(
429+
this.a_penPoints_loc,
430+
4, gl.FLOAT, false,
431+
PEN_ATTRIBUTE_STRIDE_BYTES, 6 * 4
432+
);
433+
434+
if (this.instancedRendering) {
435+
this.glVertexAttribDivisor(this.a_lineColor_loc, 1);
436+
this.glVertexAttribDivisor(this.a_lineThicknessAndLength_loc, 1);
437+
this.glVertexAttribDivisor(this.a_penPoints_loc, 1);
438+
439+
this.glDrawArraysInstanced(
440+
gl.TRIANGLE_STRIP,
441+
0, 4,
442+
this.attribute_index / PEN_ATTRIBUTE_STRIDE
421443
);
444+
445+
this.glVertexAttribDivisor(this.a_lineColor_loc, 0);
446+
this.glVertexAttribDivisor(this.a_lineThicknessAndLength_loc, 0);
447+
this.glVertexAttribDivisor(this.a_penPoints_loc, 0);
422448
} else {
423-
twgl.setAttribInfoBufferFromArray(
424-
gl,
425-
this._lineBufferInfo.attribs.a_lineColor,
426-
this.a_lineColor
427-
);
428-
twgl.setAttribInfoBufferFromArray(
429-
gl,
430-
this._lineBufferInfo.attribs.a_penPoints,
431-
this.a_penPoints
432-
);
433-
twgl.setAttribInfoBufferFromArray(
434-
gl,
435-
this._lineBufferInfo.attribs.a_lineThicknessAndLength,
436-
this.a_lineThicknessAndLength
437-
);
449+
gl.drawArrays(gl.TRIANGLES, 0, this.attribute_index / PEN_ATTRIBUTE_STRIDE);
438450
}
439-
// todo: if we skip twgl and do all this buffer stuff ourselves, we can skip some unneeded gl calls
440-
twgl.setBuffersAndAttributes(gl, currentShader, this._lineBufferInfo);
441-
442-
twgl.drawBufferInfo(gl, this._lineBufferInfo, gl.TRIANGLES, this.a_lineThicknessAndLengthIndex / 2);
443-
444-
this._resetAttributeIndexes();
445451

452+
this.attribute_index = 0;
446453
this._silhouetteDirty = true;
447454
}
448455

0 commit comments

Comments
 (0)