​
Showing posts with label Shaders. Show all posts
Showing posts with label Shaders. Show all posts

Saturday, July 10, 2021

Gravity Part 3: Create a force map and display it (CPU, Compute Shader and Fragment Shader)

Introduction

This is the third and last post in the series I am creating, based on the logic from Physics-based Solar System Lessons:
The interesting thing about this post is that there is no intrinsic changes to the physics or calculations. This is just a way to visualize forces in texture.

Gravity Part 3: Create a force map and display it

In Gravity Part 1 I polished a system that creates movement out of objects due to their gravitational force. Wouldn't it be interesting to visualize these forces somehow? This is not difficult but has a very big problem: these calculations need to be reevaluated every frame and then painted to a texture.

Artistic color representation for force direction by Shutterstock


To visualize this, I created 3 independent systems that calculate the gravity forces and then paint the texture:
  • CPU: CPU calculation, CPU painting.
  • Compute Shader: Compute Shader calculation, CPU painting.
  • Fragment Shader: Fragment Shader calculation, Fragment Shader painting.
For each case I will show calculations, source code and results. At the end I will compare all three cases and give a conclusion.
 

Initial Setup

First we need to create a Plane with the dimensions we want it to cover, and then attach the desired script to be used. The beautiful thing about the current approach is that the initial setup is almost the same for each case of texture painting.
  • CPU: enable script. Material set to Unlit/Texture.
  • Compute shader: enable script. Material set to Unlit/Texture.
  • Fragment Shader: enable script. Material set to Gravity/ForceMap.
For easier visualization check the following screenshots:

Setup for CPU/Compute Shader scripts and texture

Setup for Fragment Shader script and texture


General remarks

Force representation: In the texture we will be using a clamped logarithm scale. Following Newton’s gravity force equation produces an exponential function, which means there will be a lot of near-zero values that will not be properly displayed (but are relevant).

\[F = G\dfrac{m_1 m_2}{r^2}\]  

If we just use these values to paint, we will mostly see a bright color area around large gravity sources and everything else will be mostly blackish. By using a logarithm scale, we can see more clearly the dominant force direction in each point. If the color is close to black, then there is virtually no net force (specially in a Log scale).

Log(x) by WolframAlpha

Color representation: I will be using an RGB color representation to visualize 4 force directions (we are painting on a 2D plane, so we will ignore anything from the third axis):
  • Red: Force towards “left” of texture.
  • Green: Force towards “right” of texture.
  • Blue: Force towards “top” of texture.
  • Yellow: Force towards “bottom” of texture.
Below is the Force map texture legend:


Directions of forces are represented by  colors

Black means neutral direction of force.

CPU

This is the simplest approach; I directly use the logic I had already created in previous posts. For simplicity I redesigned all the logic within the same script, so everything is just in one place. The logic is:
  1. Initialization:
    1. Setup texture size and properties.
    2. Create world points for the texture.
  2. Update texture:
    1. Calculate forces: For each texture pixel get the net force.
    2. Paint forces: For each net force paint in the texture with a certain color
    3. Update texture from painted pixels.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
using System;
using System.Linq;
using UnityEngine;

public class ForceMapCpu : MonoBehaviour
{
    [Header("Texture details")]
    [SerializeField] private Vector2 _textureResolution = new Vector2(1000, 1000);
    [Tooltip("Regardless of texture Y position, do the calculations as if Y = value")]
    [SerializeField] private float _positionTextureY = 0; 

    private Texture2D _texture;
    private Color[] _texturePixelsColor; //All the points in the texture with a set color in each pixel
    private Vector3[] _texturePixelPositions; //Each Pixel in the texture is mapped to a world coordinate

    private float _maxForce; //Used to clamp the force visualization
    private SpaceObject[] _spaceObjects;

    private float _time;
    private float _waitSeconds = 0.01f;

    void Start()
    {
        _time = Time.realtimeSinceStartup;

        var spaceObjects = GameObject.FindGameObjectsWithTag("SpaceObject").Where(o => o.gameObject != gameObject).ToList();
        _spaceObjects = spaceObjects.Select(p => p.GetComponent<SpaceObject>()).ToArray();
        _maxForce = GetMaximumGravity();

        _texture = new Texture2D((int)_textureResolution.x, (int)_textureResolution.y)
        {
            wrapMode = TextureWrapMode.Clamp,
            filterMode = FilterMode.Bilinear
        };

        GetComponent<Renderer>().material.mainTexture = _texture;
        
        _texturePixelsColor = new Color[_texture.height * _texture.width];
        _texturePixelPositions = GetTextureWorldPoints();
    }

    void Update()
    {
        if (_time + _waitSeconds <= Time.realtimeSinceStartup)
        {
            PaintForceMap();

            _time = Time.realtimeSinceStartup;
        }
    }

    private float GetMaximumGravity()
    {
        var maxForce = float.MinValue;

        foreach (var spaceObject in _spaceObjects)
        {
            var force = GetGravity(spaceObject, spaceObject.transform.position + new Vector3(1.0f, 1.0f, 1.0f));

            if (maxForce <= force.magnitude)
            {
                maxForce = force.magnitude;
            }
        }

        return maxForce;
    }

    // Apply Log to the gravity to ease visualization of the force
    private Vector3 GetGravity(SpaceObject spaceObject, Vector3 atPoint)
    {
        var direction = spaceObject.transform.position - atPoint;
        var gravityLog = Math.Log(1 + spaceObject.GetGravitationalPullForce(direction.magnitude));

        return direction.normalized * (float)gravityLog;
    }


    private Vector3[] GetTextureWorldPoints()
    {
        var worldPoints = new Vector3[_texture.height * _texture.width];

        for (int i = 0; i < _texture.height; i++)
        {
            for (int j = 0; j < _texture.width; j++)
            {
                var localPoint = TransformTextureCoordinateToLocalCoordinates(i, j, _texture);
                var worldPoint = transform.TransformPoint(localPoint);
                worldPoint.y = _positionTextureY; //We want to check with respect to reference planet (usually Earth) and not actual position of texture
                worldPoints[j * _texture.height + i] = worldPoint;
            }
        }

        return worldPoints;
    }

    // From i,j position in texture to a local Vector3
    private Vector3 TransformTextureCoordinateToLocalCoordinates(int i, int j, Texture2D texture)
    {
        var posX = 5 - 10 * (i + 0.5f) / texture.height;
        var posZ = 5 - 10 * (j + 0.5f) / texture.width;

        return new Vector3(posX, 0, posZ);
    }

    private void PaintForceMap()
    {
        for (int i = 0; i < _texture.height; i++)
        {
            for (int j = 0; j < _texture.width; j++)
            {
                var clampedForce = GetGravityAtPoint(_texturePixelPositions[j * _texture.height + i]) / _maxForce;
                var color = GetColorFromForce(clampedForce);

                _texturePixelsColor[j * _texture.height + i] = color;
            }
        }

        _texture.SetPixels(_texturePixelsColor);
        _texture.Apply();
    }

    private Vector3 GetGravityAtPoint(Vector3 point)
    {
        var netForce = new Vector3();

        foreach (var spaceObject in _spaceObjects)
        {
            var force = GetGravity(spaceObject, point);
            netForce += force;
        }

        return netForce;
    }

    private Color GetColorFromForce(Vector3 force)
    {
        var colorForce = Color.black;

        if (force.z > 0.0f && force.x > 0.0f)
        {
            colorForce.b = force.z;
            colorForce.g = force.x;
        }
        else if (force.z > 0.0f && force.x <= 0.0f)
        {
            colorForce.b = force.z;
            colorForce.r = -force.x;
        }
        else if (force.z <= 0.0f && force.x > 0.0f)
        {
            colorForce.r = -force.z;
            colorForce.g = Mathf.Sqrt(Mathf.Pow(force.z, 2) + Mathf.Pow(force.x, 2));
        }
        else if (force.z <= 0.0f && force.x <= 0.0f)
        {
            colorForce.r = Mathf.Sqrt(Mathf.Pow(force.z, 2) + Mathf.Pow(force.x, 2));
            colorForce.g = -force.z;
        }

        return colorForce;
    }
}


How to paint pixels in a texture. Technically there are two easy ways to change pixels in a texture in Unity [1]:
  • SetPixel: only sets 1 pixel at a time.
  • SetPixels: set a whole block of pixels. I used this as it is more efficient in our case.

Optimization

When I first created this script I used small texture sizes, and still got decent framerates (60+ FPS), as I increased the texture size the FPS reduced drastically. One way to optimize it is not to calculate in every update call, but whenever is necessary. That is why I am using a time delay to paint the force map.

Compute Shader

The logic here is a bit more complex. We have 2 scripts:
  • CSForceMap.compute: This is a file that uses shader language and will be ran in the GPU.
  • ForceMapComputeShader.cs: This script loads the information into the GPU, retrieves it and then paints it.
The compute shader has all the logic to calculate the gravity source per location. The key here is to update the gravity sources when needed (usually during an Update) and do every pixel calculation in the GPU. For this there are a few variables that depend on outside parameters. I use arrays that can be Read/Write capability from inside/outside the GPU (RWStructuredBuffer<type>). As a reference I used Unity official’s documentation [2], Ronja’s tutorials [3] and Nvidia’s reference [6].

CSForceMap.compute
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#pragma kernel CSForceMap

struct Source
{
    float3 position;
    float mu;
};

//Constants. Set in initialization
uint sizeX;
uint sizeY;
uint numSources;
float maxForce;
RWStructuredBuffer<float3> pixelPositions;

//Dynamic variables
RWStructuredBuffer<Source> gravSources; //This gets updated asyncronously 
RWStructuredBuffer<float4> forcesInColor; //Contains the data to be retrieved from the C# script

float4 GetColorFromGravity(float3 force)
{
    float4 colorForce = float4(0, 0, 0, 0);

    if (force.z > 0.0f && force.x > 0.0f)
    {
        colorForce.b = force.z;
        colorForce.g = force.x;
    }
    else if (force.z > 0.0f && force.x <= 0.0f)
    {
        colorForce.b = force.z;
        colorForce.r = -force.x;
    }
    else if (force.z <= 0.0f && force.x > 0.0f)
    {
        colorForce.r = -force.z;
        colorForce.g = sqrt(pow(force.z, 2) + pow(force.x, 2));
    }
    else if (force.z <= 0.0f && force.x <= 0.0f)
    {
        colorForce.r = sqrt(pow(force.z, 2) + pow(force.x, 2));
        colorForce.g = -force.z;
    }

    return colorForce;
}

[numthreads(32, 1, 1)]
void CSForceMap(uint3 id : SV_DispatchThreadID)
{
    float3 netForce = float3(0, 0, 0);

    for (uint k = 0; k < numSources; ++k)
    {
        float3 forceDirection = (gravSources[k].position - pixelPositions[id.x]);
        float gravityLog = log(1 + gravSources[k].mu / dot(forceDirection, forceDirection));
        netForce += normalize(forceDirection) * gravityLog;
    }

    forcesInColor[id.x] = GetColorFromGravity((netForce / maxForce));
}


The other piece of code has a very similar logic to the previous implementation using only CPU:
  1. Initialization:
    1. Setup texture size and properties.
    2. Create world points for the texture.
    3. Initialize Compute Shader variables: We need to set the dynamic variables before executing the compute shader.
  2. Update Compute Shader variables:
    1. Send updated gravity sources to GPU.
    2. Compute shader doing magic: Note that this is happening in parallel to this list.
    3. Retrieve texture pixel values (gravity forces are now colors)
    4. Update texture from retrieved pixels.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
using System;
using System.Linq;
using UnityEngine;

public class ForceMapComputeShader : MonoBehaviour
{
    [Header("Texture details")]
    [SerializeField] private Vector2 _textureResolution = new Vector2(512, 512);
    [Tooltip("Regardless of texture Y position, do the calculations as if Y = value")]
    [SerializeField] private float _positionTextureY = 0;

    [Header("Compute Shader")]
    [SerializeField] private ComputeShader computeShader;
    private int _computeShaderKernel;

    private ComputeBuffer _bufferPixelPositions; //Pixels positions used to send to the computer shader. Modified once
    private ComputeBuffer _bufferGravSources; //Gravitation sources used to send to the computer shader. Modified in each update
    private ComputeBuffer _bufferForcesInColor; //Color forces from the compute shader used. Modified in each update
    private Color[] _outputForcesInColor;

    private Texture2D _texture;
    private int _pixelCount;

    private float _maxForce;
    private SpaceObject[] _spaceObjects;
    private Source[] _gravitySources; // All objects with gravity. Store their position and mu (GravConstant*mass)

    private float _time;
    private float _waitSeconds = 0.01f;


    [Serializable]
    public struct Source
    {
        public Vector3 position;
        public float mu; //GravConstant*mass

        public Source(Vector3 position, float mu)
        {
            this.position = position;
            this.mu = mu;
        }
    }

    void Start()
    {
        _time = Time.realtimeSinceStartup;

        var spaceObjects = GameObject.FindGameObjectsWithTag("SpaceObject").Where(o => o.gameObject != gameObject).ToList();
        _spaceObjects = spaceObjects.Select(p => p.GetComponent<SpaceObject>()).ToArray();
        _maxForce = GetMaximumGravity();

        _texture = new Texture2D((int)_textureResolution.x, (int)_textureResolution.y)
        {
            wrapMode = TextureWrapMode.Clamp,
            filterMode = FilterMode.Bilinear
        };

        GetComponent<Renderer>().material.mainTexture = _texture;

        _gravitySources = GetSourcesData();

        InitializeComputeShaderForceMap();
    }

    void Update()
    {
        if (_time + _waitSeconds <= Time.realtimeSinceStartup)
        {
            UpdateComputeShaderForceMap();
            _time = Time.realtimeSinceStartup;
        }
        //UpdateComputeShaderForceMap();
    }

    void OnApplicationQuit()
    {
        _bufferPixelPositions?.Dispose();
        _bufferGravSources?.Dispose();
        _bufferForcesInColor?.Dispose();
    }

    private Source[] GetSourcesData()
    {
        var sources = new Source[_spaceObjects.Length];
        var gravitationalConstant = GameObject.Find("GameManager").GetComponent<GameManager>().GravitationalConstantUnityScaled;

        for (int i = 0; i < _spaceObjects.Length; i++)
        {
            sources[i].position = _spaceObjects[i].transform.position;
            sources[i].mu = (float)(_spaceObjects[i].Mass * gravitationalConstant);
        }

        return sources;
    }

    // This is important to initialize the variables in the compute shader.
    // Specially important to set the arrays size to be used in the compute shader.
    private void InitializeComputeShaderForceMap()
    {
        _pixelCount = _texture.height * _texture.width;

        _bufferPixelPositions = new ComputeBuffer(_pixelCount, sizeof(float) * 3);
        _bufferGravSources = new ComputeBuffer(_pixelCount, sizeof(float) * 4);
        _bufferForcesInColor = new ComputeBuffer(_pixelCount, sizeof(float) * 4);

        var texturePixelPositions = GetTextureWorldPoints();
        _bufferPixelPositions.SetData(texturePixelPositions);

        _computeShaderKernel = computeShader.FindKernel("CSForceMap");
        computeShader.SetBuffer(_computeShaderKernel, "gravSources", _bufferGravSources);
        computeShader.SetBuffer(_computeShaderKernel, "pixelPositions", _bufferPixelPositions);
        computeShader.SetBuffer(_computeShaderKernel, "forcesInColor", _bufferForcesInColor);

        computeShader.SetInt("numSources", _gravitySources.Length);
        computeShader.SetInt("sizeX", _texture.height);
        computeShader.SetInt("sizeY", _texture.width);
        computeShader.SetFloat("maxForce", _maxForce);

        _outputForcesInColor = new Color[_texture.height * _texture.width];
    }

    private Vector3[] GetTextureWorldPoints()
    {
        var worldPoints = new Vector3[_texture.height * _texture.width];

        for (int i = 0; i < _texture.height; i++)
        {
            for (int j = 0; j < _texture.width; j++)
            {
                var localPoint = TransformTextureCoordinateToLocalCoordinates(i, j, _texture);
                var worldPoint = transform.TransformPoint(localPoint);
                worldPoint.y = _positionTextureY; //We want to check with respect to reference planet (usually Earth) and not actual position of texture
                worldPoints[j * _texture.height + i] = worldPoint;
            }
        }

        return worldPoints;
    }

    // From i,j position in texture to a local Vector3
    private Vector3 TransformTextureCoordinateToLocalCoordinates(int i, int j, Texture2D texture)
    {
        var posX = 5 - 10 * (i + 0.5f) / texture.height;
        var posZ = 5 - 10 * (j + 0.5f) / texture.width;

        return new Vector3(posX, 0, posZ);
    }

    // Update Compute Shader variable: gravity sources.
    // Retrieve from Compute Shader: _bufferForcesInColor and sets them in _outputForcesInColor
    private void UpdateComputeShaderForceMap()
    {
        UpdateSourcesPosition();

        _bufferGravSources.SetData(_gravitySources);
        computeShader.Dispatch(_computeShaderKernel, _pixelCount / 32, 1, 1); //The dividing value should be the same as on the numthreads

        _bufferForcesInColor.GetData(_outputForcesInColor);

        _texture.SetPixels(_outputForcesInColor);
        _texture.Apply();
    }

    private void UpdateSourcesPosition()
    {
        for (int i = 0; i < _spaceObjects.Length; i++)
        {
            _gravitySources[i].position = _spaceObjects[i].transform.position; //No need to update the mu, it hasn't changed
        }
    }

    private float GetMaximumGravity()
    {
        var maxForce = float.MinValue;

        foreach (var spaceObject in _spaceObjects)
        {
            var force = GetGravity(spaceObject, spaceObject.transform.position + new Vector3(1.0f, 1.0f, 1.0f)).magnitude;

            if (maxForce <= force)
            {
                maxForce = force;
            }
        }

        return maxForce;
    }

    // Apply Log to the gravity to ease visualization of the force
    private Vector3 GetGravity(SpaceObject spaceObject, Vector3 atPoint)
    {
        var direction = spaceObject.transform.position - atPoint;
        var gravityLog = Math.Log(1 + spaceObject.GetGravitationalPullForce(direction.magnitude));

        return direction.normalized * (float)gravityLog;
    }
}


Optimization

The main function in the compute shader has this attribute [numthreads(x,y,z)] which I have very little knowledge of. The official documentation from Microsoft [4] and some Unity Answers [5] can give some information on the attribute, which is very hardware dependent.

It is worth noting that there is a maximum thread group count of 65535 (with a group of 32 the maximum texture size is 1448x1448, with a group of 64 the maximum texture size would be 2047x2047) that can be used. My code has a group of 32 and I will be using the texture of 1448x1448 in the comparisons.

Similar to the CPU case we can also add a timer to update the texture at specified moments. This improves performance slightly.

Fragment Shader

Compute shader allows calculations to be done in the GPU, and then you can retrieve the data to be used however you want. As we are trying to paint the values in a texture it made perfect sense to use a traditional shader for this task. Doing this would remove the intermediate step of sending the data to the C# script. The script will send the data back to the GPU where the texture is painted. 

As with the compute shader we have 2 components:
  • ForceMapShader.shader: This is a file that uses shader language and will be ran in the GPU. It does the calculations and paints in the texture.
  • ForceMapFragShader.cs: This script loads the information into the GPU.
To understand the full logic of the shader I recommend having some knowledge on this topic. Nevertheless, the logic used is pretty basic: I use the frag function to calculate the forces and paint them.

The major takeaways are:
  • Properties section: these are global variables that will be modified initially.
  • Frag function: Once shader initialization is complete do all the calculations and paint the texture.
  • Only update the minimum information required.
The logic is the same as with the compute shader but having the end result directly painting on the pixel.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
Shader "Gravity/ForceMap"
{
    Properties //I will use these as constants
    {
        _MainTex("Texture", 2D) = "white" {}
        _NumSources("Sources", int) = 0
        _SizeX("SizeX", int) = 0
        _SizeY("SizeY", int) = 0
        _MaxForce("MaxForce", float) = 0
    }
    SubShader
    {
        Tags { "RenderType" = "Opaque" }
        Pass
        {
            CGPROGRAM
            #pragma target 3.0
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

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

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

            struct Source
            {
                float3 position;
                float mu;
            };

            Texture2D _MainTex;
            float4 _MainTex_ST;

            uint _NumSources;
            int _SizeX;
            int _SizeY;
            float _MaxForce;
            uniform StructuredBuffer<Source> gravSources; //This will get updated asyncrnously
            uniform StructuredBuffer<float3> pixelPositions;

            /*******/

            float4 GetColorFromGravity(float3 force)
            {
                float4 colorForce = float4(0, 0, 0, 0);

                if (force.z > 0.0f && force.x > 0.0f)
                {
                    colorForce.b = force.z;
                    colorForce.g = force.x;
                }
                else if (force.z > 0.0f && force.x <= 0.0f)
                {
                    colorForce.b = force.z;
                    colorForce.r = -force.x;
                }
                else if (force.z <= 0.0f && force.x > 0.0f)
                {
                    colorForce.r = -force.z;
                    colorForce.g = sqrt(pow(force.z, 2) + pow(force.x, 2));
                }
                else if (force.z <= 0.0f && force.x <= 0.0f)
                {
                    colorForce.r = sqrt(pow(force.z, 2) + pow(force.x, 2));
                    colorForce.g = -force.z;
                }

                return colorForce;
            }

            // Obtains the log of the gravity force vector
            float3 GetNormalizedForce(uint position)
            {
                float3 netForce = float3(0, 0, 0);

                for (uint k = 0; k < _NumSources; ++k)
                {
                    float3 forceDirection = (gravSources[k].position - pixelPositions[position]);
                    float gravityLog = log(1 + gravSources[k].mu / dot(forceDirection, forceDirection));
                    netForce += normalize(forceDirection) * gravityLog;
                }

                return netForce;
            }

            /*******/

            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                fixed4 col;
                if (_NumSources == 0)
                {
                    col = float4(0, 0, 0, 0);
                }
                else
                {
                    int x = i.uv.x * _SizeX;
                    int y = i.uv.y * _SizeY;

                    float3 clampedForce = GetNormalizedForce(y*_SizeX + x) / _MaxForce;
                    col = GetColorFromGravity(clampedForce);
                }
                return col;
            }
            ENDCG
        }
    }
}


ForceMapFragShader.cs has a very similar logic to the compute shader:
  1. Initialization:
    1. Setup texture size and properties.
    2. Create world points for the texture.
    3. Initialize shader variables.
  2. Update shader variables:
    1. Send updated gravity sources to GPU.
    2. Shader doing magic: calculate pixel colors with the updated sources. Note that this is happening in parallel to this list.

Remarks

It is interesting that in this implementation the shader pixels are not smoothed out, but we can have almost any size we desired (maximum size 11585x11585).
 

Comparisons

For each approach I will only show the average FPS. I have access to the CPU main thread, CPU render thread and FPS, but for the sake of simplicity I will not show these other values. The data was taken from the Stats option in Unity and on Maximize On Play (screen resolution 1898x1068).

Testing rig: Intel i7 7700k 4.20 GHz, 16 GB RAM, Windows 10, Nvidia RTX 3070
Testing date: June-July 2021

Tests:
  • Test 1: Earth-Moon system (1 static, 1 dynamic object)
  • Test 2: Solar system (1 sun, 8 planets)
  • Test 3: Chaos (N Bodies)

Test 1: Earth-Moon system (1 static, 1 dynamic object)

This is a simple case of only 2 bodies in the scene.

2 Bodies. FPS values per texturing mode

Earth-Moon system displaying a force map. Fragment shader 5000x5000

Test 2: Solar system (1 sun, 8 planets)

The solar system visualization with all major planets. Dwarf planets (Ceres and Pluto) are not considered here.

9 Bodies. FPS values per texturing mode

Solar system. Outer worlds visible, inner worlds is the blue blob. Fragment  shader 5000x5000

Test 3: Chaos (36 Bodies)

Modified Solar system to have a total of 36 bodies. 

36 Bodies. FPS values per texturing mode

Modified Solar system with 36 bodies. Fragment shader 3000x3000


The data shows that CPU is clearly a bottle neck. Among other methods Fragment is recommended, specially when it concerns increasing the resolution. It has great performance that barely decays with the resolution.

It is worth mentioning that there are factors that can affect the results:
  • To improve performance, we could add SystemDiagnostics and calculate the FPS in a stand-alone deployment. The unity editor generates some overhead.
  • Create a multi-threading implementation for the CPU approach.
Here I want to visually show the difference between each approach (CPU, Compute shader and Fragment shader).

The gravitation system has repetitive calculations in the FixedUpdate per object, which in return affect the general FPS. This is easily seen in the Chaos example.

Note: for the tests I had to disable the trail as it was creating a decent overhead in the results, though the GIFs shown here will display the trail.
 

Conclusion

For any type of heavy math operations independent of each other it is highly recommended to use the GPU. The speed at which it processes the data cannot be compared to anything else. It is true the CPU solution does not use multiple threads (which could improve the results considerably), but I doubt it can reach the Fragment Shader’s FPS.

This is just a quick post showing how to implement the math using 3 different approaches, and they could be improved. I am not interested in doing a foolproof analysis here, but an initial estimation.

As with the previous post a sample project can be found on this download link with all the relevant code.
Unity version: 2020.2.7f1

Hopefully this can bring my readers some insights.

References

[1] Unity Documentation Texture2D, Version 2020.3, Accessed 21 June 2021, <https://docs.unity3d.com/ScriptReference/Texture2D.html>

[2] Unity Documentation Compute shader, Version 2020.3, Accessed 21 June 2021, <https://docs.unity3d.com/Manual/class-ComputeShader.html>

[3] Ronja’s Tutorials Compute Shader, July 26 2020, Accessed 21 June 2021, <https://www.ronja-tutorials.com/post/050-compute-shader/>

[4] Microsoft Documentation, 2021, Accessed 21 June 2021, <https://docs.microsoft.com/en-us/windows/win32/direct3dhlsl/sm5-attributes-numthreads>

[5] Unity Answers, Difference Between Calling numthreads and Dispatch in a Unity Compute Shader, July 25th 2020, Accessed 21 June 2021, <https://stackoverflow.com/questions/63034523/difference-between-calling-numthreads-and-dispatch-in-a-unity-compute-shader>

[6] Nvidia Developer Zone, Cg 3.1 Toolkit Documentation, Accessed 24 June 2021, <https://developer.download.nvidia.com/cg/index_stdlib.html>

Sunday, August 13, 2017

Tutorial to use a Dimenco Monitor (3D Display) in a Unity 5 project

Unity tutorial on using a 3D stereoscopic display with shaders



This is a small tutorial on how to use the Dimenco monitor with Unity3D. At work we managed to get one of these monitors and we were interested in using it with Unity applications, and this is how I did it. As I have not found any information on how to use the 3D display in Unity I think the community will appreciate a little bit of light into the issue.

Introduction


Let's start with a brief introduction on Dimenco displays: Dimenco monitors are 3D monitors without glasses (3D displays). This implies that at certain positions and angles the image in the monitor will be seen in 3D. The pros is that you do not need glasses, the cons is that you need to be in specific positions and cannot move much (and usually far away from the monitor). 

How does a Dimenco monitor, or most of glass-free 3D technology work? The image is divided into 2 parts: Color information and depth information. The color side has a header that tells Dimenco this is a 3D image. Both sides, color and depth, will have in between dark lines. The monitor itself will do all the merging to display the image in 3D. This tutorial will explain step by step how to achieve this. is how the image is setup for the Dimenco 3D display 



Stereoscopic view of the image

Monitor setup:

This is the easy part. Have the monitor on and connected to the PC at 4K resolution (3840x2160). Simple :)

Unity setup:

The basic idea is to have a view of the scene divided into 2 parts: color image and depth image. The key here is that the output resolution of the scene has to be normal HD resolution (1920x1080), not in 4K. 


Part 1 - Unity 

Visualization of two images side by side:

Create a Canvas with 2 child objects. The canvas needs to be exactly HD resolution (1920x1080). The two child objects will have an image component each and need to be half the canvas size (960x1080). 
Basic Canvas setup

One of the images on the left side and the other on the right side. See the 2 different images in each side below. I have applied color to the images so it is easier to visualize them. 

Details on the position of each canvas child object
These images do not show anything from the scene yet. For that the camera view has to be projected into the canvas images.

Visualization of the scene in the canvas:

Create 2 render textures (for color image and depth image) in the Project (no Hierarchy). 
  • renderTexture_color
  • renderTexture_depth
See the configuration in the following images to set them up. 


Render Texture config
Note that the textures are not rendering anything yet. For that there needs to be a camera that renders to the texture, which is the next step. For the color render texture the image can be improved a bit by changing:
  • Anti-aliasing: 8 samples. (if image does not visualize properly reduce to none)
  • Filter mode: Trilinear.
The Color Format in Unity later versions (only verified in 2019.2.13f1) has changed. I am unavailable to verify in a Dimenco display as I do not have access to any now, but this should work:
  • For the renderTexture_color: R16G16B16A16_UNORM or R32G32B32A32_UINT
  • For the renderTexture_depth: DEPTH_AUTO

Camera setup (rendering to texture):

The ideal case for this is to have a single camera that renders two different textures, but Unity only allows 1 camera rendering to a single texture. To fix this problem a simple setup is created: a new empty GameObject with 3 cameras as children. Why 3? Because Unity needs a main camera, and the other two cameras are only rendering to textures.
  • Main Camera will be a normal camera. The configuration here is completely arbitrary as it will not affect anything in the scene, or what is displayed in the monitor. We will only see the canvas. Set culling mask to “nothing”.
  • Camera_color will render to target texture “renderTexture_color”.
  • Camera_depth will render to target texture “renderTexture_depth”.
The position and rotation of each camera (Camera_color and Camera_depth) should be exactly the same. In case the cameras need to be moved, just move the parent object. 


Camera Setup
This setup renders to the textures what is obtained from each camera, but this is not shown in the canvas yet. For that, the render texture will need to using a custom shader. be projected into a material and that material will be placed into the canvas image.

Part 2 - Shaders

To be honest this is the first time I have worked with shaders, so forgive me if I do not follow best practices. The first thing is to create a predefined unlit shader. 

Shader creation
This shader will be used as a baseline to modify the color and depth images. Check Unity docs for 
more info on shader examples in this link. This is the default code from the unlit shader:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
Shader "Unlit/NewUnlitShader"
{
 Properties
 {
  _MainTex ("Texture", 2D) = "white" {}
 }
 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
   {
    float2 uv : TEXCOORD0;
    UNITY_FOG_COORDS(1)
    float4 vertex : SV_POSITION;
   };

   sampler2D _MainTex;
   float4 _MainTex_ST;
   
   v2f vert (appdata v)
   {
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    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
  }
 }
}

Depth shader (2DPlusDepth_Depth): 

This is the easy one, as you only have to create in-between black lines for each texture. Additionally I will also add the functionality to allow for a parameter to change how strong the depth affects the 3D immersion.

It is good to realize that the fragment shader will affect every pixel using the UV coordinates. This means coordinates that go from 0 to 1. To be able to apply the in-between black lines we will transform the UV coordinates into height and width coordinates of 540x960 (As the image size required by Dimenco). It is good to remember that each pixel will run this function, so there is no need to create loops that iterate over the whole image. We only need to see if the current pixel belongs to an odd line or an even line.

First lets change the name of the shader in the first line. This will give the path and name of the shader. 

Change 
1
Shader "Unlit/NewUnlitShader"

for
1
Shader "Dimenco/2DPlusDepth_Depth"


The next thing is the function where the black lines are going to be written.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fixed4 frag (v2f i) : SV_Target
{

 int coord_y = i.uv.y * 540; //Gives us the current row.
 int coord_x = i.uv.x * 960; //Note: We do not use this, but it is more clear to see it.

 float mod_value = fmod(coord_y,2.0); //find whether this is an odd or even line.
 fixed4 col;

 if (mod_value == 1)
 {
  //Odd line: use the value from the texture  
  col = tex2D(_MainTex, i.uv);
 }
 else
 {
  //Even line: create a black pixel.
  col = float4(0, 0, 0, 0);
 }
 // apply fog
 UNITY_APPLY_FOG(i.fogCoord, col);
 return col;
    
}


This is the basic structure of the shader. To add a property that can modify the shader depth values in run time we do the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Properties
{
 _MainTex ("Texture", 2D) = "white" {}
 _DepthMod("Depth Modifier", float) = 1 //Added field
}

Pass
{
 […] //Other stuff

 sampler2D _MainTex;
 float4 _MainTex_ST;
 float _DepthMod; //Again, add here the field from Properties

 […] //Other Stuff   

 fixed4 frag (v2f i) : SV_Target
 {
    
  int coord_y = i.uv.y * 540; 
  int coord_x = i.uv.x * 960; 

  float mod_value = fmod(coord_y,2.0); 
  fixed4 col;

  if (mod_value == 1)
  {
   col = _DepthMod * tex2D(_MainTex, i.uv); //Use the new property here
  }
  else
  {
   col = float4(0, 0, 0, 0);
  }  

  UNITY_APPLY_FOG(i.fogCoord, col);
  return col;
    
 }
 ENDCG
}


The added/ modified lines have comments, while the previous comments have been removed. This way the material that uses this shader will have a variable “Depth Modifier” that will affect the depth image at runtime. From my own testing I have experienced that the value should be [1,3]. To manipulate this variable a simple script can be created. To see how to manipulate shader variables check the Unity documentation.

Color Shader (2DPlusDepth_Color):

The color shader has two distinct parts: in-between black lines and the header. For the black lines, the same procedure as before is followed. Just copy the same steps as before. First create a new shader (2DPlusDepth_Color) and change the name:

1
Shader "Unlit/NewUnlitShader"

for
1
Shader "Dimenco/2DPlusDepth_Color"

The header is a bit more tricky. For details on each component of the dimenco header, refer to the documentation in their website. I will just write down the standard header for the 3 possibilities that are needed:

Header 1: F10140800000C42DD3AF (Common to all 3 cases)
Header 2:
  • 2D-plus-Depth: F2140000000000000000000000000000000036958221
  • Declipse – removed redundant data: F2149A0000000000000000000000000000006BF6C689
  • Declipse – full background data: F214EF0000000000000000000000000000002FF0C45F
We will only implement the header 1 and header 2 (2D-plus-Depth), which have been highlighted. The header 2 for the other two cases are also here, but they are commented. There is a “problem” with the UV coordinates, as they go [0 1] from left to right and bottom to top. The header has to be on top, so we need to check that the “row == height-1”. This can be observed in the code.


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
fixed4 frag (v2f i) : SV_Target
{    
 int row = i.uv.y * 540; //For the black lines
 int column = i.uv.x * 960; //For the header

 float mod_value = fmod(row,2.0);
 fixed4 col;

 if (mod_value == 1)
 {
  // sample the texture in every odd row
  col = tex2D(_MainTex, i.uv);

  //Header:
  //Blue_channel(2*(7-row)+16*column, 0)^7 = H(column)^row
  float pos_row = 16 * row;
  float pos_col = 2 * (7 - column);

  int width = 960;
  int height = 540;

  if (row == height-1 && column <= 494)
  { 
   //Only first row

   //Header 1:
   //F1,01, 40,80, 00,00, C4,2D, D3,AF: H[0],..., H[9]
   //1111 0001, 0000 0001 (F1,01) - H[0], H[1]
   //0100 0000, 1000 0000 (40,80) - H[2], H[3]
   //0000 0000, 0000 0000 (00,00) - H[4], H[5]
   //1100 0100, 0010 1101 (C4,2D) - H[6], H[7]
   //1101 0011, 1010 1111 (D3,AF) - H[8], H[9]  

   float headerA_0[8] = { 1,1,1,1, 0,0,0,1 }; //H[0]
   float headerA_1[8] = { 0,0,0,0, 0,0,0,1 }; //H[1]
   float headerA_2[8] = { 0,1,0,0, 0,0,0,0 }; //H[2]
   float headerA_3[8] = { 1,0,0,0, 0,0,0,0 }; //H[3]
   float headerA_4[8] = { 0,0,0,0, 0,0,0,0 }; //H[4]
   float headerA_5[8] = { 0,0,0,0, 0,0,0,0 }; //H[5]
   float headerA_6[8] = { 1,1,0,0, 0,1,0,0 }; //H[6]
   float headerA_7[8] = { 0,0,1,0, 1,1,0,1 }; //H[7]
   float headerA_8[8] = { 1,1,0,1, 0,0,1,1 }; //H[8]
   float headerA_9[8] = { 1,0,1,0, 1,1,1,1 }; //H[9]

   //Header 2:
   //F2,14, 00,00, 00,00, 00,00, 00,00, 00,00, 00,00, 00,00, 00,00, 36,95, 82,21: H[10],..., H[31]
   //1111 0010, 0001 0100 (F2,14) - H[10], H[11]
   //0000 0000, 0000 0000 (00,00) - H[12], H[13]
   //0000 0000, 0000 0000 (00,00) - H[14], H[15]
   //0000 0000, 0000 0000 (00,00) - H[16], H[17]
   //0000 0000, 0000 0000 (00,00) - H[18], H[19]
   //0000 0000, 0000 0000 (00,00) - H[20], H[21]
   //0000 0000, 0000 0000 (00,00) - H[22], H[23]
   //0000 0000, 0000 0000 (00,00) - H[24], H[25]
   //0000 0000, 0000 0000 (00,00) - H[26], H[27]
   //0011 0101, 1001 0101 (36,95) - H[28], H[29]
   //1000 0010, 0010 0001 (82,21) - H[30], H[31]

   float headerB_10[8] = { 1,1,1,1, 0,0,1,0 }; //H[10]
   float headerB_11[8] =  { 0,0,0,1, 0,1,0,0 }; //H[11]
   float headerB_12[8] = { 0,0,0,0, 0,0,0,0 }; //H[12]
   float headerB_13_27[8] = { 0,0,0,0, 0,0,0,0 }; //H[13]-H[27]
   float headerB_28[8] = { 0,0,1,1, 0,1,0,1 }; //H[28]
   float headerB_29[8] = { 1,0,0,1, 0,1,0,1 }; //H[29]
   float headerB_30[8] = { 1,0,0,0, 0,0,1,0 }; //H[30]
   float headerB_31[8] = { 0,0,1,0, 0,0,0,1 }; //H[31]

   /******** Other 3D inputs ********/

   //Declipse – ‘Removed redundant data’ format. Change H[12], H[28] - H[31]  
   //float headerB_12[8] =   { 1,0,0,1, 1,0,1,0 }; //H[12]
   //float headerB_28[8] =   { 0,1,1,0, 1,0,1,1 }; //H[28]
   //float headerB_29[8] =   { 1,1,1,1, 0,1,1,0 }; //H[29]
   //float headerB_30[8] =   { 1,1,0,0, 0,1,1,0 }; //H[30]
   //float headerB_31[8] =   { 1,0,0,0, 1,0,0,1 }; //H[31]

   //Declipse – ‘Full background data’ format. Change H[12], H[28] - H[31] 
   //float headerB_12[8] =   { 1,1,1,0, 1,1,1,1 }; //H[12]
   //float headerB_28[8] =   { 0,0,1,0, 1,1,1,1 }; //H[28]
   //float headerB_29[8] =   { 1,1,1,1, 0,0,0,0 }; //H[29]
   //float headerB_30[8] =   { 1,1,0,0, 0,1,0,0 }; //H[30]
   //float headerB_31[8] =   { 0,1,0,1, 1,1,1,1 }; //H[31]


   //Only even columns of first row
   if (fmod(column, 2.0) == 0)
   {
    int header = floor(column / 16);
    int headerPixel = fmod(column / 2, 8.0);

    if (header == 0) //Header A
    {
     col.b = headerA_0[headerPixel];
    } 
    else if (header == 1)
    {
     col.b = headerA_1[headerPixel];
    }
    else if (header == 2)
    {
     col.b = headerA_2[headerPixel];
    }
    else if (header == 3)
    {
     col.b = headerA_3[headerPixel];
    }
    else if (header == 4)
    {
     col.b = headerA_4[headerPixel];
    }
    else if (header == 5)
    {
     col.b = headerA_5[headerPixel];
    }
    else if (header == 6)
    {
     col.b = headerA_6[headerPixel];
    }
    else if (header == 7)
    {
     col.b = headerA_7[headerPixel];
    }
    else if (header == 8)
    {
     col.b = headerA_8[headerPixel];
    }
    else if (header == 9)
    {
     col.b = headerA_9[headerPixel];
    }
    else if (header == 10) //Header B
    {
     col.b = headerB_10[headerPixel];
    }
    else if (header == 11)
    {
     col.b = headerB_11[headerPixel];
    }
    else if (header == 12)
    {
     col.b = headerB_12[headerPixel];
    }
    else if (header >= 13 && header <= 27)
    {
     col.b = headerB_13_27[headerPixel];
    }
    else if (header == 28)
    {
     col.b = headerB_28[headerPixel];
    }
    else if (header == 29)
    {
     col.b = headerB_29[headerPixel];
    }
    else if (header == 30)
    {
     col.b = headerB_30[headerPixel];
    }
    else if (header == 31)
    {
     col.b = headerB_31[headerPixel];
    }

   }
  }
 }
 else
 {
  // black lines for every even line
  col = float4(0, 0, 0, 0);
 }
 // apply fog

 UNITY_APPLY_FOG(i.fogCoord, col);
 return col;
 
}

In case you want to change the headers, I would recommend to use this website to compute the checksum of CRC-32. 

For example, in the case of modifying H[0]-H[6] the checksum has to be in H[7]-H[9]. If you use the page mentioned just in the previous paragraph select the following options

  • Web: http://www.sunshine2k.de/coding/javascript/crc/crc_js.html
  • Options: 
    • CRC-32, Custom:
      • Input reflected: Uncheck; Result reflected: Uncheck.
      • Polynomial: 0x04C11DB7L; 
      • Initial Value: 0x0;
      • Final Xor Value: 0x0;
CRC Input Data (bytes): 0xF1 0x01 0x40 0x80 0x00 0x00 (change to whichever you use in the new H[0]-H[6])

Once both shaders have been created we have to apply them into the materials to be added to the canvas.

Materials:

To visualize the images in the canvas, two new materials have to be created:
  • Dimenco_color: Apply the Shader “Dimenco/2DplusDepth_color”. Select the render texture “renderTexture_color” in the Detail (RGB).
  • Dimenco_depth: Apply the Shader “Dimenco/2DplusDepth_depth”. Select the render texture “renderTexture_depth” in the Detail (RGB).
Example of the material "Dimenco_Color"

Apply the materials to the component Image of Image_left and Image_Right GameObjects in the canvas. Change the color to white if you had it with other values. 

Example of material in image component object within the canvas

Both images can be observed in each side of the display. 




These materials are applied to the image left (color) and right (depth) in the canvas and everything should be alright. Once you build the project and run it in full screen mode the images should be in 3D. If you pay close attention you will spot bright blue pixels on the top left corner. These belong to the header and is normal (its the encoding that the Dimenco monitor uses to turn the image into 3D).

Remember the monitor has to be in 4K resolution and the game in HD resolution. 

Sometimes there is an error saying “Releasing render texture that is set as Camera.targetTexture!”. If that happens it means the render texture is no longer attached to the camera. Attach it and build the project again, everthing should be alright. From my experience that only happens when you modify the render texture and build the project. Once you reattach and build it (without modifying the render texture again) the error does not happen again. 

This is my first tutorial, so I hope everything is clear and easy to follow. In case there is a mistake, or any other input, please leave a comment.

Note: I have verified the shaders' with Unity 2019.2.13f1 and they work. I do not have a Dimenco display at hand now, but it should be working there as well.

Downloads

A simplified unity asset version of this tutorial can be found here (Unity 2019.2.13). Disclaimer: This version has not been tested using the Dimenco display. 

For reference: this is the image obtained

Gravity Part 3: Create a force map and display it (CPU, Compute Shader and Fragment Shader)

Introduction This is the third and last post in the series I am creating, based on the logic from Physics-based Solar System Lessons : ...