webgl:绘制许多圆圈的最快方法
webgl: fastest approach to drawing many circles
我目前正在绘制数千个圆圈,实例化一个 circle geometry(许多三角形)。
或者,我可以简单地实例化一个四边形(2 个三角形),但使用距离函数和 discard
.
在片段着色器中切出一个圆
哪种方法会更快? -- 绘制许多三角形是否比在片段着色器中完成的计算更昂贵?
最快的方法可能取决于 GPU 和许多其他因素,例如您如何绘制圆、2D、3D、是否混合它们、是否使用 z 缓冲区等...但是在一般来说,三角形越少越快,像素越少越快。所以....,我们能做的只有努力了。
首先让我们只绘制带纹理的四边形,不进行混合。首先,我似乎总是从 WebGL 获得不一致的性能,但在我的 GPU 测试中,我在这个 300x150 canvas 中使用实例化
以 60fps 获得 20k-30k 四边形
function main() {
const gl = document.querySelector('canvas').getContext('webgl');
const ext = gl.getExtension('ANGLE_instanced_arrays');
if (!ext) {
return alert('need ANGLE_instanced_arrays');
}
twgl.addExtensionsToContext(gl);
const vs = `
attribute float id;
attribute vec4 position;
attribute vec2 texcoord;
uniform float time;
varying vec2 v_texcoord;
varying vec4 v_color;
void main() {
float o = id + time;
gl_Position = position + vec4(
vec2(
fract(o * 0.1373),
fract(o * 0.5127)) * 2.0 - 1.0,
0, 0);
v_texcoord = texcoord;
v_color = vec4(fract(vec3(id) * vec3(0.127, 0.373, 0.513)), 1);
}`;
const fs = `
precision mediump float;
varying vec2 v_texcoord;
varying vec4 v_color;
uniform sampler2D tex;
void main() {
gl_FragColor = texture2D(tex, v_texcoord) * v_color;
}
`;
// compile shaders, link program, look up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
const maxCount = 250000;
const ids = new Float32Array(maxCount);
for (let i = 0; i < ids.length; ++i) {
ids[i] = i;
}
const x = 16 / 300 * 2;
const y = 16 / 150 * 2;
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
position: {
numComponents: 2,
data: [
-x, -y,
x, -y,
-x, y,
-x, y,
x, -y,
x, y,
],
},
texcoord: [
0, 1,
1, 1,
0, 0,
0, 0,
1, 1,
1, 0,
],
id: {
numComponents: 1,
data: ids,
divisor: 1,
}
});
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
{
const ctx = document.createElement('canvas').getContext('2d');
ctx.canvas.width = 32;
ctx.canvas.height = 32;
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(16, 16, 15, 0, Math.PI * 2);
ctx.fill();
const tex = twgl.createTexture(gl, { src: ctx.canvas });
}
const fpsElem = document.querySelector('#fps');
const countElem = document.querySelector('#count');
let count;
function getCount() {
count = Math.min(maxCount, parseInt(countElem.value));
}
countElem.addEventListener('input', getCount);
getCount();
const maxHistory = 60;
const fpsHistory = new Array(maxHistory).fill(0);
let historyNdx = 0;
let historyTotal = 0;
let then = 0;
function render(now) {
const deltaTime = now - then;
then = now;
historyTotal += deltaTime - fpsHistory[historyNdx];
fpsHistory[historyNdx] = deltaTime;
historyNdx = (historyNdx + 1) % maxHistory;
fpsElem.textContent = (1000 / (historyTotal / maxHistory)).toFixed(1);
gl.useProgram(programInfo.program);
twgl.setUniforms(programInfo, {time: now * 0.001});
ext.drawArraysInstancedANGLE(gl.TRIANGLES, 0, 6, count);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
canvas { display: block; border: 1px solid black; }
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
<canvas></canvas>
<div>fps: <span id="fps"></span></div>
<div>count: <input type="number" id="count" min="0" max="1000000" value="25000"></div>
并且我使用重复几何而不是实例化以 60fps 获得相同的性能。这让我感到惊讶,因为 7-8 年前,当我测试重复几何时,速度提高了 20-30%。这是因为现在有了更好的 GPU 还是更好的驱动程序,还是我不知道的原因。
function main() {
const gl = document.querySelector('canvas').getContext('webgl');
const vs = `
attribute float id;
attribute vec4 position;
attribute vec2 texcoord;
uniform float time;
varying vec2 v_texcoord;
varying vec4 v_color;
void main() {
float o = id + time;
gl_Position = position + vec4(
vec2(
fract(o * 0.1373),
fract(o * 0.5127)) * 2.0 - 1.0,
0, 0);
v_texcoord = texcoord;
v_color = vec4(fract(vec3(id) * vec3(0.127, 0.373, 0.513)), 1);
}`;
const fs = `
precision mediump float;
varying vec2 v_texcoord;
varying vec4 v_color;
uniform sampler2D tex;
void main() {
gl_FragColor = texture2D(tex, v_texcoord) * v_color;
}
`;
// compile shaders, link program, look up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
const maxCount = 250000;
const x = 16 / 300 * 2;
const y = 16 / 150 * 2;
const quadPositions = [
-x, -y,
x, -y,
-x, y,
-x, y,
x, -y,
x, y,
];
const quadTexcoords = [
0, 1,
1, 1,
0, 0,
0, 0,
1, 1,
1, 0,
];
const positions = new Float32Array(maxCount * 2 * 6);
const texcoords = new Float32Array(maxCount * 2 * 6);
for (let i = 0; i < maxCount; ++i) {
const off = i * 2 * 6;
positions.set(quadPositions, off);
texcoords.set(quadTexcoords, off);
}
const ids = new Float32Array(maxCount * 6);
for (let i = 0; i < ids.length; ++i) {
ids[i] = i / 6 | 0;
}
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
position: {
numComponents: 2,
data: positions,
},
texcoord: texcoords,
id: {
numComponents: 1,
data: ids,
}
});
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
{
const ctx = document.createElement('canvas').getContext('2d');
ctx.canvas.width = 32;
ctx.canvas.height = 32;
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(16, 16, 15, 0, Math.PI * 2);
ctx.fill();
const tex = twgl.createTexture(gl, { src: ctx.canvas });
}
const fpsElem = document.querySelector('#fps');
const countElem = document.querySelector('#count');
let count;
function getCount() {
count = Math.min(maxCount, parseInt(countElem.value));
}
countElem.addEventListener('input', getCount);
getCount();
const maxHistory = 60;
const fpsHistory = new Array(maxHistory).fill(0);
let historyNdx = 0;
let historyTotal = 0;
let then = 0;
function render(now) {
const deltaTime = now - then;
then = now;
historyTotal += deltaTime - fpsHistory[historyNdx];
fpsHistory[historyNdx] = deltaTime;
historyNdx = (historyNdx + 1) % maxHistory;
fpsElem.textContent = (1000 / (historyTotal / maxHistory)).toFixed(1);
gl.useProgram(programInfo.program);
twgl.setUniforms(programInfo, {time: now * 0.001});
gl.drawArrays(gl.TRIANGLES, 0, 6 * count);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
canvas { display: block; border: 1px solid black; }
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
<canvas></canvas>
<div>fps: <span id="fps"></span></div>
<div>count: <input type="number" id="count" min="0" max="1000000" value="25000"></div>
接下来是纹理或在片段着色器中计算圆。
function main() {
const gl = document.querySelector('canvas').getContext('webgl');
const ext = gl.getExtension('ANGLE_instanced_arrays');
if (!ext) {
return alert('need ANGLE_instanced_arrays');
}
twgl.addExtensionsToContext(gl);
const vs = `
attribute float id;
attribute vec4 position;
attribute vec2 texcoord;
uniform float time;
varying vec2 v_texcoord;
varying vec4 v_color;
void main() {
float o = id + time;
gl_Position = position + vec4(
vec2(
fract(o * 0.1373),
fract(o * 0.5127)) * 2.0 - 1.0,
0, 0);
v_texcoord = texcoord;
v_color = vec4(fract(vec3(id) * vec3(0.127, 0.373, 0.513)), 1);
}`;
const fs = `
precision mediump float;
varying vec2 v_texcoord;
varying vec4 v_color;
void main() {
gl_FragColor = mix(
v_color,
vec4(0),
step(1.0, length(v_texcoord.xy * 2. - 1.)));
}
`;
// compile shaders, link program, look up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
const maxCount = 250000;
const ids = new Float32Array(maxCount);
for (let i = 0; i < ids.length; ++i) {
ids[i] = i;
}
const x = 16 / 300 * 2;
const y = 16 / 150 * 2;
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
position: {
numComponents: 2,
data: [
-x, -y,
x, -y,
-x, y,
-x, y,
x, -y,
x, y,
],
},
texcoord: [
0, 1,
1, 1,
0, 0,
0, 0,
1, 1,
1, 0,
],
id: {
numComponents: 1,
data: ids,
divisor: 1,
}
});
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
const fpsElem = document.querySelector('#fps');
const countElem = document.querySelector('#count');
let count;
function getCount() {
count = Math.min(maxCount, parseInt(countElem.value));
}
countElem.addEventListener('input', getCount);
getCount();
const maxHistory = 60;
const fpsHistory = new Array(maxHistory).fill(0);
let historyNdx = 0;
let historyTotal = 0;
let then = 0;
function render(now) {
const deltaTime = now - then;
then = now;
historyTotal += deltaTime - fpsHistory[historyNdx];
fpsHistory[historyNdx] = deltaTime;
historyNdx = (historyNdx + 1) % maxHistory;
fpsElem.textContent = (1000 / (historyTotal / maxHistory)).toFixed(1);
gl.useProgram(programInfo.program);
twgl.setUniforms(programInfo, {time: now * 0.001});
ext.drawArraysInstancedANGLE(gl.TRIANGLES, 0, 6, count);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
canvas { display: block; border: 1px solid black; }
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
<canvas></canvas>
<div>fps: <span id="fps"></span></div>
<div>count: <input type="number" id="count" min="0" max="1000000" value="25000"></div>
我没有发现任何可测量的差异。试试你的圈子功能
function main() {
const gl = document.querySelector('canvas').getContext('webgl');
const ext = gl.getExtension('ANGLE_instanced_arrays');
if (!ext) {
return alert('need ANGLE_instanced_arrays');
}
twgl.addExtensionsToContext(gl);
const vs = `
attribute float id;
attribute vec4 position;
attribute vec2 texcoord;
uniform float time;
varying vec2 v_texcoord;
varying vec4 v_color;
void main() {
float o = id + time;
gl_Position = position + vec4(
vec2(
fract(o * 0.1373),
fract(o * 0.5127)) * 2.0 - 1.0,
0, 0);
v_texcoord = texcoord;
v_color = vec4(fract(vec3(id) * vec3(0.127, 0.373, 0.513)), 1);
}`;
const fs = `
precision mediump float;
varying vec2 v_texcoord;
varying vec4 v_color;
float circle(in vec2 st, in float radius) {
vec2 dist = st - vec2(0.5);
return 1.0 - smoothstep(
radius - (radius * 0.01),
radius +(radius * 0.01),
dot(dist, dist) * 4.0);
}
void main() {
gl_FragColor = mix(
vec4(0),
v_color,
circle(v_texcoord, 1.0));
}
`;
// compile shaders, link program, look up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
const maxCount = 250000;
const ids = new Float32Array(maxCount);
for (let i = 0; i < ids.length; ++i) {
ids[i] = i;
}
const x = 16 / 300 * 2;
const y = 16 / 150 * 2;
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
position: {
numComponents: 2,
data: [
-x, -y,
x, -y,
-x, y,
-x, y,
x, -y,
x, y,
],
},
texcoord: [
0, 1,
1, 1,
0, 0,
0, 0,
1, 1,
1, 0,
],
id: {
numComponents: 1,
data: ids,
divisor: 1,
}
});
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
const fpsElem = document.querySelector('#fps');
const countElem = document.querySelector('#count');
let count;
function getCount() {
count = Math.min(maxCount, parseInt(countElem.value));
}
countElem.addEventListener('input', getCount);
getCount();
const maxHistory = 60;
const fpsHistory = new Array(maxHistory).fill(0);
let historyNdx = 0;
let historyTotal = 0;
let then = 0;
function render(now) {
const deltaTime = now - then;
then = now;
historyTotal += deltaTime - fpsHistory[historyNdx];
fpsHistory[historyNdx] = deltaTime;
historyNdx = (historyNdx + 1) % maxHistory;
fpsElem.textContent = (1000 / (historyTotal / maxHistory)).toFixed(1);
gl.useProgram(programInfo.program);
twgl.setUniforms(programInfo, {time: now * 0.001});
ext.drawArraysInstancedANGLE(gl.TRIANGLES, 0, 6, count);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
canvas { display: block; border: 1px solid black; }
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
<canvas></canvas>
<div>fps: <span id="fps"></span></div>
<div>count: <input type="number" id="count" min="0" max="1000000" value="25000"></div>
我再次没有得到可测量的差异。注意:就像我上面说的,我在 WebGL 中得到了非常不一致的结果。当我 运行 第一次测试时,我以 60fps 的速度获得了 28k。当我 运行 第二次我得到了 23k。我很惊讶,因为我预计第二个会更快,所以我 运行 又是第一个,但只得到了 23k。最后一个我得到了 29k 并且再次感到惊讶但后来我回去做了前一个并得到了 29k。基本上这意味着在 WebGL 中测试时序几乎是不可能的。鉴于一切都是多进程的,因此有太多活动部件,因此似乎不可能获得恒定的结果。
可以尝试丢弃
function main() {
const gl = document.querySelector('canvas').getContext('webgl');
const ext = gl.getExtension('ANGLE_instanced_arrays');
if (!ext) {
return alert('need ANGLE_instanced_arrays');
}
twgl.addExtensionsToContext(gl);
const vs = `
attribute float id;
attribute vec4 position;
attribute vec2 texcoord;
uniform float time;
varying vec2 v_texcoord;
varying vec4 v_color;
void main() {
float o = id + time;
gl_Position = position + vec4(
vec2(
fract(o * 0.1373),
fract(o * 0.5127)) * 2.0 - 1.0,
0, 0);
v_texcoord = texcoord;
v_color = vec4(fract(vec3(id) * vec3(0.127, 0.373, 0.513)), 1);
}`;
const fs = `
precision mediump float;
varying vec2 v_texcoord;
varying vec4 v_color;
float circle(in vec2 st, in float radius) {
vec2 dist = st - vec2(0.5);
return 1.0 - smoothstep(
radius - (radius * 0.01),
radius +(radius * 0.01),
dot(dist, dist) * 4.0);
}
void main() {
if (circle(v_texcoord, 1.0) < 0.5) {
discard;
}
gl_FragColor = v_color;
}
`;
// compile shaders, link program, look up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
const maxCount = 250000;
const ids = new Float32Array(maxCount);
for (let i = 0; i < ids.length; ++i) {
ids[i] = i;
}
const x = 16 / 300 * 2;
const y = 16 / 150 * 2;
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
position: {
numComponents: 2,
data: [
-x, -y,
x, -y,
-x, y,
-x, y,
x, -y,
x, y,
],
},
texcoord: [
0, 1,
1, 1,
0, 0,
0, 0,
1, 1,
1, 0,
],
id: {
numComponents: 1,
data: ids,
divisor: 1,
}
});
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
const fpsElem = document.querySelector('#fps');
const countElem = document.querySelector('#count');
let count;
function getCount() {
count = Math.min(maxCount, parseInt(countElem.value));
}
countElem.addEventListener('input', getCount);
getCount();
const maxHistory = 60;
const fpsHistory = new Array(maxHistory).fill(0);
let historyNdx = 0;
let historyTotal = 0;
let then = 0;
function render(now) {
const deltaTime = now - then;
then = now;
historyTotal += deltaTime - fpsHistory[historyNdx];
fpsHistory[historyNdx] = deltaTime;
historyNdx = (historyNdx + 1) % maxHistory;
fpsElem.textContent = (1000 / (historyTotal / maxHistory)).toFixed(1);
gl.useProgram(programInfo.program);
twgl.setUniforms(programInfo, {time: now * 0.001});
ext.drawArraysInstancedANGLE(gl.TRIANGLES, 0, 6, count);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
canvas { display: block; border: 1px solid black; }
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
<canvas></canvas>
<div>fps: <span id="fps"></span></div>
<div>count: <input type="number" id="count" min="0" max="1000000" value="25000"></div>
鉴于时间不一致,我不能确定,但我的印象是丢弃速度较慢。 IIRC 丢弃很慢,因为没有丢弃,GPU 甚至在执行片段着色器之前就知道它将更新 z 缓冲区,而丢弃它直到着色器执行后才知道,并且这种差异意味着某些事情不能也要优化。
我要到此为止了,因为要尝试的组合太多了。
我们可以尝试融合。混合通常也较慢,因为它必须混合(阅读背景)但它比丢弃慢吗?我不知道。
你有开启深度测试吗?如果是这样,那么绘制顺序将很重要。
要测试的另一件事是使用非四边形,如六边形或八边形,因为这会 运行 通过片段着色器减少像素。我怀疑您可能需要将圆圈变大才能看到,但如果我们有一个 100x100 像素的四边形,那就是 10k 像素。如果我们有大约 pi*r^2 或 ~7853 或少 21% 像素的完美圆几何。六边形将约为 8740 像素或减少 11%。介于两者之间的八角形。少画 11% 到 21% 的像素通常是一个胜利,但当然,对于六边形,您要多画 3 倍的三角形,对于八边形,多画 4 倍。您基本上必须测试所有这些情况。
这指出了另一个问题,我相信在更大的 canvas 上使用更大的圆圈会得到不同的相对结果,因为每个圆圈的像素更多,因此对于任何给定数量的圆圈绘制更多% 的时间将用于绘制像素和更少的计算顶点 and/or 更少的时间重新启动 GPU 以绘制下一个圆。
更新
在 Chrome 与 Firefox 上进行测试我在同一台机器上的 Chrome 中的所有情况下都得到了 60k-66k。鉴于 WebGL 本身几乎什么都不做,不知道为什么差异如此之大。所有 4 个测试每帧只有一个绘制调用。但无论如何,至少截至 2019 年 10 月 Chrome 在这种特殊情况下比 Firefox
快两倍多
一个想法是我有一台双 GPU 笔记本电脑。当您创建上下文时,您可以通过传入 powerPreference
上下文创建属性来告诉 WebGL 您的目标是什么
const gl = document.createContext('webgl', {
powerPreference: 'high-performance',
});
选项有'default'、'low-power'、'high-performance'。 'default' 意味着 "let the browser decide" 但最终所有这些都意味着 "let the browser decide"。无论如何,上面的设置对我来说并没有改变 firefox 中的任何内容。
我目前正在绘制数千个圆圈,实例化一个 circle geometry(许多三角形)。
或者,我可以简单地实例化一个四边形(2 个三角形),但使用距离函数和 discard
.
哪种方法会更快? -- 绘制许多三角形是否比在片段着色器中完成的计算更昂贵?
最快的方法可能取决于 GPU 和许多其他因素,例如您如何绘制圆、2D、3D、是否混合它们、是否使用 z 缓冲区等...但是在一般来说,三角形越少越快,像素越少越快。所以....,我们能做的只有努力了。
首先让我们只绘制带纹理的四边形,不进行混合。首先,我似乎总是从 WebGL 获得不一致的性能,但在我的 GPU 测试中,我在这个 300x150 canvas 中使用实例化
以 60fps 获得 20k-30k 四边形function main() {
const gl = document.querySelector('canvas').getContext('webgl');
const ext = gl.getExtension('ANGLE_instanced_arrays');
if (!ext) {
return alert('need ANGLE_instanced_arrays');
}
twgl.addExtensionsToContext(gl);
const vs = `
attribute float id;
attribute vec4 position;
attribute vec2 texcoord;
uniform float time;
varying vec2 v_texcoord;
varying vec4 v_color;
void main() {
float o = id + time;
gl_Position = position + vec4(
vec2(
fract(o * 0.1373),
fract(o * 0.5127)) * 2.0 - 1.0,
0, 0);
v_texcoord = texcoord;
v_color = vec4(fract(vec3(id) * vec3(0.127, 0.373, 0.513)), 1);
}`;
const fs = `
precision mediump float;
varying vec2 v_texcoord;
varying vec4 v_color;
uniform sampler2D tex;
void main() {
gl_FragColor = texture2D(tex, v_texcoord) * v_color;
}
`;
// compile shaders, link program, look up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
const maxCount = 250000;
const ids = new Float32Array(maxCount);
for (let i = 0; i < ids.length; ++i) {
ids[i] = i;
}
const x = 16 / 300 * 2;
const y = 16 / 150 * 2;
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
position: {
numComponents: 2,
data: [
-x, -y,
x, -y,
-x, y,
-x, y,
x, -y,
x, y,
],
},
texcoord: [
0, 1,
1, 1,
0, 0,
0, 0,
1, 1,
1, 0,
],
id: {
numComponents: 1,
data: ids,
divisor: 1,
}
});
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
{
const ctx = document.createElement('canvas').getContext('2d');
ctx.canvas.width = 32;
ctx.canvas.height = 32;
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(16, 16, 15, 0, Math.PI * 2);
ctx.fill();
const tex = twgl.createTexture(gl, { src: ctx.canvas });
}
const fpsElem = document.querySelector('#fps');
const countElem = document.querySelector('#count');
let count;
function getCount() {
count = Math.min(maxCount, parseInt(countElem.value));
}
countElem.addEventListener('input', getCount);
getCount();
const maxHistory = 60;
const fpsHistory = new Array(maxHistory).fill(0);
let historyNdx = 0;
let historyTotal = 0;
let then = 0;
function render(now) {
const deltaTime = now - then;
then = now;
historyTotal += deltaTime - fpsHistory[historyNdx];
fpsHistory[historyNdx] = deltaTime;
historyNdx = (historyNdx + 1) % maxHistory;
fpsElem.textContent = (1000 / (historyTotal / maxHistory)).toFixed(1);
gl.useProgram(programInfo.program);
twgl.setUniforms(programInfo, {time: now * 0.001});
ext.drawArraysInstancedANGLE(gl.TRIANGLES, 0, 6, count);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
canvas { display: block; border: 1px solid black; }
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
<canvas></canvas>
<div>fps: <span id="fps"></span></div>
<div>count: <input type="number" id="count" min="0" max="1000000" value="25000"></div>
并且我使用重复几何而不是实例化以 60fps 获得相同的性能。这让我感到惊讶,因为 7-8 年前,当我测试重复几何时,速度提高了 20-30%。这是因为现在有了更好的 GPU 还是更好的驱动程序,还是我不知道的原因。
function main() {
const gl = document.querySelector('canvas').getContext('webgl');
const vs = `
attribute float id;
attribute vec4 position;
attribute vec2 texcoord;
uniform float time;
varying vec2 v_texcoord;
varying vec4 v_color;
void main() {
float o = id + time;
gl_Position = position + vec4(
vec2(
fract(o * 0.1373),
fract(o * 0.5127)) * 2.0 - 1.0,
0, 0);
v_texcoord = texcoord;
v_color = vec4(fract(vec3(id) * vec3(0.127, 0.373, 0.513)), 1);
}`;
const fs = `
precision mediump float;
varying vec2 v_texcoord;
varying vec4 v_color;
uniform sampler2D tex;
void main() {
gl_FragColor = texture2D(tex, v_texcoord) * v_color;
}
`;
// compile shaders, link program, look up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
const maxCount = 250000;
const x = 16 / 300 * 2;
const y = 16 / 150 * 2;
const quadPositions = [
-x, -y,
x, -y,
-x, y,
-x, y,
x, -y,
x, y,
];
const quadTexcoords = [
0, 1,
1, 1,
0, 0,
0, 0,
1, 1,
1, 0,
];
const positions = new Float32Array(maxCount * 2 * 6);
const texcoords = new Float32Array(maxCount * 2 * 6);
for (let i = 0; i < maxCount; ++i) {
const off = i * 2 * 6;
positions.set(quadPositions, off);
texcoords.set(quadTexcoords, off);
}
const ids = new Float32Array(maxCount * 6);
for (let i = 0; i < ids.length; ++i) {
ids[i] = i / 6 | 0;
}
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
position: {
numComponents: 2,
data: positions,
},
texcoord: texcoords,
id: {
numComponents: 1,
data: ids,
}
});
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
{
const ctx = document.createElement('canvas').getContext('2d');
ctx.canvas.width = 32;
ctx.canvas.height = 32;
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(16, 16, 15, 0, Math.PI * 2);
ctx.fill();
const tex = twgl.createTexture(gl, { src: ctx.canvas });
}
const fpsElem = document.querySelector('#fps');
const countElem = document.querySelector('#count');
let count;
function getCount() {
count = Math.min(maxCount, parseInt(countElem.value));
}
countElem.addEventListener('input', getCount);
getCount();
const maxHistory = 60;
const fpsHistory = new Array(maxHistory).fill(0);
let historyNdx = 0;
let historyTotal = 0;
let then = 0;
function render(now) {
const deltaTime = now - then;
then = now;
historyTotal += deltaTime - fpsHistory[historyNdx];
fpsHistory[historyNdx] = deltaTime;
historyNdx = (historyNdx + 1) % maxHistory;
fpsElem.textContent = (1000 / (historyTotal / maxHistory)).toFixed(1);
gl.useProgram(programInfo.program);
twgl.setUniforms(programInfo, {time: now * 0.001});
gl.drawArrays(gl.TRIANGLES, 0, 6 * count);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
canvas { display: block; border: 1px solid black; }
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
<canvas></canvas>
<div>fps: <span id="fps"></span></div>
<div>count: <input type="number" id="count" min="0" max="1000000" value="25000"></div>
接下来是纹理或在片段着色器中计算圆。
function main() {
const gl = document.querySelector('canvas').getContext('webgl');
const ext = gl.getExtension('ANGLE_instanced_arrays');
if (!ext) {
return alert('need ANGLE_instanced_arrays');
}
twgl.addExtensionsToContext(gl);
const vs = `
attribute float id;
attribute vec4 position;
attribute vec2 texcoord;
uniform float time;
varying vec2 v_texcoord;
varying vec4 v_color;
void main() {
float o = id + time;
gl_Position = position + vec4(
vec2(
fract(o * 0.1373),
fract(o * 0.5127)) * 2.0 - 1.0,
0, 0);
v_texcoord = texcoord;
v_color = vec4(fract(vec3(id) * vec3(0.127, 0.373, 0.513)), 1);
}`;
const fs = `
precision mediump float;
varying vec2 v_texcoord;
varying vec4 v_color;
void main() {
gl_FragColor = mix(
v_color,
vec4(0),
step(1.0, length(v_texcoord.xy * 2. - 1.)));
}
`;
// compile shaders, link program, look up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
const maxCount = 250000;
const ids = new Float32Array(maxCount);
for (let i = 0; i < ids.length; ++i) {
ids[i] = i;
}
const x = 16 / 300 * 2;
const y = 16 / 150 * 2;
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
position: {
numComponents: 2,
data: [
-x, -y,
x, -y,
-x, y,
-x, y,
x, -y,
x, y,
],
},
texcoord: [
0, 1,
1, 1,
0, 0,
0, 0,
1, 1,
1, 0,
],
id: {
numComponents: 1,
data: ids,
divisor: 1,
}
});
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
const fpsElem = document.querySelector('#fps');
const countElem = document.querySelector('#count');
let count;
function getCount() {
count = Math.min(maxCount, parseInt(countElem.value));
}
countElem.addEventListener('input', getCount);
getCount();
const maxHistory = 60;
const fpsHistory = new Array(maxHistory).fill(0);
let historyNdx = 0;
let historyTotal = 0;
let then = 0;
function render(now) {
const deltaTime = now - then;
then = now;
historyTotal += deltaTime - fpsHistory[historyNdx];
fpsHistory[historyNdx] = deltaTime;
historyNdx = (historyNdx + 1) % maxHistory;
fpsElem.textContent = (1000 / (historyTotal / maxHistory)).toFixed(1);
gl.useProgram(programInfo.program);
twgl.setUniforms(programInfo, {time: now * 0.001});
ext.drawArraysInstancedANGLE(gl.TRIANGLES, 0, 6, count);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
canvas { display: block; border: 1px solid black; }
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
<canvas></canvas>
<div>fps: <span id="fps"></span></div>
<div>count: <input type="number" id="count" min="0" max="1000000" value="25000"></div>
我没有发现任何可测量的差异。试试你的圈子功能
function main() {
const gl = document.querySelector('canvas').getContext('webgl');
const ext = gl.getExtension('ANGLE_instanced_arrays');
if (!ext) {
return alert('need ANGLE_instanced_arrays');
}
twgl.addExtensionsToContext(gl);
const vs = `
attribute float id;
attribute vec4 position;
attribute vec2 texcoord;
uniform float time;
varying vec2 v_texcoord;
varying vec4 v_color;
void main() {
float o = id + time;
gl_Position = position + vec4(
vec2(
fract(o * 0.1373),
fract(o * 0.5127)) * 2.0 - 1.0,
0, 0);
v_texcoord = texcoord;
v_color = vec4(fract(vec3(id) * vec3(0.127, 0.373, 0.513)), 1);
}`;
const fs = `
precision mediump float;
varying vec2 v_texcoord;
varying vec4 v_color;
float circle(in vec2 st, in float radius) {
vec2 dist = st - vec2(0.5);
return 1.0 - smoothstep(
radius - (radius * 0.01),
radius +(radius * 0.01),
dot(dist, dist) * 4.0);
}
void main() {
gl_FragColor = mix(
vec4(0),
v_color,
circle(v_texcoord, 1.0));
}
`;
// compile shaders, link program, look up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
const maxCount = 250000;
const ids = new Float32Array(maxCount);
for (let i = 0; i < ids.length; ++i) {
ids[i] = i;
}
const x = 16 / 300 * 2;
const y = 16 / 150 * 2;
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
position: {
numComponents: 2,
data: [
-x, -y,
x, -y,
-x, y,
-x, y,
x, -y,
x, y,
],
},
texcoord: [
0, 1,
1, 1,
0, 0,
0, 0,
1, 1,
1, 0,
],
id: {
numComponents: 1,
data: ids,
divisor: 1,
}
});
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
const fpsElem = document.querySelector('#fps');
const countElem = document.querySelector('#count');
let count;
function getCount() {
count = Math.min(maxCount, parseInt(countElem.value));
}
countElem.addEventListener('input', getCount);
getCount();
const maxHistory = 60;
const fpsHistory = new Array(maxHistory).fill(0);
let historyNdx = 0;
let historyTotal = 0;
let then = 0;
function render(now) {
const deltaTime = now - then;
then = now;
historyTotal += deltaTime - fpsHistory[historyNdx];
fpsHistory[historyNdx] = deltaTime;
historyNdx = (historyNdx + 1) % maxHistory;
fpsElem.textContent = (1000 / (historyTotal / maxHistory)).toFixed(1);
gl.useProgram(programInfo.program);
twgl.setUniforms(programInfo, {time: now * 0.001});
ext.drawArraysInstancedANGLE(gl.TRIANGLES, 0, 6, count);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
canvas { display: block; border: 1px solid black; }
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
<canvas></canvas>
<div>fps: <span id="fps"></span></div>
<div>count: <input type="number" id="count" min="0" max="1000000" value="25000"></div>
我再次没有得到可测量的差异。注意:就像我上面说的,我在 WebGL 中得到了非常不一致的结果。当我 运行 第一次测试时,我以 60fps 的速度获得了 28k。当我 运行 第二次我得到了 23k。我很惊讶,因为我预计第二个会更快,所以我 运行 又是第一个,但只得到了 23k。最后一个我得到了 29k 并且再次感到惊讶但后来我回去做了前一个并得到了 29k。基本上这意味着在 WebGL 中测试时序几乎是不可能的。鉴于一切都是多进程的,因此有太多活动部件,因此似乎不可能获得恒定的结果。
可以尝试丢弃
function main() {
const gl = document.querySelector('canvas').getContext('webgl');
const ext = gl.getExtension('ANGLE_instanced_arrays');
if (!ext) {
return alert('need ANGLE_instanced_arrays');
}
twgl.addExtensionsToContext(gl);
const vs = `
attribute float id;
attribute vec4 position;
attribute vec2 texcoord;
uniform float time;
varying vec2 v_texcoord;
varying vec4 v_color;
void main() {
float o = id + time;
gl_Position = position + vec4(
vec2(
fract(o * 0.1373),
fract(o * 0.5127)) * 2.0 - 1.0,
0, 0);
v_texcoord = texcoord;
v_color = vec4(fract(vec3(id) * vec3(0.127, 0.373, 0.513)), 1);
}`;
const fs = `
precision mediump float;
varying vec2 v_texcoord;
varying vec4 v_color;
float circle(in vec2 st, in float radius) {
vec2 dist = st - vec2(0.5);
return 1.0 - smoothstep(
radius - (radius * 0.01),
radius +(radius * 0.01),
dot(dist, dist) * 4.0);
}
void main() {
if (circle(v_texcoord, 1.0) < 0.5) {
discard;
}
gl_FragColor = v_color;
}
`;
// compile shaders, link program, look up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
const maxCount = 250000;
const ids = new Float32Array(maxCount);
for (let i = 0; i < ids.length; ++i) {
ids[i] = i;
}
const x = 16 / 300 * 2;
const y = 16 / 150 * 2;
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
position: {
numComponents: 2,
data: [
-x, -y,
x, -y,
-x, y,
-x, y,
x, -y,
x, y,
],
},
texcoord: [
0, 1,
1, 1,
0, 0,
0, 0,
1, 1,
1, 0,
],
id: {
numComponents: 1,
data: ids,
divisor: 1,
}
});
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
const fpsElem = document.querySelector('#fps');
const countElem = document.querySelector('#count');
let count;
function getCount() {
count = Math.min(maxCount, parseInt(countElem.value));
}
countElem.addEventListener('input', getCount);
getCount();
const maxHistory = 60;
const fpsHistory = new Array(maxHistory).fill(0);
let historyNdx = 0;
let historyTotal = 0;
let then = 0;
function render(now) {
const deltaTime = now - then;
then = now;
historyTotal += deltaTime - fpsHistory[historyNdx];
fpsHistory[historyNdx] = deltaTime;
historyNdx = (historyNdx + 1) % maxHistory;
fpsElem.textContent = (1000 / (historyTotal / maxHistory)).toFixed(1);
gl.useProgram(programInfo.program);
twgl.setUniforms(programInfo, {time: now * 0.001});
ext.drawArraysInstancedANGLE(gl.TRIANGLES, 0, 6, count);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
canvas { display: block; border: 1px solid black; }
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
<canvas></canvas>
<div>fps: <span id="fps"></span></div>
<div>count: <input type="number" id="count" min="0" max="1000000" value="25000"></div>
鉴于时间不一致,我不能确定,但我的印象是丢弃速度较慢。 IIRC 丢弃很慢,因为没有丢弃,GPU 甚至在执行片段着色器之前就知道它将更新 z 缓冲区,而丢弃它直到着色器执行后才知道,并且这种差异意味着某些事情不能也要优化。
我要到此为止了,因为要尝试的组合太多了。
我们可以尝试融合。混合通常也较慢,因为它必须混合(阅读背景)但它比丢弃慢吗?我不知道。
你有开启深度测试吗?如果是这样,那么绘制顺序将很重要。
要测试的另一件事是使用非四边形,如六边形或八边形,因为这会 运行 通过片段着色器减少像素。我怀疑您可能需要将圆圈变大才能看到,但如果我们有一个 100x100 像素的四边形,那就是 10k 像素。如果我们有大约 pi*r^2 或 ~7853 或少 21% 像素的完美圆几何。六边形将约为 8740 像素或减少 11%。介于两者之间的八角形。少画 11% 到 21% 的像素通常是一个胜利,但当然,对于六边形,您要多画 3 倍的三角形,对于八边形,多画 4 倍。您基本上必须测试所有这些情况。
这指出了另一个问题,我相信在更大的 canvas 上使用更大的圆圈会得到不同的相对结果,因为每个圆圈的像素更多,因此对于任何给定数量的圆圈绘制更多% 的时间将用于绘制像素和更少的计算顶点 and/or 更少的时间重新启动 GPU 以绘制下一个圆。
更新
在 Chrome 与 Firefox 上进行测试我在同一台机器上的 Chrome 中的所有情况下都得到了 60k-66k。鉴于 WebGL 本身几乎什么都不做,不知道为什么差异如此之大。所有 4 个测试每帧只有一个绘制调用。但无论如何,至少截至 2019 年 10 月 Chrome 在这种特殊情况下比 Firefox
快两倍多一个想法是我有一台双 GPU 笔记本电脑。当您创建上下文时,您可以通过传入 powerPreference
上下文创建属性来告诉 WebGL 您的目标是什么
const gl = document.createContext('webgl', {
powerPreference: 'high-performance',
});
选项有'default'、'low-power'、'high-performance'。 'default' 意味着 "let the browser decide" 但最终所有这些都意味着 "let the browser decide"。无论如何,上面的设置对我来说并没有改变 firefox 中的任何内容。