Why doesn't Unity provide a way to disable completely frustum culling?

In my project, there are often ~ 600 meshes and i intentionally want to render it all so frustum culling is none sense and it eats up 20-30% Camera.render time? And there is just no way to disable it. That’s a tragedy!
From technical point of view i believe it’s not that hard to implement an option like that.
Why doesn’t Unity want to do it at all as there are so many similar requests from 6,7 years ago?

1 Like

Why do you want to disable frustum culling? Cameras should only automatically cull what they will not render, and the act of culling those objects is much cheaper than running them through the GPU and culling them pixel-by-pixel. Is it that your mesh is fully outside the camera until your vertices are displaced using a shader?

If you absolutely must fool the engine into thinking these objects are on-screen, maybe you can modify your Mesh.Bounds to cover a large enough area that your camera will always see it? It’s a guess, I haven’t tested it.

Good luck!

If you have 600 of the same mesh (or a handful of meshes) you could use indirect instancing which skips past the culling.

1 Like

SRP offers manual control over culling but if it was me I would also go with the suggestion bgolus offered.

Indirect instancing works best if we want to draw a mesh for a particular amount of times. That’s not my case, my dynamic scene changes frame by frame.

I’m currently in optimization phase so i’m not sure i want to rewrite all my shaders using shader graph just to be able to disable frustum culling.

In this case i know beforehand that all the meshes are in the camera view.

1 Like

I agree. I suffer the same problem, I would like to see if disabling culling would result in faster render times. This makes sense for a lot of things, like UI cameras for example (if you’re not using Unity UI and canvases etc).

How is it that the mesh can be in the camera view but not the frustum?

EDIT: Nevermind, I misunderstood the issue. I’m surprised (and a bit skeptical) that your performance issues would be caused by frustum culling. Using 20-30% render time is not a red flag unless the render time itself is large.

I have the same issue, i instantiate some particles in a script and move them in the vertex shader.

I will emit circa 1000 particles and by the end of the emission i will have about 2 mil particles, is it possible to achieve that with indirect instancing. And it might not be wise to not do cull at all, i just want unity to be able to know the position of particles i changed via shaders so that the frustum culling to be valid

2 million quads is exactly the kind of use case instanced indirect is good at handling.

However if that bypass frustum culling won’t i have performance issues with that many particles? Especially when it will be in VR and i am going to need 90fps

Depends. Would drawing a 8 million poly mesh kill your frame rate on the hardware you’re targeting? Because if you were to draw a single large mesh, all of those vertices are still being calculated even if they’re off screen. This is no different.

But if you’re moving particles on the shader via compute or the older shader blit style, it’s monstrously expensive to get that data back to the CPU to cull. So the answers might be to cull them on the GPU with a compute shader, or use fewer particles.

Its not like an app/game that needs to be run in a variety of targets. I have some specific data from a smoke diffuse in a room simulation and i just want to do the 3d rendering part in VR in Unity. So the answer is that it will be run in a high end system (2080ti/20-series). So you suggest to do the culling via compute shaders? Do the draw calls via computeInstancedIndirect?

I also cant use less particles i have specific data(amount of CO2 per voxel). So i have a specific amount of particles i need to cover the room.

I suggest not worrying about it unless it’s causing a performance issue.

Hey i tried to render my particles via DrawMeshInstancedIndirect and i have two issues. First of all particles seem to flicker when i am using FixedUpdate with interval(0.01) but they dont with normal Update.(However since i am gonna need a constant 0.01s per frame a fixedupdate is preferable). The second and the biggest is that blending doesn’t seem to work. Smoke is transparent obviously and the farthest particles do show and overwrite full the nearest.
4923014--477032--upload_2019-9-2_18-39-38.png
I do have zwrite off and Blend SrcAlpha OneMinusSrcAlpha. About the code, i basically used the example code from the documentation https://docs.unity3d.com/ScriptReference/Graphics.DrawMeshInstancedIndirect.html with some changes to match my case. For instance i initiate at start a starting position and just issue the indirect draw call at update.
4923014--477035--upload_2019-9-2_18-45-4.png

4923014--477038--upload_2019-9-2_18-45-26.png
positions is an array of list which contains the positions of smoke instances in every 0.01sec
This is the shader, ignore the commented code

Shader "Unlit/SmokeShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Opacity("Opacity", Float) = 0.0
        /*_Color("Albedo", Color) = (1,1,1,1)
       
        [HideInInspector] _ID("Id",Int) =0.0*/
        //[HideInInspector] _PreviousPos("PreviousPosition", VectorArray) = (0.0,0.0,1.0)
       
       
    }
        SubShader
    {
        Tags { "Queue" = "Transparent" "RenderType" = "TransparentCutout" "IgnoreProjector" = "True" }
        LOD 100


        ZWrite Off

        Blend SrcAlpha OneMinusSrcAlpha
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 4.5
            // make fog work

            #pragma multi_compile_instancing
            #include "UnityCG.cginc"
            sampler2D _MainTex;
            float _Opacity;
            #if SHADER_TARGET >= 45
                StructuredBuffer<float3> previousPosition;
            #endif
    //        struct appdata
    //        {
                //UNITY_VERTEX_INPUT_INSTANCE_ID
                //    //uint n : SV_InstanceID;
                //
    //            float4 vertex :POSITION;
    //            float2 uv : TEXCOORD0;
    //        };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv_prev : TEXCOORD0;
                float2 uv_next : TEXCOORD1;

            };
           
           
               
           
           

            v2f vert (appdata_full v, uint instanceID : SV_InstanceID)
            {
                v2f o;
                /*UNITY_SETUP_INSTANCE_ID(v);
                UNITY_TRANSFER_INSTANCE_ID(v, o);*/
                #if SHADER_TARGET >= 45
                    float3 pos = previousPosition[instanceID];
                #else
                    float3 pos = 0;
                #endif
               

                float3 worldPosition = pos.xyz;
               
                //int id = UNITY_ACCESS_INSTANCED_PROP(InstanceProperties, _ID);
               
               
                //float l = 100.0*fmod(_Time.x,0.01);
                //float3 currentPos = (1 - l) * (_PreviousPos[id].xyz) + l * (_NextPos[id].xyz);
                //l /= 0.02;
               
                ////float l = 2.0;
                ////float3 currentPos = (1-l)* nextPos + l * nextPos003;
                //if (l <= 1)
                //{
                //    currentPos = (1 - l) * previousPos + l * nextPos001;
                //}
                //else if (l <= 2)
                //{
                //    currentPos = (2 - l) * nextPos001 + (l - 1) * nextPos;
                //}
                //else if (l <= 3)
                //{
                //    currentPos = (3 - l) * nextPos + (l - 2) * nextPos003;
                //}
                //else if (l <= 4)
                //{
                //    currentPos = (4 - l) * nextPos003 + (l - 3) * nextPos004;
                //}
                //else if (l <= 5)
                //{
                //    currentPos = (5 - l) * nextPos004 + (l - 4) * nextPos005;
                //}
                //float3 currentPos = previousPos + _Time.y * vel;
               
                //float4 currentPos = float4(nextPos,1.0);
               
                float4x4 worldMatrix = unity_ObjectToWorld;
                worldMatrix[0][3] = worldPosition.x;
                worldMatrix[1][3] = worldPosition.y;
                worldMatrix[2][3] = worldPosition.z;

                float3 worldpos = worldPosition.xyz + v.vertex.xyz;
                o.pos = mul(UNITY_MATRIX_VP, float4(worldpos,1.0f));
               
                /*worldMatrix[0][3] = 1;
                worldMatrix[1][3] = 1;
                worldMatrix[2][3] = 1;
                worldMatrix[3][3] = 1.0;*/
                /*worldMatrix[3][0] = 1.0;
                worldMatrix[3][1] = 1.0;
                worldMatrix[3][2] = 1.0;
                worldMatrix[3][3] = 1.0;*/
               
                //float3 vpos = mul((float3x3)unity_ObjectToWorld, v.vertex.xyz);
                //float4 worldPos = float4(nextPos.x, nextPos.y, nextPos.z, 1);
                //o.vertex = mul(UNITY_MATRIX_P, mul(UNITY_MATRIX_V, mul(_Translate, v.vertex)));
                /*o.pos = mul(UNITY_MATRIX_P,
                    mul(mul(UNITY_MATRIX_V,worldMatrix), float4(0.0, 0.0, 0.0, 1.0))
                    + float4(v.vertex.x, v.vertex.y,0.0, 0.0)*float4(2.0f,2.0f,1.0f,1.0f));*/
                //v.vertex = mul(unity_WorldToObject, worldPos);





                float speed = 12;
             
                float2 size = float2(1.0f / 8, 1.0f / 8);
                uint totalFrames = 64;
                uint index = _Time.y*speed + instanceID;
                uint indexX = (index)%8;
                uint indexX2 = (index  + 1 ) % 8;
                uint indexY = floor(((index ) % 64) / 8);
                uint indexY2 = floor(((index  +1) % 64) / 8);
                float2 offset = float2(size.x * indexX, -size.y * indexY);
                float2 offset2 = float2(size.x * indexX2, -size.y * indexY2);
                float2 newUV = v.texcoord * size;
                newUV.y = newUV.y + size.y * (8 - 1);
                o.uv_prev = newUV + offset;
                o.uv_next = newUV + offset2;

               
               
                //o.vertex = UnityObjectToClipPos(v.vertex);
                //o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                /*float3 RS = mul((float3x3)unity_ObjectToWorld, v.vertex.xyz);
                float3 newRS = mul((float3x3)_Rotation, RS);
                float4 worldCoord = float4(unity_ObjectToWorld._m03, unity_ObjectToWorld._m13, unity_ObjectToWorld._m23, 1);
               
                float4 viewPos = mul(UNITY_MATRIX_V, worldCoord) + float4(vpos, 0);
                float4 outPos = mul(UNITY_MATRIX_P, viewPos);*/



               
                   
                    /*o.vertex = mul(UNITY_MATRIX_P,
                    mul(UNITY_MATRIX_MV, float4(0.0, 0.0, 0.0, 1.0))
                    + float4(v.vertex.x, v.vertex.y, 0.0, 0.0)*float4(2.0,2.0,1.0,1.0));*/

                   
               
                return o;
            }   

            fixed4 frag (v2f i) : SV_Target
            {
                float speed = 12;
                /*UNITY_SETUP_INSTANCE_ID(i);*/
                // sample the texture
               
                float l = fmod(_Time.y,1.0/speed);
                l = l*speed;
                fixed4 tex = lerp(tex2D(_MainTex, i.uv_prev), tex2D(_MainTex, i.uv_next), l);
               
                fixed4 col = tex;
                // apply fog
                //UNITY_APPLY_FOG(i.fogCoord, col);
                //col.a *= _Opacity;
               
                return col;
            }
            ENDCG
        }
    }
}

Are you’re calling Graphics.DrawMeshInstancedindirect in the fixed update? That’s a big no-no as it might be getting called multiple times per rendered frame. Updating the particle positions using a fixed update loop is totally fine, but you should only be drawing in update or using a camera attached command buffer.

Blending is working just fine. Sorting is not working, because you need to handle that manually when using DrawMeshInstancedIndirect, likely using a bitonic merge compute shader. Or you need to use opaque geometry, or some form of approximated order independent transparency like weighted blended OIT.

Ok you got me confused there. How will be the draw call valid when the material’s buffers arent updated. Basically i have in a list the position of particles every 0.01sec so i want to update the material every that interval. Correct me if i am wrong but in a basic rendering loop you update some buffers change the state of parametres in the rendering pipeline and then you issue the draw call.Then the gpu executes the corresponding shaders. If the draw call is every 0.02 for instance and i want new data every 0.01 how will be that correct? Also i don’t understand how can be called multiple times per frame since i have one draw call in the update.

Yeah i kinda thought of it might need that, i have seen some experiments in github of people working with compute shaders and draw indirect so i will check those out.

FixedUpdate is potentially called multiple times per rendered frame. It’s useful for updating animator or physics at a fixed time step, but the actual rendering of the scene is unrelated to FixedUpdate and happens after Update.

For example, if you have a fixed update of 0.01, but you’re only rendering at 10 fps (update delta of 0.1), fixed update will be called 10 times before each update, and before a frame is rendered.

If you’re rendering at 90hz (like for desktop VR), that’s an update of 0.0111…, which means every 10 frames or so the fixed update will be called twice.