WebGL 有没有办法在片段着色器中加载动态缓冲区?
WebGL is there a way to load dynamic buffers in fragment shaders?
我有一个片段着色器,可以根据一组参数绘制弧线。我的想法是让着色器分辨率独立,所以我将圆弧的中心和边界半径作为屏幕上的像素值传递。然后,您可以通过将顶点位置设置为正方形来渲染着色器。这是着色器:
precision mediump float;
#define PI 3.14159265359
#define _2_PI 6.28318530718
#define PI_2 1.57079632679
// inputs
vec2 center = u_resolution / 2.;
vec2 R = vec2( 100., 80. );
float ang1 = 1.0 * PI;
float ang2 = 0.8 * PI;
vec3 color = vec3( 0., 1.0, 0. );
// prog vars
uniform vec2 u_resolution;
float smOOth = 1.3;
vec3 bkgd = vec3( 0.0 ); // will be a sampler
void main () {
// get the dist from the current pixel to the coord.
float r = distance( gl_FragCoord.xy, center );
if ( r < R.x && r > R.y ) {
// If we are in the radius, do some trig to find the angle and normalize
// to
float theta = -( atan( gl_FragCoord.y - center.y,
center.x - gl_FragCoord.x ) ) + PI;
// This is to make sure the angles are clipped at 2 pi, but if you pass
// the values already clipped, then you can safely delete this and make
// the code more efficinent.
ang1 = mod( ang1, _2_PI );
ang2 = mod( ang2, _2_PI );
float angSum = ang1 + ang2;
bool thetaCond;
vec2 thBound; // short for theta bounds: used to calculate smoothing
// at the edges of the circle.
if ( angSum > _2_PI ) {
thBound = vec2( ang2, angSum - _2_PI );
thetaCond = ( theta > ang2 && theta < _2_PI ) ||
( theta < thetaBounds.y );
} else {
thBound = vec2( ang2, angSum );
thetaCond = theta > ang2 && theta < angSum;
}
if ( thetaCond ) {
float angOpMult = 10000. / ( R.x - R.y ) / smOOth;
float opacity = smoothstep( 0.0, 1.0, ( R.x - r ) / smOOth ) -
smoothstep( 1.0, 0.0, ( r - R.y ) / smOOth ) -
smoothstep( 1.0, 0.0, ( theta - thBound.x )
* angOpMult ) -
smoothstep( 1.0, 0.0, ( thBound.y - theta )
* angOpMult );
gl_FragColor = vec4( mix( bkgd, color, opacity ), 1.0 );
} else
discard;
} else
discard;
}
我认为这种绘制圆的方法会产生质量更好的圆,并且比加载一堆顶点和绘制三角形扇形更省事,尽管它可能效率不高。这很好用,但我不只是想画一个固定的圆圈。我想在屏幕上画出我想要的任何圆圈。所以我有一个想法将 'inputs' 设置为 varyings 并将带有参数的缓冲区传递给给定边界正方形的每个顶点。所以我的顶点着色器看起来像这样:
attribute vec2 a_square;
attribute vec2 a_center;
attribute vec2 a_R;
attribute float a_ang1;
attribute float a_ang2;
attribute vec3 a_color;
varying vec2 center;
varying vec2 R;
varying float ang1;
varying float ang2;
varying vec3 color;
void main () {
gl_Position = vec4( a_square, 0.0, 1.0 );
center = a_center;
R = a_R;
ang1 = a_ang1;
ang2 = a_ang2;
color = a_color;
}
'a_square' 只是圆所在的边界正方形的顶点。
接下来,我为一个测试循环(在 JS 中)定义一个输入缓冲区。这样做的问题之一是圆参数必须为每个顶点重复,对于一个盒子,这意味着四次。 'pw' 和 'ph' 分别是 canvas 的宽度和高度。
var circleData = new Float32Array( [
pw / 2, ph / 2,
440, 280,
Math.PI * 1.2, Math.PI * 0.2,
1000, 0, 0,
pw/2,ph/2,440,280,Math.PI*1.2,Math.PI*0.2,1000,0,0,
pw/2,ph/2,440,280,Math.PI*1.2,Math.PI*0.2,1000,0,0,
pw/2,ph/2,440,280,Math.PI*1.2,Math.PI*0.2,1000,0,0,
] );
然后我只需将我的数据加载到 gl 缓冲区 (circleBuffer) 并将适当的属性绑定到它。
gl.bindBuffer( gl.ARRAY_BUFFER, bkgd.circleBuffer );
gl.vertexAttribPointer( bkgd.aCenter, 2, gl.FLOAT, false, 0 * floatSiz, 9 * floatSiz );
gl.enableVertexAttribArray( bkgd.aCenter );
gl.vertexAttribPointer( bkgd.aR, 2, gl.FLOAT, false, 2 * floatSiz, 9 * floatSiz );
gl.enableVertexAttribArray( bkgd.aR );
gl.vertexAttribPointer( bkgd.aAng1, 1, gl.FLOAT, false, 4 * floatSiz, 9 * floatSiz );
gl.enableVertexAttribArray( bkgd.aAng1 );
gl.vertexAttribPointer( bkgd.aAng2, 1, gl.FLOAT, false, 5 * floatSiz, 9 * floatSiz );
gl.enableVertexAttribArray( bkgd.aAng2 );
gl.vertexAttribPointer( bkgd.aColor, 3, gl.FLOAT, false, 6 * floatSiz, 9 * floatSiz );
gl.enableVertexAttribArray( bkgd.aColor );
当我加载我的页面时,我确实看到了一个圆圈,但在我看来,半径是唯一实际反映任何类型的响应能力的属性。角度、中心和颜色没有反映它们应有的值,我完全不知道为什么半径是唯一实际有效的东西。
尽管如此,这似乎是一种将参数加载到片段着色器以绘制圆的低效方法,因为我必须重新加载框的每个顶点的值,然后 GPU 无缘无故地插入这些值.有没有更好的方法将属性缓冲区之类的东西传递给片段着色器,或者通常以这种方式使用片段着色器?或者我应该只使用顶点来绘制我的圆圈?
如果您只绘制圆圈,则可以使用实例化绘图来避免重复信息。
查看此问答:
实例化允许您使用一些数据每个实例,就像每个圆圈一样。
您还可以使用纹理来存储每圈数据或所有数据。请参阅此问答:How to do batching without UBOs?
效率高低取决于 GPU/driver/OS/Browser。如果您需要绘制 1000 个圆圈,这可能会很有效。大多数应用程序绘制各种各样的东西,所以会选择更通用的解决方案,除非他们有特殊需要绘制 1000 个圆圈。
此外,它可能效率不高,因为您仍在为正方形但不在圆形中的每个像素调用片段着色器。这比使用三角形多调用片段着色器 30%,并且假设您的代码正在绘制适合圆圈的四边形。乍一看,您的实际代码正在绘制完整的 canvas 四边形,这是非常低效的。
我有一个片段着色器,可以根据一组参数绘制弧线。我的想法是让着色器分辨率独立,所以我将圆弧的中心和边界半径作为屏幕上的像素值传递。然后,您可以通过将顶点位置设置为正方形来渲染着色器。这是着色器:
precision mediump float;
#define PI 3.14159265359
#define _2_PI 6.28318530718
#define PI_2 1.57079632679
// inputs
vec2 center = u_resolution / 2.;
vec2 R = vec2( 100., 80. );
float ang1 = 1.0 * PI;
float ang2 = 0.8 * PI;
vec3 color = vec3( 0., 1.0, 0. );
// prog vars
uniform vec2 u_resolution;
float smOOth = 1.3;
vec3 bkgd = vec3( 0.0 ); // will be a sampler
void main () {
// get the dist from the current pixel to the coord.
float r = distance( gl_FragCoord.xy, center );
if ( r < R.x && r > R.y ) {
// If we are in the radius, do some trig to find the angle and normalize
// to
float theta = -( atan( gl_FragCoord.y - center.y,
center.x - gl_FragCoord.x ) ) + PI;
// This is to make sure the angles are clipped at 2 pi, but if you pass
// the values already clipped, then you can safely delete this and make
// the code more efficinent.
ang1 = mod( ang1, _2_PI );
ang2 = mod( ang2, _2_PI );
float angSum = ang1 + ang2;
bool thetaCond;
vec2 thBound; // short for theta bounds: used to calculate smoothing
// at the edges of the circle.
if ( angSum > _2_PI ) {
thBound = vec2( ang2, angSum - _2_PI );
thetaCond = ( theta > ang2 && theta < _2_PI ) ||
( theta < thetaBounds.y );
} else {
thBound = vec2( ang2, angSum );
thetaCond = theta > ang2 && theta < angSum;
}
if ( thetaCond ) {
float angOpMult = 10000. / ( R.x - R.y ) / smOOth;
float opacity = smoothstep( 0.0, 1.0, ( R.x - r ) / smOOth ) -
smoothstep( 1.0, 0.0, ( r - R.y ) / smOOth ) -
smoothstep( 1.0, 0.0, ( theta - thBound.x )
* angOpMult ) -
smoothstep( 1.0, 0.0, ( thBound.y - theta )
* angOpMult );
gl_FragColor = vec4( mix( bkgd, color, opacity ), 1.0 );
} else
discard;
} else
discard;
}
我认为这种绘制圆的方法会产生质量更好的圆,并且比加载一堆顶点和绘制三角形扇形更省事,尽管它可能效率不高。这很好用,但我不只是想画一个固定的圆圈。我想在屏幕上画出我想要的任何圆圈。所以我有一个想法将 'inputs' 设置为 varyings 并将带有参数的缓冲区传递给给定边界正方形的每个顶点。所以我的顶点着色器看起来像这样:
attribute vec2 a_square;
attribute vec2 a_center;
attribute vec2 a_R;
attribute float a_ang1;
attribute float a_ang2;
attribute vec3 a_color;
varying vec2 center;
varying vec2 R;
varying float ang1;
varying float ang2;
varying vec3 color;
void main () {
gl_Position = vec4( a_square, 0.0, 1.0 );
center = a_center;
R = a_R;
ang1 = a_ang1;
ang2 = a_ang2;
color = a_color;
}
'a_square' 只是圆所在的边界正方形的顶点。
接下来,我为一个测试循环(在 JS 中)定义一个输入缓冲区。这样做的问题之一是圆参数必须为每个顶点重复,对于一个盒子,这意味着四次。 'pw' 和 'ph' 分别是 canvas 的宽度和高度。
var circleData = new Float32Array( [
pw / 2, ph / 2,
440, 280,
Math.PI * 1.2, Math.PI * 0.2,
1000, 0, 0,
pw/2,ph/2,440,280,Math.PI*1.2,Math.PI*0.2,1000,0,0,
pw/2,ph/2,440,280,Math.PI*1.2,Math.PI*0.2,1000,0,0,
pw/2,ph/2,440,280,Math.PI*1.2,Math.PI*0.2,1000,0,0,
] );
然后我只需将我的数据加载到 gl 缓冲区 (circleBuffer) 并将适当的属性绑定到它。
gl.bindBuffer( gl.ARRAY_BUFFER, bkgd.circleBuffer );
gl.vertexAttribPointer( bkgd.aCenter, 2, gl.FLOAT, false, 0 * floatSiz, 9 * floatSiz );
gl.enableVertexAttribArray( bkgd.aCenter );
gl.vertexAttribPointer( bkgd.aR, 2, gl.FLOAT, false, 2 * floatSiz, 9 * floatSiz );
gl.enableVertexAttribArray( bkgd.aR );
gl.vertexAttribPointer( bkgd.aAng1, 1, gl.FLOAT, false, 4 * floatSiz, 9 * floatSiz );
gl.enableVertexAttribArray( bkgd.aAng1 );
gl.vertexAttribPointer( bkgd.aAng2, 1, gl.FLOAT, false, 5 * floatSiz, 9 * floatSiz );
gl.enableVertexAttribArray( bkgd.aAng2 );
gl.vertexAttribPointer( bkgd.aColor, 3, gl.FLOAT, false, 6 * floatSiz, 9 * floatSiz );
gl.enableVertexAttribArray( bkgd.aColor );
当我加载我的页面时,我确实看到了一个圆圈,但在我看来,半径是唯一实际反映任何类型的响应能力的属性。角度、中心和颜色没有反映它们应有的值,我完全不知道为什么半径是唯一实际有效的东西。
尽管如此,这似乎是一种将参数加载到片段着色器以绘制圆的低效方法,因为我必须重新加载框的每个顶点的值,然后 GPU 无缘无故地插入这些值.有没有更好的方法将属性缓冲区之类的东西传递给片段着色器,或者通常以这种方式使用片段着色器?或者我应该只使用顶点来绘制我的圆圈?
如果您只绘制圆圈,则可以使用实例化绘图来避免重复信息。
查看此问答:
实例化允许您使用一些数据每个实例,就像每个圆圈一样。
您还可以使用纹理来存储每圈数据或所有数据。请参阅此问答:How to do batching without UBOs?
效率高低取决于 GPU/driver/OS/Browser。如果您需要绘制 1000 个圆圈,这可能会很有效。大多数应用程序绘制各种各样的东西,所以会选择更通用的解决方案,除非他们有特殊需要绘制 1000 个圆圈。 此外,它可能效率不高,因为您仍在为正方形但不在圆形中的每个像素调用片段着色器。这比使用三角形多调用片段着色器 30%,并且假设您的代码正在绘制适合圆圈的四边形。乍一看,您的实际代码正在绘制完整的 canvas 四边形,这是非常低效的。