为什么我的遮挡剔除失败 Three.js?

Why is my occlusion culling failed with Three.js?

首先我知道 Three.js 官方不支持遮挡剔除。但是,我认为可以在屏幕外进行遮挡剔除 canvas 并将结果复制回我的 Three.js WebGLCanvas。

基本上,我想改造这个demo Three.JS 演示。我使用 Three.js 来创建所有内容,并在同步的屏幕外 canvas 中,我针对每个边界框测试遮挡剔除。如果任何边界框被遮挡,我将关闭该球体在主 canvas 中的可见性。这些就是我在这个片段中所做的。但是我不知道为什么它没有遮挡任何球体。

我认为计算边界框的 ModelViewProjection 矩阵可能会出现问题,但我没有发现任何错误。有人可以帮忙吗?

var camera, scene, renderer, light;
var spheres = [];
var NUM_SPHERES, occludedSpheres = 0;
var gl;
var boundingBoxPositions;
var boundingBoxProgram, boundingBoxArray, boundingBoxModelMatrixLocation, viewProjMatrixLocation;
var viewMatrix, projMatrix;
var firstRender = true;

var sphereCountElement = document.getElementById("num-spheres");
var occludedSpheresElement = document.getElementById("num-invisible-spheres");

// depth sort variables
var sortPositionA = new THREE.Vector3();
var sortPositionB = new THREE.Vector3();
var sortModelView = new THREE.Matrix4();

init();
animate();

function init() {

    scene = new THREE.Scene();
    scene.add( new THREE.AmbientLight( 0x222222 ) );
    light = new THREE.DirectionalLight( 0xffffff, 1 );
    scene.add( light );

    camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 1000);
    
    renderer = new THREE.WebGLRenderer( { antialias: true } );
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);
    
    // set up offscreen canvas
  
    var offscreenCanvas = new OffscreenCanvas(window.innerWidth, window.innerHeight);
    gl = offscreenCanvas.getContext('webgl2');
    
    if ( !gl ) {
      console.error("WebGL 2 not available");
      document.body.innerHTML = "This example requires WebGL 2 which is unavailable on this system."
    }
    
    // define spheres

    var GRID_DIM = 6;
    var GRID_OFFSET = GRID_DIM / 2 - 0.5;
    NUM_SPHERES = GRID_DIM * GRID_DIM;
    sphereCountElement.innerHTML = NUM_SPHERES;

    var geometry = new THREE.SphereGeometry(20, 64, 64);
    var material = new THREE.MeshPhongMaterial( {
        color: 0xff0000,
        specular: 0x050505,
        shininess: 50,
        emissive: 0x000000
    } );
    geometry.computeBoundingBox();

    for ( var i = 0; i < NUM_SPHERES; i ++ ) {
    
      var x = Math.floor(i / GRID_DIM) - GRID_OFFSET;
      var z = i % GRID_DIM - GRID_OFFSET;
      var mesh = new THREE.Mesh( geometry, material );
      spheres.push(mesh);
      scene.add(mesh);

      mesh.position.set(x * 35, 0, z * 35);
      mesh.userData.query = gl.createQuery();
      mesh.userData.queryInProgress = false;
      mesh.userData.occluded = false;
      
    }
    
    //////////////////////////
    // WebGL code
    //////////////////////////
    
    // boundingbox shader
    
    var boundingBoxVSource =  document.getElementById("vertex-boundingBox").text.trim();
    var boundingBoxFSource =  document.getElementById("fragment-boundingBox").text.trim();
    var boundingBoxVertexShader = gl.createShader(gl.VERTEX_SHADER);
    gl.shaderSource(boundingBoxVertexShader, boundingBoxVSource);
    gl.compileShader(boundingBoxVertexShader);

    if (!gl.getShaderParameter(boundingBoxVertexShader, gl.COMPILE_STATUS)) {
      console.error(gl.getShaderInfoLog(boundingBoxVertexShader));
    }

    var boundingBoxFragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
    gl.shaderSource(boundingBoxFragmentShader, boundingBoxFSource);
    gl.compileShader(boundingBoxFragmentShader);

    if (!gl.getShaderParameter(boundingBoxFragmentShader, gl.COMPILE_STATUS)) {
      console.error(gl.getShaderInfoLog(boundingBoxFragmentShader));
    }

    boundingBoxProgram = gl.createProgram();
    gl.attachShader(boundingBoxProgram, boundingBoxVertexShader);
    gl.attachShader(boundingBoxProgram, boundingBoxFragmentShader);
    gl.linkProgram(boundingBoxProgram);

    if (!gl.getProgramParameter(boundingBoxProgram, gl.LINK_STATUS)) {
      console.error(gl.getProgramInfoLog(boundingBoxProgram));
    }
    
    // uniform location
    
    boundingBoxModelMatrixLocation = gl.getUniformLocation(boundingBoxProgram, "uModel");
    viewProjMatrixLocation = gl.getUniformLocation(boundingBoxProgram, "uViewProj");

    // vertex location
    
    boundingBoxPositions = computeBoundingBoxPositions(geometry.boundingBox);

    boundingBoxArray = gl.createVertexArray();
    gl.bindVertexArray(boundingBoxArray);

    var positionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, boundingBoxPositions, gl.STATIC_DRAW);
    gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(0);

    gl.bindVertexArray(null);

    window.addEventListener( 'resize', onWindowResize, false );

}

function onWindowResize() {

    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();

    renderer.setSize( window.innerWidth, window.innerHeight );

}

function animate() {

    requestAnimationFrame(animate);
    render();

}

function depthSort(a, b) {
    sortPositionA.copy(a.position);
    sortPositionB.copy(b.position);

    sortModelView.copy(viewMatrix).multiply(a.matrix);
    sortPositionA.applyMatrix4(sortModelView);
    sortModelView.copy(viewMatrix).multiply(b.matrix);
    sortPositionB.applyMatrix4(sortModelView);
    return sortPositionB[2] - sortPositionA[2];
}

function render() {

    var timer = Date.now() * 0.0001;
    camera.position.x = Math.cos( timer ) * 250;
    camera.position.z = Math.sin( timer ) * 250;
    camera.lookAt( scene.position );
    light.position.copy( camera.position );
    
    occludedSpheres = 0;
    
    gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
    gl.clearColor(0, 0, 0, 1);
    gl.enable(gl.DEPTH_TEST);
    gl.colorMask(true, true, true, true);
    gl.depthMask(true);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
        
    if (!firstRender) {
    viewMatrix = camera.matrixWorldInverse.clone();
    projMatrix = camera.projectionMatrix.clone();
    var viewProjMatrix = projMatrix.multiply(viewMatrix);

    spheres.sort(depthSort);

    // for occlusion test
          
    gl.colorMask(false, false, false, false);
    gl.depthMask(false);
    gl.useProgram(boundingBoxProgram);
    gl.bindVertexArray(boundingBoxArray);

    for (var i = 0; i < NUM_SPHERES; i ++) {
          
        spheres[i].visible = true;
        spheres[i].rotation.y += 0.003;

        var sphereData = spheres[i].userData;

        gl.uniformMatrix4fv(boundingBoxModelMatrixLocation, false, spheres[i].matrix.elements);
        gl.uniformMatrix4fv(viewProjMatrixLocation, false, viewProjMatrix.elements);

        // check query results here (will be from previous frame)
              
        if (sphereData.queryInProgress && gl.getQueryParameter(sphereData.query, gl.QUERY_RESULT_AVAILABLE)) {
              
            sphereData.occluded = !gl.getQueryParameter(sphereData.query, gl.QUERY_RESULT);
            if (sphereData.occluded) occludedSpheres ++;
            sphereData.queryInProgress = false;
                
       }

       // Query is initiated here by drawing the bounding box of the sphere
              
       if (!sphereData.queryInProgress) {
              
           gl.beginQuery(gl.ANY_SAMPLES_PASSED_CONSERVATIVE, sphereData.query);
           gl.drawArrays(gl.TRIANGLES, 0, boundingBoxPositions.length / 3);
           gl.endQuery(gl.ANY_SAMPLES_PASSED_CONSERVATIVE);
           sphereData.queryInProgress = true;
                
       }

       if (sphereData.occluded) {
              
            spheres[i].visible = false;
                
       }
            
    }
          
        occludedSpheresElement.innerHTML = occludedSpheres;
          
    }
        
    firstRender = false;

    renderer.render(scene, camera);

}

function computeBoundingBoxPositions(box) {

    var dimension = box.max.sub(box.min);
    var width = dimension.x;
    var height = dimension.y;
    var depth = dimension.z;
    var x = box.min.x;
    var y = box.min.y;
    var z = box.min.z;

    var fbl = {x: x,         y: y,          z: z + depth};
    var fbr = {x: x + width, y: y,          z: z + depth};
    var ftl = {x: x,         y: y + height, z: z + depth};
    var ftr = {x: x + width, y: y + height, z: z + depth};
    var bbl = {x: x,         y: y,          z: z };
    var bbr = {x: x + width, y: y,          z: z };
    var btl = {x: x,         y: y + height, z: z };
    var btr = {x: x + width, y: y + height, z: z };

    var positions = new Float32Array([
      //front
      fbl.x, fbl.y, fbl.z,
      fbr.x, fbr.y, fbr.z,
      ftl.x, ftl.y, ftl.z,
      ftl.x, ftl.y, ftl.z,
      fbr.x, fbr.y, fbr.z,
      ftr.x, ftr.y, ftr.z,

      //right
      fbr.x, fbr.y, fbr.z,
      bbr.x, bbr.y, bbr.z,
      ftr.x, ftr.y, ftr.z,
      ftr.x, ftr.y, ftr.z,
      bbr.x, bbr.y, bbr.z,
      btr.x, btr.y, btr.z,

      //back
      fbr.x, bbr.y, bbr.z,
      bbl.x, bbl.y, bbl.z,
      btr.x, btr.y, btr.z,
      btr.x, btr.y, btr.z,
      bbl.x, bbl.y, bbl.z,
      btl.x, btl.y, btl.z,

      //left
      bbl.x, bbl.y, bbl.z,
      fbl.x, fbl.y, fbl.z,
      btl.x, btl.y, btl.z,
      btl.x, btl.y, btl.z,
      fbl.x, fbl.y, fbl.z,
      ftl.x, ftl.y, ftl.z,

      //top
      ftl.x, ftl.y, ftl.z,
      ftr.x, ftr.y, ftr.z,
      btl.x, btl.y, btl.z,
      btl.x, btl.y, btl.z,
      ftr.x, ftr.y, ftr.z,
      btr.x, btr.y, btr.z,

      //bottom
      bbl.x, bbl.y, bbl.z,
      bbr.x, bbr.y, bbr.z,
      fbl.x, fbl.y, fbl.z,
      fbl.x, fbl.y, fbl.z,
      bbr.x, bbr.y, bbr.z,
      fbr.x, fbr.y, fbr.z,
    ]);

  return positions;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r125/three.js"></script>
<div id="occlusion-controls">
  Spheres: <span id="num-spheres"></span><br> Culled spheres: <span id="num-invisible-spheres"></span><br>
</div>

<script type="x-shader/vs" id="vertex-boundingBox">#version 300 es
layout(std140, column_major) uniform;
layout(location=0) in vec4 position;
uniform mat4 uModel;
uniform mat4 uViewProj;
void main() {
  gl_Position = uViewProj * uModel * position;
}
</script>
<script type="x-shader/vf" id="fragment-boundingBox">#version 300 es
precision highp float;
layout(std140, column_major) uniform;
out vec4 fragColor;
void main() {
  fragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
</script>

至少,这些是我发现的问题。

  1. 你需要写入深度缓冲区(否则怎么会被遮挡?)

    所以删除 gl.depthMask(false)

  2. 您需要 gl.flush OffscreenCanvas 因为在屏幕外,不会自动为您添加一个。我通过使用普通 canvas 并将其添加到页面中发现了这一点。我还通过注释掉 gl.colorMask(false, false, false, false) 来打开绘图,只是为了仔细检查您的框是否正确绘制。我注意到,当我进行某种工作时,当我切换回屏幕外时,它的行为会有所不同 canvas。如果我没有将正常的 canvas 添加到页面,我会发现相同的不同行为。添加 gl.flush 修复了不同的行为。

  3. depthSort 不工作

    我通过将着色器更改为使用一种颜色来检查这一点,并将 i / NUM_SPHERES 作为颜色传入,这清楚地表明它们没有被排序。问题是这个

    return sortPositionB[2] - sortPositionA[2];
    

    需要

    return sortPositionB.z - sortPositionA.z;
    

var camera, scene, renderer, light;
var spheres = [];
var NUM_SPHERES, occludedSpheres = 0;
var gl;
var boundingBoxPositions;
var boundingBoxProgram, boundingBoxArray, boundingBoxModelMatrixLocation, viewProjMatrixLocation;
var viewMatrix, projMatrix;
var firstRender = true;

var sphereCountElement = document.getElementById("num-spheres");
var occludedSpheresElement = document.getElementById("num-invisible-spheres");

// depth sort variables
var sortPositionA = new THREE.Vector3();
var sortPositionB = new THREE.Vector3();
var sortModelView = new THREE.Matrix4();

init();
animate();

function init() {

    scene = new THREE.Scene();
    scene.add( new THREE.AmbientLight( 0x222222 ) );
    light = new THREE.DirectionalLight( 0xffffff, 1 );
    scene.add( light );

    camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 1000);
    
    renderer = new THREE.WebGLRenderer( { antialias: true } );
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);
    
    // set up offscreen canvas
  
    var offscreenCanvas = new OffscreenCanvas(window.innerWidth, window.innerHeight);
    //var offscreenCanvas = document.createElement('canvas');
    //offscreenCanvas.width = window.innerWidth;
    //offscreenCanvas.height = window.innerHeight;
    //document.body.appendChild(offscreenCanvas);
    gl = offscreenCanvas.getContext('webgl2');
    
    if ( !gl ) {
      console.error("WebGL 2 not available");
      document.body.innerHTML = "This example requires WebGL 2 which is unavailable on this system."
    }
    
    // define spheres

    var GRID_DIM = 6;
    var GRID_OFFSET = GRID_DIM / 2 - 0.5;
    NUM_SPHERES = GRID_DIM * GRID_DIM;
    sphereCountElement.innerHTML = NUM_SPHERES;

    var geometry = new THREE.SphereGeometry(20, 64, 64);
    var material = new THREE.MeshPhongMaterial( {
        color: 0xff0000,
        specular: 0x050505,
        shininess: 50,
        emissive: 0x000000
    } );
    geometry.computeBoundingBox();

    for ( var i = 0; i < NUM_SPHERES; i ++ ) {
    
      var x = Math.floor(i / GRID_DIM) - GRID_OFFSET;
      var z = i % GRID_DIM - GRID_OFFSET;
      var mesh = new THREE.Mesh( geometry, material );
      spheres.push(mesh);
      scene.add(mesh);

      mesh.position.set(x * 35, 0, z * 35);
      mesh.userData.query = gl.createQuery();
      mesh.userData.queryInProgress = false;
      mesh.userData.occluded = false;
      
    }
    
    //////////////////////////
    // WebGL code
    //////////////////////////
    
    // boundingbox shader
    
    var boundingBoxVSource =  document.getElementById("vertex-boundingBox").text.trim();
    var boundingBoxFSource =  document.getElementById("fragment-boundingBox").text.trim();
    var boundingBoxVertexShader = gl.createShader(gl.VERTEX_SHADER);
    gl.shaderSource(boundingBoxVertexShader, boundingBoxVSource);
    gl.compileShader(boundingBoxVertexShader);

    if (!gl.getShaderParameter(boundingBoxVertexShader, gl.COMPILE_STATUS)) {
      console.error(gl.getShaderInfoLog(boundingBoxVertexShader));
    }

    var boundingBoxFragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
    gl.shaderSource(boundingBoxFragmentShader, boundingBoxFSource);
    gl.compileShader(boundingBoxFragmentShader);

    if (!gl.getShaderParameter(boundingBoxFragmentShader, gl.COMPILE_STATUS)) {
      console.error(gl.getShaderInfoLog(boundingBoxFragmentShader));
    }

    boundingBoxProgram = gl.createProgram();
    gl.attachShader(boundingBoxProgram, boundingBoxVertexShader);
    gl.attachShader(boundingBoxProgram, boundingBoxFragmentShader);
    gl.linkProgram(boundingBoxProgram);

    if (!gl.getProgramParameter(boundingBoxProgram, gl.LINK_STATUS)) {
      console.error(gl.getProgramInfoLog(boundingBoxProgram));
    }
    
    // uniform location
    
    boundingBoxModelMatrixLocation = gl.getUniformLocation(boundingBoxProgram, "uModel");
    viewProjMatrixLocation = gl.getUniformLocation(boundingBoxProgram, "uViewProj");

    // vertex location
    
    boundingBoxPositions = computeBoundingBoxPositions(geometry.boundingBox);

    boundingBoxArray = gl.createVertexArray();
    gl.bindVertexArray(boundingBoxArray);

    var positionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, boundingBoxPositions, gl.STATIC_DRAW);
    gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(0);

    gl.bindVertexArray(null);

    window.addEventListener( 'resize', onWindowResize, false );

}

function onWindowResize() {

    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();

    renderer.setSize( window.innerWidth, window.innerHeight );

}

function animate() {

    requestAnimationFrame(animate);
    render();

}

function depthSort(a, b) {
    sortPositionA.copy(a.position);
    sortPositionB.copy(b.position);

    sortModelView.copy(viewMatrix).multiply(a.matrix);
    sortPositionA.applyMatrix4(sortModelView);
    sortModelView.copy(viewMatrix).multiply(b.matrix);
    sortPositionB.applyMatrix4(sortModelView);
    return sortPositionB.z - sortPositionA.z;
}

function render() {

    var timer = Date.now() * 0.0001;
    camera.position.x = Math.cos( timer ) * 250;
    camera.position.z = Math.sin( timer ) * 250;
    camera.lookAt( scene.position );
    light.position.copy( camera.position );
    
    occludedSpheres = 0;
    
    gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
    gl.clearColor(0, 0, 0, 1);
    gl.enable(gl.DEPTH_TEST);
    gl.colorMask(true, true, true, true);
    gl.depthMask(true);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
        
    if (!firstRender) {
    viewMatrix = camera.matrixWorldInverse.clone();
    projMatrix = camera.projectionMatrix.clone();
    var viewProjMatrix = projMatrix.multiply(viewMatrix);

    spheres.sort(depthSort);

    // for occlusion test
          
    gl.colorMask(false, false, false, false);
    //gl.depthMask(false);
    gl.useProgram(boundingBoxProgram);
    gl.bindVertexArray(boundingBoxArray);

    for (var i = 0; i < NUM_SPHERES; i ++) {
          
        spheres[i].visible = true;
        spheres[i].rotation.y += 0.003;

        var sphereData = spheres[i].userData;

        gl.uniformMatrix4fv(boundingBoxModelMatrixLocation, false, spheres[i].matrix.elements);
        gl.uniformMatrix4fv(viewProjMatrixLocation, false, viewProjMatrix.elements);
        gl.uniform4f(gl.getUniformLocation(boundingBoxProgram, 'color'),
          i / NUM_SPHERES, 0, 0, 1);

        // check query results here (will be from previous frame)
              
        if (sphereData.queryInProgress && gl.getQueryParameter(sphereData.query, gl.QUERY_RESULT_AVAILABLE)) {
              
            sphereData.occluded = !gl.getQueryParameter(sphereData.query, gl.QUERY_RESULT);
            if (sphereData.occluded) occludedSpheres ++;
            sphereData.queryInProgress = false;
                
       }

       // Query is initiated here by drawing the bounding box of the sphere
              
       if (!sphereData.queryInProgress) {
              
           gl.beginQuery(gl.ANY_SAMPLES_PASSED_CONSERVATIVE, sphereData.query);
           gl.drawArrays(gl.TRIANGLES, 0, boundingBoxPositions.length / 3);
           gl.endQuery(gl.ANY_SAMPLES_PASSED_CONSERVATIVE);
           sphereData.queryInProgress = true;
                
       }

       if (sphereData.occluded) {
              
            spheres[i].visible = false;
                
       }
            
    }
    gl.flush();
          
        occludedSpheresElement.innerHTML = occludedSpheres;
          
    }
        
    firstRender = false;

    renderer.render(scene, camera);

}

function computeBoundingBoxPositions(box) {

    var dimension = box.max.sub(box.min);
    var width = dimension.x;
    var height = dimension.y;
    var depth = dimension.z;
    var x = box.min.x;
    var y = box.min.y;
    var z = box.min.z;

    var fbl = {x: x,         y: y,          z: z + depth};
    var fbr = {x: x + width, y: y,          z: z + depth};
    var ftl = {x: x,         y: y + height, z: z + depth};
    var ftr = {x: x + width, y: y + height, z: z + depth};
    var bbl = {x: x,         y: y,          z: z };
    var bbr = {x: x + width, y: y,          z: z };
    var btl = {x: x,         y: y + height, z: z };
    var btr = {x: x + width, y: y + height, z: z };

    var positions = new Float32Array([
      //front
      fbl.x, fbl.y, fbl.z,
      fbr.x, fbr.y, fbr.z,
      ftl.x, ftl.y, ftl.z,
      ftl.x, ftl.y, ftl.z,
      fbr.x, fbr.y, fbr.z,
      ftr.x, ftr.y, ftr.z,

      //right
      fbr.x, fbr.y, fbr.z,
      bbr.x, bbr.y, bbr.z,
      ftr.x, ftr.y, ftr.z,
      ftr.x, ftr.y, ftr.z,
      bbr.x, bbr.y, bbr.z,
      btr.x, btr.y, btr.z,

      //back
      fbr.x, bbr.y, bbr.z,
      bbl.x, bbl.y, bbl.z,
      btr.x, btr.y, btr.z,
      btr.x, btr.y, btr.z,
      bbl.x, bbl.y, bbl.z,
      btl.x, btl.y, btl.z,

      //left
      bbl.x, bbl.y, bbl.z,
      fbl.x, fbl.y, fbl.z,
      btl.x, btl.y, btl.z,
      btl.x, btl.y, btl.z,
      fbl.x, fbl.y, fbl.z,
      ftl.x, ftl.y, ftl.z,

      //top
      ftl.x, ftl.y, ftl.z,
      ftr.x, ftr.y, ftr.z,
      btl.x, btl.y, btl.z,
      btl.x, btl.y, btl.z,
      ftr.x, ftr.y, ftr.z,
      btr.x, btr.y, btr.z,

      //bottom
      bbl.x, bbl.y, bbl.z,
      bbr.x, bbr.y, bbr.z,
      fbl.x, fbl.y, fbl.z,
      fbl.x, fbl.y, fbl.z,
      bbr.x, bbr.y, bbr.z,
      fbr.x, fbr.y, fbr.z,
    ]);

  return positions;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r125/three.js"></script>
<div id="occlusion-controls">
  Spheres: <span id="num-spheres"></span><br> Culled spheres: <span id="num-invisible-spheres"></span><br>
</div>

<script type="x-shader/vs" id="vertex-boundingBox">#version 300 es
layout(std140, column_major) uniform;
layout(location=0) in vec4 position;
uniform mat4 uModel;
uniform mat4 uViewProj;
void main() {
  gl_Position = uViewProj * uModel * position;
}
</script>
<script type="x-shader/vf" id="fragment-boundingBox">#version 300 es
precision highp float;
layout(std140, column_major) uniform;
out vec4 fragColor;
void main() {
  fragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
</script>