Camera facing billboards for VR

Camera facing particle billboards for VR. Has anyone managed to stop the rotation of them when rotating heads.

See here: http://i.giphy.com/l3q2FlgFUsr5qVHTa.gif

The particles look horrible in VR as they rotate when the head rotates. Has anyone been able to reduce the effect of this? I’ve tried setting Billboard Alignment to facing.

1 Like

Same problem here, have you found any solution?

Workaround:
For ground based billboards (like trees) you can use a regular quad object or prefab with this optimized 1 dimension rotation script on it:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class vr_billboard : MonoBehaviour
{
    static Transform tCam = null;
    void Update ()
    {
        if(!tCam)
        {
            if(!Camera.main)
            {
                return;
            }
            tCam = Camera.main.transform;
        }
        transform.LookAt(tCam.position, Vector3.up);
    }
}

For clouds up above like @eastes gif: Put them high enough and flat in XZ and no billboard needed or you could try swapping Vector3.up in above code for another axis if you want something above and fairly close.

Full and general quad faces camera solution would require 2 dimensional constrained quaternion rotation.

I just spent a week a little while ago tackling grass in Unity terrain, which does the same thing, and found some solutions. I have no experience with the particle billboards so I don’t know if it will transfer over but maybe you will find one useful.

  1. as SiliconDroid said, for far away clouds don’t even bother billboarding them

  2. you can rotate the objects themselves with a script as SiliconDroid said and not billboard in the shader, but if you have a lot of objects (like several thousand grass quads) this is really, really slow. But this is easiest.

  3. You can write a new billboard shader with a vert shader that orients to camera position instead of camera.forward. This approach suffered from some weird problem where you’d have to disable batching in the shader or the vert shader would run incorrectly and all your different quads would end up transformed on top of each other. Turning off batching was OK for small numbers but bad for ~5000 grass cards.

  4. The “best” solution I found which I’m using now for my grass was to copy the builtin unity grass shader and override it with my custom copy where I oriented to the camera position instead of camera forward. Again, I haven’t messed with the particle system at all, but I would guess you could do something similar.

1 Like

Yup… I guess that’s gonna be the best solution for particles also.

I’m guessing the “broken” billboard shader was written to match camera rotation instead of lookat camera for some batching performance reason as all quads will have same worldspace vert matrix, not sure tho? Shaders to me at the moment are a black art.

Shaders are totally a black art to me too. I get the sl part but the hooks into unity and cg and pragma this and Tags that are totally confusing.

Weirdly enough my custom “face the camera instead of camera.forward” shader which overrides the builtin grass shader and is working in the builtin terrain system, although it does have a “DisableBatching” = “True” line in it, somehow results in very few draw calls - much fewer than if I added the same number of grass objects on my own. I don’t know how or what the Terrain system is doing but it actually works really well.

For Shuriken particles you can counter rotate the particles against the camera’s roll, but it is expensive. For billboard shaders, most do a trick to expand themselves in view space, but that means they feel “attached” to your face. @RibsNGibs comments are good ones. A side effect of using a billboard shader to orient towards the camera position rather than “camera forward” is you need a second vector to construct a full orientation, and the easiest world vector to use is world up, the result is a billboard that isn’t locked to the roll of the camera.

It does have one problem, which is if you look up or down close to 90 degrees your billboards will either start to spin or shrink depending on the implementation, but luckily people don’t usually rotate their heads that far.

I have same problem. I’d like to try your solution. Would you like to share your custom “face the camera instead of camera.forward” shader code?

I’m sure this isn’t the best way to do this, but this worked for me (I just copied the builtin shader and the functions it called to do the billboarding and changed some code in the TerrainBillboardGrass function. I don’t know if the naming matters, but if you put this in a file called WavingGrassBillboard.shader somewhere in your asset folder your grass should face the camera position instead of the camera view direction.

// Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)

Shader "Hidden/TerrainEngine/Details/BillboardWavingDoublePass" {
    Properties {
        _WavingTint ("Fade Color", Color) = (.7,.6,.5, 0)
        _MainTex ("Base (RGB) Alpha (A)", 2D) = "white" {}
        _WaveAndDistance ("Wave and distance", Vector) = (12, 3.6, 1, 1)
        _Cutoff ("Cutoff", float) = 0.5
    }
   
CGINCLUDE
#include "UnityCG.cginc"
#include "TerrainEngine.cginc"

        void TerrainBillboardGrassKritoa( inout float4 pos, float2 offset )
        {
            float3 grasspos = pos.xyz - _CameraPosition.xyz;
            if (dot(grasspos, grasspos) > _WaveAndDistance.w)
                offset = 0.0;

// aim at camera instead of orienting to camera plane. Probably looks better to use (0,1,0) instead of newup.

            float3 right = normalize(cross(float3(0, 1, 0), grasspos));
            float3 newup = normalize(cross(grasspos, right));

            pos.xyz += offset.x * right;
            pos.xyz += offset.y * newup;

//            pos.xyz += offset.x * _CameraRight.xyz;    // old method to orient to camera orientation
//            pos.xyz += offset.y * _CameraUp.xyz;
        }


void WavingGrassBillboardVertKritoa (inout appdata_full v)
{
    TerrainBillboardGrassKritoa (v.vertex, v.tangent.xy);
    // wave amount defined by the grass height
    float waveAmount = v.tangent.y;
    v.color = TerrainWaveGrass (v.vertex, waveAmount, v.color);
}

struct v2f {
    float4 pos : SV_POSITION;
    fixed4 color : COLOR;
    float4 uv : TEXCOORD0;
    UNITY_VERTEX_OUTPUT_STEREO
};
v2f BillboardVert (appdata_full v) {
    v2f o;
    UNITY_SETUP_INSTANCE_ID(v);
    UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
    WavingGrassBillboardVert (v);
    o.color = v.color;
   
    o.color.rgb *= ShadeVertexLights (v.vertex, v.normal);
       
    o.pos = UnityObjectToClipPos(v.vertex);   
    o.uv = v.texcoord;
    return o;
}
ENDCG

    SubShader {
        Tags {
            "Queue" = "Geometry+200"
            "IgnoreProjector"="True"
            "RenderType"="GrassBillboard"
            "DisableBatching"="True"
        }
        Cull Off
        LOD 200
        ColorMask RGB
               
CGPROGRAM
#pragma surface surf Lambert vertex:WavingGrassBillboardVertKritoa addshadow exclude_path:deferred
           
sampler2D _MainTex;
fixed _Cutoff;

struct Input {
    float2 uv_MainTex;
    fixed4 color : COLOR;
};

void surf (Input IN, inout SurfaceOutput o) {
    fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * IN.color;
    o.Albedo = c.rgb;
    o.Alpha = c.a;
    clip (o.Alpha - _Cutoff);
    o.Alpha *= IN.color.a;
}

ENDCG           
    }

    Fallback Off
}
2 Likes

Thank you for sharing your shader. I understood it and tried to modify TerrainBillboardTree in same way but trees still rotate.

void TerrainBillboardTree( inout float4 pos, float2 offset, float offsetz )
{
    float3 treePos = pos.xyz - _TreeBillboardCameraPos.xyz;
    float treeDistanceSqr = dot(treePos, treePos);
    if( treeDistanceSqr > _TreeBillboardDistances.x )
        offset.xy = offsetz = 0.0;

    float3 right = normalize(cross(float3(0, 1, 0), treePos));
    float3 newup = normalize(cross(treePos, right));

    // positioning of billboard vertices horizontally
//    pos.xyz += _TreeBillboardCameraRight.xyz * offset.x;
    pos.xyz += right * offset.x;

    // tree billboards can have non-uniform scale,
    // so when looking from above (or bellow) we must use
    // billboard width as billboard height
   
    // 1) non-compensating
    //pos.xyz += _TreeBillboardCameraUp.xyz * offset.y;
   
    // 2) correct compensating (?)
    //float alpha = _TreeBillboardCameraPos.w;
    //float a = offset.y;
    //float b = offsetz;
        // 2a) using elipse-radius formula
        ////float r = abs(a * b) / sqrt(sqr(a * sin(alpha)) + sqr(b * cos(alpha))) * sign(b);
        //float r = abs(a) * b / sqrt(sqr(a * sin(alpha)) + sqr(b * cos(alpha)));
        // 2b) sin-cos lerp
        //float r = b * sin(alpha) + a * cos(alpha);   
    //pos.xyz += _TreeBillboardCameraUp.xyz * r;
   
    // 3) incorrect compensating (using lerp)
    // _TreeBillboardCameraPos.w contains ImposterRenderTexture::GetBillboardAngleFactor()
    //float billboardAngleFactor = _TreeBillboardCameraPos.w;
    //float r = lerp(offset.y, offsetz, billboardAngleFactor);   
    //pos.xyz += _TreeBillboardCameraUp.xyz * r;
   
    // so now we take solution #3 and complicate it even further...
    //
    // case 49851: Flying trees
    // The problem was that tree billboard was fixed on it's center, which means
    // the root of the tree is not fixed and can float around. This can be quite visible
    // on slopes (checkout the case on fogbugz for screenshots).
    //
    // We're fixing this by fixing billboards to the root of the tree.
    // Note that root of the tree is not necessary the bottom of the tree -
    // there might be significant part of the tree bellow terrain.
    // This fixation mode doesn't work when looking from above/below, because
    // billboard is so close to the ground, so we offset it by certain distance
    // when viewing angle is bigger than certain treshold (40 deg at the moment)
   
    // _TreeBillboardCameraPos.w contains ImposterRenderTexture::billboardAngleFactor
    float billboardAngleFactor = _TreeBillboardCameraPos.w;
    // The following line performs two things:
    // 1) peform non-uniform scale, see "3) incorrect compensating (using lerp)" above
    // 2) blend between vertical and horizontal billboard mode
    float radius = lerp(offset.y, offsetz, billboardAngleFactor);
           
    // positioning of billboard vertices veritally
//    pos.xyz += _TreeBillboardCameraUp.xyz * radius;
    pos.xyz += newup * offset.y;

    // _TreeBillboardCameraUp.w contains ImposterRenderTexture::billboardOffsetFactor
    float billboardOffsetFactor = _TreeBillboardCameraUp.w;
    // Offsetting billboad from the ground, so it doesn't get clipped by ztest.
    // In theory we should use billboardCenterOffsetY instead of offset.x,
    // but we can't because offset.y is not the same for all 4 vertices, so
    // we use offset.x which is the same for all 4 vertices (except sign).
    // And it doesn't matter a lot how much we offset, we just need to offset
    // it by some distance
    pos.xyz += _TreeBillboardCameraFront.xyz * abs(offset.x) * billboardOffsetFactor;
}

Are you having problems with the actual billboarded trees - the ones really, really far away that are drawn with a single texture, or the trees with billboarded leaf and branch clusters - the trees up close that look mostly correct (trunk and branches and ~50% of the leaves look OK, and ~50% of the leaves rotate with camera)? I did an (awful) fix for geometry trees w/ billboarded leaf clusters that I could dig up later, but I haven’t looked at the full billboarded trees (I think my quality settings have an LOD bias that makes them never go to full billboard).

In any case, a cursory look of your code looks right. To be safe, I’d name the function something different (TerrainBillboardTreeAimino), and make sure that your shader calls that. I am unsure of the order of precedence if you have identically named functions.

I have problems with the full billboarded trees. I confirmed that my shader is called by changing some parameters. I don’t completely understand the mechanism of Unity’s billboard processing, but it seems to be rotated by another process.

I just tried out the billboarded trees and I was able to fix them by just going into Edit->ProjectSettings->Quality and toggling on the “Billboards Face Camera Position” button option.

1 Like

Thank you for trying that. I guess that you tried with SpeedTree billboards.

In this thread Unity stuff wrote that the quality option “Billboards face camera position” only works with SpeedTree billboards.
https://forum.unity3d.com/threads/unity-tree-billboard-bug-with-oculus-unity-what-is-going-on.406006/

For example, with this asset(set billboard start to 5), it doesn’t work.
https://www.assetstore.unity3d.com/jp/#!/content/6459

Hi, we’ve just published a package that supports billboards facing camera’s position, not orientation, that helps fixing that.
More info: [RELEASED] Massive Vegetation - ultra high performance billboard rendering system

Could you collaborate on that please? I am using this code which billboards good, but VR camera roll rotation is bad (pitch would be tolerable).

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(_Scale.x, _Scale.y, 1.0, 1.0));

For Unity particles, just click off the “Allow Roll” option in the particle system’s Renderer settings.
6334290--703041--upload_2020-9-21_10-54-20.png

If you’re writing a custom shader not for particles, you can use something like this:

Shader "Unlit/WorldUpBillboardTest"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _CameraFacing ("Camera Facing", Range(0,1)) = 1.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _CameraFacing;

            v2f vert (appdata v)
            {
                v2f o;

                // center camera position
                #ifdef UNITY_SINGLE_PASS_STEREO
                float3 camPos = (unity_StereoWorldSpaceCameraPos[0] + unity_StereoWorldSpaceCameraPos[1]) * 0.5;
                #else
                float3 camPos = _WorldSpaceCameraPos;
                #endif

                // world space mesh pivot
                float3 worldPivot = unity_ObjectToWorld._m03_m13_m23;

                // x & y axis scales only
                float3 scale = float3(
                    length(unity_ObjectToWorld._m00_m01_m02),
                    length(unity_ObjectToWorld._m10_m11_m12),
                    1.0
                    );

                // calculate billboard rotation matrix
                float3 f = normalize(lerp(
                    -UNITY_MATRIX_V[2].xyz, // view forward dir
                    normalize(worldPivot - camPos), // camera to pivot dir
                    _CameraFacing));
                float3 u = float3(0.0,1.0,0.0);
                float3 r = normalize(cross(u, f));
                u = -normalize(cross(r, f));
                float3x3 billboardRotation = float3x3(r, u, f);

                // apply scale, rotation, and translation to billboard
                float3 worldPos = mul(v.vertex.xyz * scale, billboardRotation) + worldPivot;

                // transform into clip space
                o.pos = UnityWorldToClipPos(worldPos);

                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}
4 Likes