What kind of shader creates this blured helicopter rotor?

Hello,

what kind of shader could create this rotating object (the mesh itself is stationary)? Could this be some kind of animated parallax mapping with transparency?

Thanks in advance for any ideas.9859839--1420266--Rotor_001.gif

1 Like

Could be parallax mapped. Or something cheaper. To me it looks like it’s maybe 2 different techniques.

On top you have a mesh that’s using a cylindrical UV mapping, and then blur it with multiple texture samples along the horizontal UV while also panning it.

This thread talks about radial blurring for tires, but it’d be done in a very similar way.

Under that it seems like each blade grip as a little more to it than just an external blurred shell would allow for. This could be done with parallax mapping of some kind, and doing the same blur / panning. But that’d distort quite badly and this isn’t showing those kinds of artifacts. So I suspect it’s either a low resolution voxel representation that’s being ray marched against (and blurred with the same multisample approach) or something even simpler like some basic horizontal planes or other simple shapes that are being raycast against (and blurred).

1 Like

Thank you for your reply.

I also think, it is more than a “external blurred shell”. The shader conveys a feeling of spatial depth and the reflections are also convincing.

The transition from slow rotations speeds with real 3D geometry to this blured representation happens imediately. I think, the strength of the blur effect is from there on constant and independent of changes of the rotational speed.

I noticed that the blured representation of the geometry changes depending on the positioning of the individual components. For example tilting the lower swashplate results also in tilting of the blured upper swashplate (see images below). This might be a hint that the basis could be shapes or voxels. But for me it does not look like (blurred) voxels.

I think they use the same shader for the blades, with aliasing artifacts visible there (see image).

Your last link “Pistons with Motion Blur” samples the whole scene at different times within one frame and mixes the result. Enabling the “useTemporalJitter” in that shader creates at least something similar, like the blades, because they also look from closer noisy. The spinning wheel example seams very simliar to the pistons, but only the UV map is rotated. Both approaches seem to me different to this shader.

What program is this? Is it a single player sim? If so, you could try to use something like Nvidia Nsight to inspect each frame and look at what’s happening. The shader code will all be in assembly, but you can look at what kind of data is being fed to it (meshes, textures, etc) which might give you more information about how it’s being done.

It is called DCS (API: D3D11). I have tested today PIX, Nsight and RenderDoc but only RenderDoc can start an older version of the game on Win10 in VR mode, whereby the game crashes when a mission starts or nothing is captured.

Finaly I got RenderDoc working and captured a frame FrameCapture (537 MB) :slight_smile:

If I understand correctly in Color Pass #13 the 3d rotor-hub is rendered and in Pass #14 / DrawIUndexed(288) / PixelShader 36004 uses this texture and calculates the blurring effect. I’m a newbie to shaders and I’m wondering if it’s possible to create this blur effect really with this short code.

I think Color Pass #13 / texProject is projected onto the cylinder (yellow mesh) 64 times while rotating the cylinder’s TEXTCOORD0 (-20deg … +20 deg) and then mixing the resulting color through a weighted sinusoidal curve. @bgolus , to me this part looks very similar to your radial blurring for tires rotateUV function you mentioned. But there are many small steps that I don’t understand (marked with ???) and in particular lines 91… 97.:eyes:

It is the first time I read asm code, therefore I tried to transfer it in MATLAB. Swizzling doesn’t work there, and maybe there is a better way to test code? At the end of this post you can see the weighting curve (r0.w).

// MATLAB code (replaced comment % by //)
//
// pixel shader input ---------------------------------------------------
// TEXCOORD0 v0.xyz float3 1.0691, 0.10849, 0.11406
// TEXCOORD1 v1.xyz float3 -14.42381, 21.19727, -5.75075
// TEXCOORD2 v2.xyzw float4 -2.26694, 7.94644, 0.02, 13.64417
// SV_POSITION v3.xyzw float4 800.50, 225.50, 0.00147, 13.64417
// texProject t0 Resource 2D Render Target 36041
// sbAtmosphereSamples t119 Resource Buffer 31539
// gTrilinearClampSampler s12 Sampler Sampler State 74
//
// // "Globals"
// Name,Value,Byte Offset,Type
// atmosphereSamplesId,"4, 1",0,int2
// bladePos,,16,float4x4 (column_major)
// bladePos.row0,"0.87316, -0.02592, 0.48674, 0.00",,float4
// bladePos.row1,"0.02264, 0.99966, 0.01262, 0.00",,float4
// bladePos.row2,"-0.48691, 0.00, 0.87345, 0.00",,float4
// bladePos.row3,"-0.97587, 0.02048, -0.53288, 1.00",,float4
// modelPos,,80,float4x4 (column_major)
// modelPos.row0,"-0.7303, 0.05582, -0.68084, 0.00",,float4
// modelPos.row1,"0.04083, 0.99844, 0.03807, 0.00",,float4
// modelPos.row2,"0.68191, -5.15716E-08, -0.73144, 0.00",,float4
// modelPos.row3,"-13.72525, 21.02928, -4.94356, 1.00",,float4
// viewProj,,144,float4x4 (column_major)
// viewProj.row0,"-0.0574, 0.00949, -0.14735, 0.00",,float4
// viewProj.row1,"-0.00171, 0.30021, 0.005, 0.00",,float4
// viewProj.row2,"0.29484, 0.00359, -0.02866, 0.00",,float4
// viewProj.row3,"1.84859, -6.33603, -2.05326, 1.00",,float4
// sampleMatrix,,208,float4x4 (column_major)
// sampleMatrix.row0,"-0.23928, -0.01111, 0.38364, 0.00",,float4
// sampleMatrix.row1,"0.0108, -0.45211, -0.00636, 0.00",,float4
// sampleMatrix.row2,"-0.38365, -0.00579, -0.23945, 0.00",,float4
// sampleMatrix.row3,"0.50, 0.50, 0.50, 1.00",,float4
// instanceCount,"1",272,uint
// texScale,"0.53906",276,float
// sigma,"0.35468",280,float
// scaleY,"1.95604",284,float
// sagging,"0.15431, 0.00, 0.00",288,float3
// cScale,"0.00, 0.00, 0.00",304,float3
// flirCoeff,"0.00, 0.00, 0.00, 0.00",320,float4
// -------------------------------------------------------------------------


// pixel shader ------------------------------------------------------------
clear all; close all; clc
x=1; y=2; z=3; w=4;
plot_curve1=[]; plot_curve2=[]; plot_curve3=[];

// input values
v0           = [1.0691, 0.10849, 0.11406];          // TEXCOORD0
sigma        = [0.35468];                           // seems to be constant, controls weighting function 
texScale     =  0.53906;                            // ?????
sampleMatrix = [-0.23928, -0.01111,  0.38364, 0.00; // ?????
                 0.0108,  -0.45211, -0.00636, 0.00;
                -0.38365, -0.00579, -0.23945, 0.00;
                 0.50,     0.50,     0.50,    1.00]; // det(sampleMatrix(1:3,1:3)) = -0.0925   ?!!!!

// initialize variables
r0 = [sigma(x)^2 * 0.5, 0, 0, 0];   // ?????
r1 = [0, v0(y), 0, 1.0];       
r2 = [0, 0, 0, 0];

while 1
    // loop for 64 steps
    if (r0(z) >= 64); r0(w) = 1; else; r0(w) = 0; end
    if (r0(w) ~= 0); break; end
 
    // linear increasing counter
    r0(w)   = (floor(r0(z)) + 0.5000) * 1/64 - 0.5000;  // -0.5000 ... 0.5000 in 64 steps
    r3(x)    = r0(w) * 40*pi/180;      // [rad] 0.6981 rad ~= 40.0 deg ??
    plot_curve1(end+1) = r3(x);

    // sinus-like curve --> weighting for color mixing ??
    r0(w)   = 2^( (-(r0(w)^2) / r0(x)) * 1.4427 );          // 1.4427  ???
    plot_curve2(end+1) = r0(w);
    plot_curve3(end+1) = r0(y);

    // rotation around y-axis --> rotates TEXCOORD0 of cylinder ?????
    // Ry = [cos(theta)   0   sin(theta)
    //          0          1    0
    //         -sin(theta)    0   cos(theta)]
    // r1 = Ry * v0
    r1(x) = v0(x) * cos(r3(x)) + 0            + v0(z) * sin(r3(x));
    r1(y) = 0                   + v0(y)       + 0                        ;
    r1(z) = v0(x) * -sin(r3(x)) + 0             + v0(z) * cos(r3(x));
    r1(w) = 1.0;
 
    // ?????
    r3(x) = dot( sampleMatrix(1,:), r1 );
    r3(y) = dot( sampleMatrix(2,:), r1 );

    r1(x) = dot( sampleMatrix(4,:), r1 );

    // ?????
    r1 = [ (r3(x)/r1(x))*texScale, r1(y) , (r3(y)/r1(x))*texScale, r1(z) ];
 
    // ?????
    // sample_l(texture2d)(float,float,float,float) r3.xyzw, r1.xzxx, texProject.xyzw, gTrilinearClampSampler, l(0)
    r3 = [0,0,0,0];  // don't know what r3 is yet, because sample_l(...) must be coded in Matlab
                      
    // mix color  (mad r2.xyzw, r3.xyzw, r0.wwww, r2.xyzw)
    r2 = r3 .* r0(w) + r2;

    // acculumate sinus-like curve
    r0(y) = r0(w) + r0(y);
 
    // increase loop counter r0(z)++
    r0(z) = r0(z) + 1;
end

r0 = r2 ./ r0(y);       //  div r0.xyzw, r2.xyzw, r0.yyyy

// ?????
// 34: ld_structured_indexable(structured_buffer, stride=36)(mixed,mixed,mixed,mixed) r1.xyz, atmosphereSamplesId.x, l(24), sbAtmosphereSamples.xyzx
r1 = [0,0,0,0];  // don't know what r1 is

o0 = r1 .* r0(w) + r0;    //mad o0.xyz, r1.xyzx, r0.wwww, r0.xyzx
o0(w) = r0(w);
// -------------------------------------------------------------------------


// plot --------------------------------------------------------------------
figure;
subplot(2,1,1);hold on; grid on; xlabel('loop nr (1...64)'); // xlim([0,64])
h1 = plot(plot_curve1,'r.-','DisplayName','r3.x');
h2 = plot(plot_curve2,'b.-','DisplayName','r0.w');
legend show
subplot(2,1,2);hold on; grid on; xlabel('loop nr (1...64)'); // xlim([0,64])
h3 = plot(plot_curve3,'b.-','DisplayName','r0.y');
legend show
// -------------------------------------------------------------------------

Yep, your reading of what it’s doing seems to be accurate.

The dot products at line 91 are a matrix multiply.

97 is doing something like parallax offset mapping.

But I’m not entirely sure what space it’s transforming the offset vector into. It has to be something like transforming from mesh object space to screen space.

Thanks, but going one step back to the rotation matrix in line 85…87: This is a 3D-rotation in contrast to the 2D-rotation in your radial bluring for tires function. The pixel shader’s dcl_input_ps linear v0.xyz (in line 52) has also three components?! If I plot the vertex shader’s output, all TEXCOORDx have at least three components, see 3D pictures below. It turns out that for some reason TEXCOORD0 is a copy of the cylinder’s object space POSITION coordinates, and the pixel/fragmet shaders input v0.xyz is a point on the cylinder’s surface (in object space).

Is this all normal? Why is TEXCOORD0 three dimensional? From where does the pixel/fragmet shader get its v0.xyz?

It’s passing along the local space vertex position.

“Texcoords” on vertex shader outputs are used for any arbitrary data being passed between the vertex and fragment. Sometimes it’s actually a texture coordinate, but it doesn’t have to be.

So I think what the fragment shader is doing is taking the interpolated local vertex position, rotating it a bit, and calculating the resulting render texture screen space position. Then sampling the low res render texture at that position. So the sample matrix must be an object space to render texture projection space transform they’re calculating and passing in.

1 Like

Thank you very much for the explanations. I think it is exactly how it works!

I have attached the result that I am happy with. Small details still missing:

  • What the “ld_structured_indexable( … atmosphereSamples…)” at the end of the asm does is not clear so far.
  • In the original version there is also a slight noisy/spinning (~5Hz) motion that better mimics the rotation.
    Maybe there is potential for speed optimization somewhere.

In Unity I used a second camera, attached to the main camera, while a render texture (Color format: R32G32B32A32_SFLOAT) is the Capture Camera’s target texture. Rotor and cylinder have their own layers, I also set each camera’s culling mask accordingly.
A c# script rotates and focuses the Capture Camera at the rotor. It also passes the Capture Camera’s transformation matrix (aka sampleMatrix) to the shader.

Thank you bgolus for your help!

The cylinder’s shader:

Shader "Unlit/RotationBlurShader"
{
    Properties
    {
        [NoScaleOffset] _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags {"Queue"="Transparent" "IgnoreProjector"="False" "RenderType"="Transparent" "DisableBatching" = "True"}
        ZWrite On Lighting Off  Fog { Mode Off } Blend One OneMinusSrcAlpha

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            // Projection matrix of the camera acting as a projector * world to camera (acting as a projector) matrix  *  ObjectToWorld
            float4x4 _ProjectionMatrix_times_WorldToCameraMatrix_times_ObjectToWorld;
            float _sigma_mod = 22.9368; // []
            float _spreading = 40; // [deg]

            #pragma multi_compile_fwdbase nolightmap nodirlightmap nodynlightmap novertexlight
            struct v2f
            {
                float4 pos : SV_POSITION;
                float4 oPos : TEXCOORD0;     // Object (cylinder) position in object space
            };

            v2f vert (appdata_base v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.oPos = v.vertex; // v.vertex is the local vertex coordinte           
                return o;
            }

            inline float4 RotateAroundYInRad (float4 vertex, float theta)
            {
                float sint, cost;
                sincos(theta, sint, cost);
                float2x2 m = float2x2(cost, -sint, sint, cost);
                return float4(mul(m, vertex.xz), vertex.yw).xzyw;
            }

            sampler2D _MainTex;

            // ©bgolus: "The fragment shader is taking the interpolated
            //           local vertex position, rotating it a bit, and calculating
            //           the resulting render texture screen space position. Then
            //           sampling the low res render texture at that position. So
            //           the sample matrix must be an object space to render texture
            //           projection space transform they're calculating and passing in."
            fixed4 frag (v2f i) : SV_Target
            {
                #define NUMBER_OF_STEPS 64
                //#define ROTATION_RANGE 40
               
                float4 o0 = {0.0, 0.0, 0.0, 0.0};                               // fragment color
                float linear_curve = 0.0;                                       // linearly growing variable (from -0.5 ... +0.5 in 64 steps)
                float weight = 0.0;                                             // color mixing weight of sample
                float normalize_weight = 0.0;                                   // accumulated color mixing weight for normalizing result
                float theta = 0.0;                                              // [rad] rotation of cylinder

                for ( int s=0; s<NUMBER_OF_STEPS; s++ )
                {
                    linear_curve = (s + 0.5f) / NUMBER_OF_STEPS - 0.5f;         // linearly growing variable (from -0.5 ... +0.5 in 64 steps)   
                    //theta = linear_curve * ROTATION_RANGE * UNITY_PI / 180.0f;  // [rad] rotate the vertex (-20 deg .. 20 deg)
                    theta = linear_curve * _spreading * UNITY_PI / 180.0f;      // [rad] rotate the vertex (-20 deg .. 20 deg)
                    //weight = pow(2, ( -pow(linear_curve, 2) / (_sigma * _sigma * 0.5f) )  * 1.4427f);        // creates a sinusoidal curve y = (~0...1), weighting the center of frame at most
                    weight = pow(2, ( -pow(linear_curve, 2) * _sigma_mod ));    // simplified above linecreates a sinusoidal curve y = (~0...1), weighting the center of frame at most

                    float4 oVertex = RotateAroundYInRad(i.oPos, theta);         // rotate cylinder's vertex around local y axis

                    float4 renderTextureProjectorSpace =
                      mul(_ProjectionMatrix_times_WorldToCameraMatrix_times_ObjectToWorld, oVertex);// clipped position in projector space (principally same as UNITY_MATRIX_MVP but for capture_camera)

                    float4 o = renderTextureProjectorSpace * 0.5f;              // https://discussions.unity.com/t/731961
                    o.xy = float2(o.x, o.y) + o.w;
                    o.w = renderTextureProjectorSpace.w;

                    fixed4 col = tex2D(_MainTex, o.xy/o.w);                     // sampling the low res render texture         
                    o0 += col * weight;                                         // mix color   
                    normalize_weight += weight;                                 // summ up weight
                }

                return o0 / normalize_weight;                                   // normalize color and alpha
            }
            ENDCG
        }
    }
}

The c# script attached to the cylinder GameObject:

using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using TMPro;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;

public class SetProjectorToShader : MonoBehaviour
{
    public Camera capture_camera;
    public Material cylinder_material;
    public GameObject rotor;
    public GameObject cylinder;

    private Slider slider_rpm;
    private Slider slider_sigma;
    private Slider slider_spreading;
    private TextMeshProUGUI slider_rpm_value;
    private TextMeshProUGUI slider_sigma_value;
    private TextMeshProUGUI slider_spreading_value;

    private float ZoomAmount = 20;

    void Awake()
    {
        slider_rpm              = GameObject.Find("Slider_rpm").GetComponent<Slider>();
        slider_sigma            = GameObject.Find("Slider_sigma").GetComponent<Slider>();
        slider_spreading        = GameObject.Find("Slider_spreading").GetComponent<Slider>();
        slider_rpm_value        = GameObject.Find("Text_rpm (1)").GetComponent<TextMeshProUGUI>();
        slider_sigma_value      = GameObject.Find("Text_sigma (1)").GetComponent<TextMeshProUGUI>();
        slider_spreading_value  = GameObject.Find("Text_spreading (1)").GetComponent<TextMeshProUGUI>();

        FollowAndFocusOn(capture_camera, rotor, 0.65f);
    }

    public static Bounds GetBoundsWithChildren(GameObject gameObject)
    {
        Renderer[] renderers = gameObject.GetComponentsInChildren<Renderer>();
        Bounds bounds = renderers.Length > 0 ? renderers.FirstOrDefault().bounds : new Bounds();

        for (int i = 1; i < renderers.Length; i++)
        {
            if (renderers[i].enabled)
            {
                bounds.Encapsulate(renderers[i].bounds);
            }
        }
        return bounds;
    }
    public static void FollowAndFocusOn(Camera camera, GameObject focusedObject, float spacingfactor)
    {
        Bounds bounds = GetBoundsWithChildren(focusedObject); // Debug.Log(bounds.extents.magnitude);
        float aspectRatio = 1; // Screen.width / Screen.height;
        float distance = (camera.transform.position - focusedObject.transform.position).magnitude;
        camera.fieldOfView = 2.0f * Mathf.Rad2Deg * Mathf.Atan((0.5f * bounds.extents.magnitude) / (distance * aspectRatio * spacingfactor));
        camera.transform.LookAt(focusedObject.transform);
    }

    void Update()
    {

        slider_rpm_value.text = slider_rpm.value.ToString("0");
        slider_sigma_value.text = slider_sigma.value.ToString("0.00");
        slider_spreading_value.text = slider_spreading.value.ToString("0");

        // demonstration: move camera
        //Camera.main.transform.RotateAround(Camera.main.transform.position, -UnityEngine.Vector3.up, -Input.GetAxis("Mouse X") * 0.25f);
        //Camera.main.transform.RotateAround(Camera.main.transform.position, transform.right, Input.GetAxis("Mouse Y") * 0.25f);
        Camera.main.fieldOfView = Mathf.Clamp(ZoomAmount += Input.GetAxis("Mouse ScrollWheel")*10, 5, 100);
        // demonstration: rotate rotor slowly to shwo effect from different angles
        rotor.transform.Rotate(0, -slider_rpm.value * 6.0f * Time.deltaTime  ,0 , Space.Self);

        ///////////////////////////////////////////////////////////////////////////////////////
        // capture camera must follow rotor and change FOV
        FollowAndFocusOn(capture_camera, cylinder, 0.65f);
        // pass matrix and parameter to shader
        cylinder_material.SetMatrix("_ProjectionMatrix_times_WorldToCameraMatrix_times_ObjectToWorld", capture_camera.projectionMatrix * capture_camera.worldToCameraMatrix * cylinder.transform.worldToLocalMatrix.inverse);
        cylinder_material.SetFloat("_sigma_mod", 1.0f / (Mathf.Pow(slider_sigma.value, 2.0f) * 0.5f / 1.4427f)); //
        cylinder_material.SetFloat("_spreading", slider_spreading.value ); //
        ///////////////////////////////////////////////////////////////////////////////////////
     
        if (Input.GetKeyDown(KeyCode.Escape))
            Application.Quit();
    }
}

Sources:

4 Likes

Likely for handling their atmosphere & cloud rendering systems. Opaque things will just have clouds rendered over them, but anything that’s using a transparent blend need to be able to re-add clouds “over” themselves.

That’s on the blades though, not on the rotorhead. I suspect those use a slightly different shader.

The atmospheric data now makes sense. Thanks.

What I meant with “noisy/spinning (~5Hz) motion” can be seen below.

In the original shader there was a variable called sigma, which I abbreviated because I thought it was constant. I think this variable causes the flickering effect. I adapted the shader (line 70) and c# code (line 60) in posting #12 (and also simplified it by moving the transformation matrix multiplication … * ObjectToWorld … to the c# script).

9875079--1423869--Rotor.gif

The sigma is the falloff for the blur and determines how “thick” the blur looks.

That flickering looks like aliasing caused by the fact they render the rotorhead at a lower resolution.

this was fun to read.

Yes, the sigma only controls the blur strength, and you’re right, the flickering comes from this low resolution texture projector blurring approach.

One (I hope) last thing:
The shader also projects the render texture onto the back of the cylinder (see picture, left side). The game doesn’t do that. The result is some unsightly dark areas.

I could convert the local cylinder normal to world coordinates and test against the camera direction. But the game’s shader doesn’t do that either and they still don’t have the effect. Can this behavior be controlled by some settings?

Remove Cull Off

1 Like

bgolus, thank you for your time and help. I really appreciate that.

The Unity project can be found here:
https://github.com/zulugithub/RotorBlur

3 Likes