[SOLVED] Camera Rendering Instanced Objects Behind Camera

Ok so I added instanced geometry to my scene in 4 x 4 tiles. Each tiles 30 trees and the tree is composed of two meshes. That means that I would get 32 draw calls if all the trees are in the cameras view. I didn’t want to render 480 trees if they were behind me so that’s why I split them up into 16 tiles on the map. This would reduce the draw calls from 15,360, to 32.
The problem is that the renderer always says that I have 32 draw calls even when I face none of the trees. For some reason the instanced mesh from this module doesn’t seem to be tested if it’s in the camera’s view. Am I doing something wrong or is the module handling the meshes incorrectly.

function loadSnubTrees() {
    //add trees
    loader.load( "/models/tree_snub_trunk.glb", 
    
    // called when the resource is loaded
    function ( gltf ) {
        
        let nodeTree = gltf.scene.children[2].children;
        let materials = [];
        let geometries = [];

        nodeTree.forEach(node => {
            if(node instanceof THREE.Mesh) {
                node.castShadow = true;

                materials.push(node.material);
                geometries.push(node.geometry);
                
            }
        });
        
        for(let row = 0; row < 4; row++) {
            for(let col = 0; col < 4; col++) {
                const position = getPositions(-100 + (col * 50), -100 + (row * 50));
                for(let index = 0; index < geometries.length; index++) {
                    addCluster(geometries[index], materials[index], 50, position);
                }
            }
        }
    });
}

function addCluster(geometry, material, count, positions) {

    geometry.rotateX(Math.PI / 2);
    //the instance group
    var cluster = new THREE.InstancedMesh( 
        geometry,                  //this is the same 
        material, 
        count,                       //instance count
        false,                     //is it dynamic
        false,                     //does it have color
        true,                      //uniform scale, axis' are scaled proportionally --> +Performance
    );
    cluster.castShadow = true;
    
    var _v3 = new THREE.Vector3();
    var _q = new THREE.Quaternion();
    
    for ( var i = 0 ; i < count ; i ++ ) {
        cluster.setQuaternionAt( i , _q );
        cluster.setPositionAt( i , _v3.set( positions[i] , -1.3, positions[i + 1]) );
        cluster.setScaleAt( i , _v3.set(1,1,1) );
    }
    //remove our alterations
    geometry.rotateX(-Math.PI / 2);
    scene.add( cluster );
}

function getPositions(startX, startZ, tileSize = 50, count = 30) {
    let positions = [];
    //count is how many x, z, coordinate pairs we want returned
    for(let coord = 0; coord < count; coord++) {
        positions.push(Math.random() * tileSize + startX);
        positions.push(Math.random() * tileSize + startZ);
    }
    return positions;
}

Any help is appreciated!

AFAIK, THREE.InstancedMesh does set Object3D.frustumCulled to false in order to prevent view frustum culling. This default value makes sense since instanced rendering makes it hard to automatically compute the bounding volume you need for the related frustum-intersection test.

The solution for your problem is to set the property to true, manually compute the bounding sphere for each mesh and then assign it to InstancedBufferGeometry.boundingSphere.

3 Likes

Thanks for the reply but it didn’t work entirely. The draw calls are looking more like I want them but the trees seem to only be viewable in the distance.


Here’s a look at the bounding sphere from the console.

Added code:

cluster.frustumCulled = true;
cluster.boundingSphere = new THREE.Sphere( new THREE.Vector3(), 25 );
cluster.position.set(startX, 0, startZ);
console.log(cluster);
scene.add( cluster );

I fiddled with it some more today and I found that if rotate the camera that’s what causes the objects to disappear. It seems that if I rotate the camera 30 - 45 degrees the objects are not rendered.
Can you give an example of how I should construct the bounding sphere.
I have a feeling that there’s a simple solution that I’m not seeing.
Thanks. :grinning:

I think I would create an instance of Box3 and would then use .expandByPoint() in order to create the AABB for each vertex of your cluster. After that, I would create a bounding sphere from the AABB.

When constructing the AABB, you have to transform the vertices of your geometry with the instanced attributes. Unfortunately, there is now example that demonstrate this approach.

I already know the size of my cluster because it creates trees at random locations from point from the 2d points (0, 0) to (50, 50). I even set my bounding sphere to have a radius of 50 instead of 25 but it stops working once I rotate the camera enough.

Okay, I see. Any chances to demonstrate the issue with a live example? I think it will be hard without debugging to provide more feedback.

here
You might to run it in incognito to clear the cache if you’ve run it before

Any chances to provide an unminified bundle.js? :innocent:

1 Like

I tried to not minify it but I’m too stupid to make it work lol
Here’s all the code though, some of it’s in separate imports but I’ve made sure that those parts work. Sorry if this code looks like a mess (I’m new to the web).

import * as THREE from 'three';
import * as OrbitControls from './utils/three-orbit-controls';
import GLTFLoader from 'three-gltf-loader';
import * as Stats from 'stats.js';
import {loadGround} from './utils/loadGround';
import {loadPineTrees} from './utils/loadPineTrees';
import {loadBushes} from './utils/loadBushes';
var InstancedMesh = require('three-instanced-mesh')( THREE );       //pass in THREE as a parameter to the returned function 

var stats = new Stats();
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.001, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
const loader = new GLTFLoader();
const controls = new OrbitControls(camera, renderer.domElement);
const ambientLight = new THREE.AmbientLight( 0xAAAAAA, 0.5 );
const dirLight = new THREE.DirectionalLight(0xFFFFFF, 0.8);
const modelScale = [1, 1, 1];
const medievalScale = [0.5, 0.5, 0.5];
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio( window.devicePixelRatio );
renderer.gammaOutput = true;
renderer.gammaFactor = 2.2;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);

dirLight.position.set( 100, 100, -50 );
dirLight.castShadow = true;
//make the shadows map only the area around the camera for performance
dirLight.target = camera;
scene.add( ambientLight );
scene.add( dirLight );

//how wide the shadow area should be
var d = 50;
dirLight.castShadow = true;
dirLight.shadow.camera.left = - d;
dirLight.shadow.camera.right = d;
dirLight.shadow.camera.top = d;
dirLight.shadow.camera.bottom = - d;
dirLight.shadow.camera.near = 20;  //150 is the distance from light to ground
dirLight.shadow.camera.far = 200;

//the resolution of the shadow
dirLight.shadow.mapSize.x = 1000;
dirLight.shadow.mapSize.y = 1000;

camera.position.set(0, 0, 3);
scene.background = new THREE.Color( 0x11f8bb );

//lerp and rate    > rate --> Faster the animation
controls.enableDamping = true;
controls.dampingFactor = 0.2;
controls.rotateSpeed = 0.3;

//show stats
stats.showPanel( 0 ); // 0: fps, 1: ms, 2: mb, 3+: custom
document.body.appendChild( stats.dom );

loadModels();

window.addEventListener('resize', () => {
    let width = window.innerWidth;
    let height = window.innerHeight;
    renderer.setSize(width, height);
    camera.aspect = width / height;
    //update camera frustum
    camera.updateProjectionMatrix();
});

//Render --> This is works for vr
//and requestAnimationFrame doesn't
//so we'll future proof rn instead of later
renderer.setAnimationLoop( () => {
    console.log(renderer.info.render.calls);
    stats.begin();
    controls.update();
    controls.runKeyBinds();
    dirLight.position.set( camera.position.x + 100, 100, camera.position.z -50 );
    renderer.render(scene, camera);
    stats.end();
});

function stop() {
    renderer.setAnimationLoop( null );
}

function loadModels() {
    loadGround(scene);
    loadSnubTrees();
    // loadPineTrees(scene, loader);
    // loadBushes(scene, loader);
}

function loadSnubTrees() {
    //add trees
    loader.load( "/models/tree_snub_trunk.glb", 
    
    // called when the resource is loaded
    function ( gltf ) {
        
        let nodeTree = gltf.scene.children[2].children;
        let materials = [];
        let geometries = [];

        nodeTree.forEach(node => {
            if(node instanceof THREE.Mesh) {
                node.castShadow = true;

                materials.push(node.material);
                geometries.push(node.geometry);
                
            }
        });
        
        //add 16 tree clusters (tiles)
        for(let row = 0; row < 4; row++) {
            for(let col = 0; col < 4; col++) {
                const positions = getPositions();
                for(let index = 0; index < geometries.length; index++) {
                    addCluster(geometries[index], materials[index], 50, positions, -100 + (col * 50), -100 + (row * 50));
                }
            }
        }
    });
}

function addCluster(geometry, material, count, positions, startX = 0, startZ = 0) {

    geometry.rotateX(Math.PI / 2);
    //the instance group
    var cluster = new THREE.InstancedMesh( 
        geometry,                  //this is the same 
        material, 
        count,                       //instance count
        false,                     //is it dynamic
        false,                     //does it have color
        true,                      //uniform scale, axis' are scaled proportionally --> +Performance
    );
    cluster.castShadow = true;
    
    var _v3 = new THREE.Vector3();
    var _q = new THREE.Quaternion();
    
    for ( var i = 0 ; i < count ; i ++ ) {
        cluster.setQuaternionAt( i , _q );
        cluster.setPositionAt( i , _v3.set( positions[i * 2] , -1.3, positions[i * 2 + 1]) );
        cluster.setScaleAt( i , _v3.set(1,1,1) );
    }
    //remove our alterations
    
    geometry.rotateX(-Math.PI / 2);
    
    cluster.frustumCulled = true;
    cluster.position.set(startX, 0, startZ);
    cluster.boundingSphere = new THREE.Sphere(new THREE.Vector3(0, -1.3, 0), 25.0);
    console.log(cluster);
    scene.add( cluster );
}

function getPositions(startX = 0, startZ = 0, tileSize = 50, count = 30) {
    let positions = [];
    //count is how many x, z, coordinate pairs we want returned
    for(let coord = 0; coord < count; coord++) {
        positions.push(Math.random() * tileSize + startX);
        positions.push(Math.random() * tileSize + startZ);
    }
    return positions;
}

Um, not sure this is correct. Frustum.intersectsObject() is looking for geometry.boundingSphere and not object.boundingSphere. If geometry.boundingSphere is null, a new one is automatically computed which will not work in your scenario.

1 Like

Ok I wrote
cluster.geometry.boundingSphere = new THREE.Sphere(new THREE.Vector3(0, -1.3, 0), 60.0);
and that seemed to fix things.
The reason the radius of 25.0 didn’t work was because some of the cluster would be outside of the bounding box and the bounding box isn’t centered within the objects because it ranges from (0,0,0) to (50, 5, 50). I think I’m gonna change this to a bounding box instead of a sphere because I can fit the trees in a smaller box.

Thank you so much for the help!

Hey I tried to make it work with the boundingBox but threejs seems to ignore it.

I assigned the box that I made to the boundingSphere and it works but then the draw calls are back to 32 instead of being dynamic.

Is this a bug with threejs?

No. Even if JavaScript allows the assignment of a bounding box, it does not mean this is valid. Frustum.intersectsObject() always expects to work with a bounding sphere.

1 Like

Oh ok that makes sense. Thanks
:slightly_smiling_face:

hmmm maybe an example for InstancedMesh would be in order. It seems like you were adding the bounding sphere to the wrong class, but then i can see it being confusing since the frustumCulled flas is on the mesh.

is there a way to display the used .boundingSphere ? like the THREE.BoundingBoxHelper ?

There is not explicit helper but you always can create a sphere mesh. Just pass the radius of the bounding sphere as the first parameter to SphereBufferGeometry and then use the center to position the mesh.

1 Like

thx this works, and give me more feeling how the frustum culling is done

1 Like