首页 > 解决方案 > WebGL。为每个像素调用片段着色器的全屏四边形或三角形?

问题描述

在互联网上的许多示例(例如webglfundamentalswebgl-bolerplate)中,作者使用两个三角形覆盖全屏并为画布上的每个像素调用像素着色器。

var canvas, gl, buffer, 
			    vertex_shader, fragment_shader, 
			    currentProgram, vertex_position,
			    timeLocation, resolutionLocation,
			    parameters = {  start_time  : new Date().getTime(), 
			                    time        : 0, 
			                    screenWidth : 0, 
			                    screenHeight: 0 };
			init();
			animate();
 
			function init() {
				vertex_shader = document.getElementById('vs').textContent;
				fragment_shader = document.getElementById('fs').textContent;
				canvas = document.querySelector( 'canvas' );
				try {
					gl = canvas.getContext( 'experimental-webgl' );
				} catch( error ) { }
				if ( !gl ) 
					throw "cannot create webgl context";

				// Create Vertex buffer (2 triangles)
				buffer = gl.createBuffer();
				gl.bindBuffer( gl.ARRAY_BUFFER, buffer );
				gl.bufferData( gl.ARRAY_BUFFER, new Float32Array( [ - 1.0, - 1.0, 1.0, - 1.0, - 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, - 1.0, 1.0 ] ), gl.STATIC_DRAW );
 
				currentProgram = createProgram( vertex_shader, fragment_shader );
				timeLocation = gl.getUniformLocation( currentProgram, 'time' );
				resolutionLocation = gl.getUniformLocation( currentProgram, 'resolution' );
			}
 
			function createProgram( vertex, fragment ) {
				var program = gl.createProgram();
				var vs = createShader( vertex, gl.VERTEX_SHADER );
				var fs = createShader( '#ifdef GL_ES\nprecision highp float;\n#endif\n\n' + fragment, gl.FRAGMENT_SHADER );
 
				if ( vs == null || fs == null ) 
            return null;
 
				gl.attachShader( program, vs );
				gl.attachShader( program, fs );
				gl.deleteShader( vs );
				gl.deleteShader( fs );
				gl.linkProgram( program );
 
				if ( !gl.getProgramParameter( program, gl.LINK_STATUS ) ) {
					alert( "ERROR:\n" +
					"VALIDATE_STATUS: " + gl.getProgramParameter( program, gl.VALIDATE_STATUS ) + "\n" +
					"ERROR: " + gl.getError() + "\n\n" +
					"- Vertex Shader -\n" + vertex + "\n\n" +
					"- Fragment Shader -\n" + fragment );
					return null;
				}
				return program;
			}
 
			function createShader( src, type ) {
				var shader = gl.createShader( type );
				gl.shaderSource( shader, src );
				gl.compileShader( shader );
				if ( !gl.getShaderParameter( shader, gl.COMPILE_STATUS ) ) {
					alert( ( type == gl.VERTEX_SHADER ? "VERTEX" : "FRAGMENT" ) + " SHADER:\n" + gl.getShaderInfoLog( shader ) );
					return null;
				}
				return shader;
			}
 
			function resizeCanvas( event ) {
				if ( canvas.width != canvas.clientWidth ||
					 canvas.height != canvas.clientHeight ) {
					canvas.width = canvas.clientWidth;
					canvas.height = canvas.clientHeight;
					parameters.screenWidth = canvas.width;
					parameters.screenHeight = canvas.height;
					gl.viewport( 0, 0, canvas.width, canvas.height );
				}
			}
 
			function animate() {
				resizeCanvas();
				render();
				requestAnimationFrame( animate );
			}
 
			function render() {
				if ( !currentProgram ) 
            return;
				parameters.time = new Date().getTime() - parameters.start_time;
				gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT );
				gl.useProgram( currentProgram );
				gl.uniform1f( timeLocation, parameters.time / 1000 );
				gl.uniform2f( resolutionLocation, parameters.screenWidth, parameters.screenHeight );
				gl.bindBuffer( gl.ARRAY_BUFFER, buffer );
				gl.vertexAttribPointer( vertex_position, 2, gl.FLOAT, false, 0, 0 );
				gl.enableVertexAttribArray( vertex_position );
				gl.drawArrays( gl.TRIANGLES, 0, 6 );
				gl.disableVertexAttribArray( vertex_position );
			}
html, body {
  background-color: #000000;
  margin: 0px;
  overflow: hidden;
  width: 100%;
  height: 100%;
}
canvas {
  width: 100%;
  height: 100%;
}
<canvas></canvas>
<div id="info"></div> 

<script id="vs" type="x-shader/vertex"> 
  attribute vec3 position;
  
  void main() {
    gl_Position = vec4( position, 1.0 );
  }
</script> 

<script id="fs" type="x-shader/fragment"> 
  uniform float time;
  uniform vec2 resolution;

  void main( void ) {
    vec2 position = - 1.0 + 2.0 * gl_FragCoord.xy / resolution.xy;
    float red = abs( sin( position.x * position.y + time / 5.0 ) );
    float green = abs( sin( position.x * position.y + time / 4.0 ) );
    float blue = abs( sin( position.x * position.y + time / 3.0 ) );
    gl_FragColor = vec4( red, green, blue, 1.0 );
  }
</script>

此代码使用具有 6 个顶点的缓冲区来呈现如下内容:

全屏四边形

这种方法有什么优点吗?

与方法相比,我们渲染一个三角形(3个顶点)覆盖全屏,如下图所示:

全屏三角形

body{
  margin: 0;
  overflow: hidden;
}
<canvas></canvas>

<script type='glsl/vertex'>
  attribute vec2 coords;
  
  void main(void) {
    gl_Position = vec4(coords.xy, 0.0, 1.0);
  }
</script>

<script type='glsl/fragment'>precision highp float;

   uniform vec4 mr;
   
   void main(void) {
     vec2 p = gl_FragCoord.xy;
     vec2 q = (p + p - mr.ba) / mr.b;
     for(int i = 0; i < 13; i++) {
          q = abs(q)/dot(q,q) -  mr.xy/mr.zw;
     }
     gl_FragColor = vec4(q, q.x/q.y, 1.0);
   }
</script>

<script>
  let canvas = document.querySelector('canvas');
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
  let gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
  var h = gl.drawingBufferHeight;
  var w = gl.drawingBufferWidth;
  
  let pid = gl.createProgram();
  shader('glsl/vertex', gl.VERTEX_SHADER);
  shader('glsl/fragment', gl.FRAGMENT_SHADER);
  gl.linkProgram(pid);
  gl.useProgram(pid);

  let array = new Float32Array([-1,  3, -1, -1, 3, -1]);
  gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
  gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW);

  let al = gl.getAttribLocation(pid, "coords");
  gl.vertexAttribPointer(al, 2 /*components per vertex */, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(al);
  
  let mr = gl.getUniformLocation(pid, 'mr');
  
  window.addEventListener('mousemove', draw);
  window.addEventListener('touchmove', draw);

  draw();

  function draw(e) {
    let ev = e && e.touches ? e.touches[0] : e;
    let x = ev ? ev.clientX : 250;
    let y = ev ? h - ev.clientY: 111;
    gl.uniform4f(mr, x, y, w, h);
    gl.viewport(0, 0, w, h);
    gl.clearColor(0, 0, 0, 0);
    gl.drawArrays(gl.TRIANGLES, 0, 3);
  }

  function shader(name, type) {
    let src = [].slice.call(document.scripts).find(s => s.type === name).innerText;
    let sid = gl.createShader(type);
    gl.shaderSource(sid, src);
    gl.compileShader(sid);
    gl.attachShader(pid, sid);
  }
</script>

标签: javascriptwebglfragment-shader

解决方案


在这两种情况下,屏幕上的每个像素都会被光栅化一次,但它们不一定只被着色一次。使用两个三角形,您将受到沿对角线的四边形阴影;一些像素即使在它们的三角形之外也会被着色,作为 2×2 四边形的辅助调用,然后被另一个三角形再次着色。

由于 GPU 如何将像素着色器调用打包到 SIMD 工作组中的实现细节,使用两个三角形的缓存效率也可能较低——同样,在两个三角形之间的边缘周围,您最终可能会在空间中将像素靠近在一起与使用单个全屏三角形时发生的情况相比,在时间上被阴影隔得更远。

Michal Drobot 在上一段链接的博客文章中发现,单三角形和双三角形全屏绘制之间的性能差异约为 8%。这仅适用于他正在使用的特定硬件和着色器,但它表明这些过度着色和缓存问题会导致可测量的性能下降。

另请注意,GPU 不会将全屏三角形裁剪为四边形。GPU 使用保护带剪裁,这意味着它们不会剪裁屏幕外几何体,直到顶点离屏幕太远以至于光栅化器中的数值精度会丢失(非常远)。在全屏三角形的情况下,光栅化器会将其作为单个三角形进行处理,并且不会为它的屏幕外部分生成片段。

简而言之,使用全屏三角形没有缺点,它可能会给你带来轻微的性能提升,所以在所有情况下我都更喜欢全屏四边形。


推荐阅读