[SOLVED] Image Projection Shader

I know Unity has a built in image projector but I am writing my own to learn how it works.
My implementation mostly works but I am seeing some weirdness.

Essentially I have created a script attached to my projector object that creates a view and projection matrix that I pass to all the projector receiver objects, (IE. Plane_1 and Plane_2) and then have a shader that calculates where the new uvs.

So my hierarchy looks like

If I translate or rotate my Projector object, the image is projected correctly onto the planes.
If I only translate my World object, again, the the image is still projected correctly.
However if I rotate my World object, the projected image is skewed.
If a picture is worth 1000 words, a video will be worth 1,000,000 so take a look here.

My code looks like so

using UnityEngine;
using System.Collections;

public class ImageProjector : MonoBehaviour
{
    public Texture ProjectionTexture = null;
    public GameObject[] ProjectionReceivers = null;
    public float Angle = 0.0f;

    Vector4 Vec3ToVec4(Vector3 vec3, float w)
    {
        return new Vector4(vec3.x, vec3.y, vec3.z, w);
    }

    // Use this for initialization
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        Matrix4x4 matProj = Matrix4x4.Perspective(this.camera.fieldOfView, 1, this.camera.nearClipPlane, this.camera.farClipPlane);

        Matrix4x4 matView = Matrix4x4.identity;
        matView = Matrix4x4.TRS(Vector3.zero, this.camera.transform.rotation, Vector3.one);

        float x = Vector3.Dot(this.camera.transform.right,      -this.camera.transform.position);
        float y = Vector3.Dot(this.camera.transform.up,         -this.camera.transform.position);
        float z = Vector3.Dot(this.camera.transform.forward,    -this.camera.transform.position);

        matView.SetColumn(3, new Vector4(x, y, z, 1));
        matView = matView.transpose;

        Matrix4x4 LightViewProjMatrix = matView * matProj;

        if (ProjectionReceivers == null || ProjectionReceivers.Length <= 0)
        {
            return;
        }

        foreach (GameObject imageReceiver in ProjectionReceivers)
        {
            ProjectionTexture.wrapMode = TextureWrapMode.Clamp;
            imageReceiver.renderer.sharedMaterial.SetTexture("_ShadowMap", ProjectionTexture);
            imageReceiver.renderer.sharedMaterial.SetMatrix("_LightViewProj", LightViewProjMatrix);
            imageReceiver.renderer.sharedMaterial.SetFloat("_Angle", Angle);
        }
    }

    void OnDrawGizmos()
    {
        Gizmos.color = Color.red;

        Gizmos.DrawLine(this.camera.transform.position, this.camera.transform.position + (this.camera.transform.forward * 100.0f));
    }
}

My shader is

Shader"Custom/DiffuseWithShadow"
{
    Properties
    {
        _MainTex ("Main Texture", 2D) = "white" {}
        _ShadowMap ("Shadow Map", 2D) = "white" {}
        _Angle ("Angle", Float) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        Pass
        {
            CGPROGRAM
            // Upgrade NOTE: excluded shader from OpenGL ES 2.0 because it does not contain a surface program or both vertex and fragment programs.
            #pragma exclude_renderers gles

            #pragma vertex v
            #pragma fragment p

            uniform sampler2D _MainTex;
            uniform sampler2D _ShadowMap;
            uniform float4x4 _LightViewProj;
            uniform float _Angle;


            struct VertexOut
            {
                float4 position : POSITION;
                float2 uv : TEXCOORD0;
                float4 proj : TEXCOORD1;
            } ;

            VertexOut v( float4 position : POSITION, float2 uv : TEXCOORD0 )
            {
                VertexOut OUT;

                OUT.position =  mul( UNITY_MATRIX_MVP, position );
                OUT.uv = uv;
                OUT.proj = mul( mul( _Object2World, float4(position.xyz, 1)), _LightViewProj );

                return OUT;
            }

            struct PixelOut
            {
                float4    color : COLOR;
            } ;

            PixelOut p(VertexOut IN)
            {
                PixelOut OUT;

                float2 ndc = float2(IN.proj.x/IN.proj.w, IN.proj.y/IN.proj.w);
                float2 uv = (1 + float2( ndc.x, ndc.y)) * 0.5;

                float theta = _Angle*3.14159/ 180;
                float2x2 matRot = float2x2( cos(theta), sin(theta),
                                            -sin(theta), cos(theta) );
                uv = mul( uv, matRot);

                float4 c = tex2D( _ShadowMap, uv );

                if( uv.x < 0 || uv.y < 0 ||
                    uv.x > 1  || uv.y > 1 || c.a <= 0.00f )
                    {
                        c = tex2D(_MainTex, IN.uv);
                    }

                OUT.color = c;

                return OUT;
            }

            ENDCG
        }

    } 
    FallBack"Diffuse"
}

Any clues as to what I might be doing wrong? Thanks!

Ok, so I noticed a couple of issues with my math but the issue still remains…
The following is my new update code.

// Update is called once per frame
    void Update()
    {
        Matrix4x4 matProj = Matrix4x4.Perspective(this.camera.fieldOfView, 1, this.camera.nearClipPlane, this.camera.farClipPlane);

        Matrix4x4 matView = Matrix4x4.identity;
        matView = Matrix4x4.TRS(Vector3.zero, this.camera.transform.rotation, Vector3.one);

// Fixed - I should transpose before setting the poition.
        matView = matView.transpose;

        float x = Vector3.Dot(this.camera.transform.right,      -this.camera.transform.position);
        float y = Vector3.Dot(this.camera.transform.up,         -this.camera.transform.position);
        float z = Vector3.Dot(this.camera.transform.forward,    -this.camera.transform.position);

// Fixed - This should be row and not column
        matView.SetRow(3, new Vector4(x, y, z, 1));

        Matrix4x4 LightViewProjMatrix = matView * matProj;

        if (ProjectionReceivers == null || ProjectionReceivers.Length <= 0)
        {
            return;
        }

        foreach (GameObject imageReceiver in ProjectionReceivers)
        {
            ProjectionTexture.wrapMode = TextureWrapMode.Clamp;
            imageReceiver.renderer.sharedMaterial.SetTexture("_ShadowMap", ProjectionTexture);
            imageReceiver.renderer.sharedMaterial.SetMatrix("_LightViewProj", LightViewProjMatrix);
            imageReceiver.renderer.sharedMaterial.SetFloat("_Angle", Angle);
        }
    }

Haha ok I was being silly. I forgot that Unity’s matricies are column major so when I calculate the projector view matrix, I don’t need to transpose that matrix. The following is the corrected code for anyone interested!

using UnityEngine;
using System.Collections;

public class ImageProjector : MonoBehaviour
{
    public Texture ProjectionTexture = null;
    public GameObject[] ProjectionReceivers = null;
    public float Angle = 0.0f;

    Vector4 Vec3ToVec4(Vector3 vec3, float w)
    {
        return new Vector4(vec3.x, vec3.y, vec3.z, w);
    }

    // Use this for initialization
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        Matrix4x4 matProj = Matrix4x4.Perspective(this.camera.fieldOfView, 1, this.camera.nearClipPlane, this.camera.farClipPlane);

        Matrix4x4 matView = Matrix4x4.identity;
        matView = Matrix4x4.TRS(Vector3.zero, this.camera.transform.rotation, Vector3.one);

        float x = Vector3.Dot(this.camera.transform.right,      -this.camera.transform.position);
        float y = Vector3.Dot(this.camera.transform.up,         -this.camera.transform.position);
        float z = Vector3.Dot(this.camera.transform.forward,    -this.camera.transform.position);

        matView.SetRow(3, new Vector4(x, y, z, 1));

        Matrix4x4 LightViewProjMatrix = matView * matProj;

        if (ProjectionReceivers == null || ProjectionReceivers.Length <= 0)
        {
            return;
        }

        foreach (GameObject imageReceiver in ProjectionReceivers)
        {
            ProjectionTexture.wrapMode = TextureWrapMode.Clamp;
            imageReceiver.renderer.sharedMaterial.SetTexture("_ShadowMap", ProjectionTexture);
            imageReceiver.renderer.sharedMaterial.SetMatrix("_LightViewProj", LightViewProjMatrix);
            imageReceiver.renderer.sharedMaterial.SetFloat("_Angle", Angle);
        }
    }

    void OnDrawGizmos()
    {
        Gizmos.color = Color.red;

        Gizmos.DrawLine(this.camera.transform.position, this.camera.transform.position + (this.camera.transform.forward * 100.0f));
    }
}

The final working demo!

2 Likes

if I add another image projector , i see only one is working , can you tell how to make the two image projectors work at the same time

1 Like

You made my day is so simple! Have you ever thinked to put it in asset store?