0
\$\begingroup\$

I would like to know how they created the pixel perfect noise shader in monument valley 2 in that b/w level (example picture). I think it starts with a billboard shader, but I have no clue how to scale the texture to it's native size.

I'm using ShaderLab (unity, mobile platforms)

\$\endgroup\$
  • 1
    \$\begingroup\$ What do you mean by "pixel-perfect"? \$\endgroup\$ – Bálint Feb 25 '18 at 11:31
  • \$\begingroup\$ I mean the noise texture should not be scaled in any way, and should have it's native size on the screen. I've found out that my assumption about the billboard was probably wrong, _ScreenSpace is the better way of starting with this. \$\endgroup\$ – antpaw Feb 25 '18 at 14:25
1
\$\begingroup\$

The trick I like to use is reading the _TexelSize information in the shader to adapt to whatever size of noise or other overlay we're using.

Here's an example using an image effect to get an overlay blend mode:

Shader "Hidden/ScreenspaceOverlay"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _OverlayTex ("Overlay", 2D) = "white" {}
        _Effect ("Effect", Range(0, 1)) = 1.0
    }
    SubShader
    {
        // No culling or depth
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

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

            struct v2f
            {
                float2 screenUV : TEXCOORD0;
                float2 overlayUV : TEXCOORD1;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            sampler2D _OverlayTex;
            float4 _OverlayTex_TexelSize;
            float4 _OverlayTex_ST;
            float _Effect;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);

                // Map the scene render normally, 0...1
                o.screenUV = v.uv;

                // Scale the overlay UV according to the size of the texture,
                // so we get 1 overlay texel per screen texel.
                o.overlayUV = v.uv * _ScreenParams.xy * _OverlayTex_TexelSize.xy;

                // In case you want to use material.SetTextureOffset()
                // to scroll or randomize the noise position.
                o.overlayUV += _OverlayTex_ST.zw;

                return o;
            }



            fixed4 frag (v2f i) : SV_Target
            {
                // Sample the composed scene render & the overlay texture.
                half4 col = tex2D(_MainTex, i.screenUV);
                half4 overlay = tex2D(_OverlayTex, i.overlayUV);

                // Overlay blend mode.
                half4 blended = lerp(
                    2.0f*col*overlay, 
                    1.0f - 2.0f * (1.0f - col) * (1.0f - overlay),
                    step(0.5f, col)
                );

                // Return anything from 0% to 100% of the effect.
                return lerp(col, blended, _Effect);
            }
            ENDCG
        }
    }
}

The key bit is this line:

o.overlayUV = v.uv * _ScreenParams.xy * _OverlayTex_TexelSize.xy;

Once we have the position on screen in the 0...1 range from left to right, bottom to top (which we get conveniently as the input UV when blitting an image effect - for 3D geometry it's a bit trickier), we just need to

  • scale it up by _ScreenParams.xy, which contains the width & height of the current render target in pixels. This gives us the position in pixel coordinates.

  • scale it down by _OverlayTex_TexelSize.xy, which contains one divided by the texture width & height. This normalizes the texture coordinates within the texture's sampling space.

    (eg. if I'm 256 pixels from the left edge of my screen, and my texture is 128 pixels wide, this will give me 2.0, meaning the image tiles twice horizontally up to this point)

I then use a tiny little script on my camera to glue a material using this shader into the rendering pipeline after the whole scene has been drawn:

using UnityEngine;

[ExecuteInEditMode]
[RequireComponent(typeof(Camera))]
public class PostEffector : MonoBehaviour {

    public Material effect;

    void OnRenderImage(RenderTexture source, RenderTexture destination) {
        if (effect == null)
            Graphics.Blit(source, destination);
        else 
            Graphics.Blit(source, destination, effect);
    }
}
\$\endgroup\$
  • \$\begingroup\$ thank you for this great answer _OverlayTex_TexelSize is a huge help. In monuments valley's case, I think they didn't use a camera material effect to add noise because there is no noise on the character or the red bar. Maybe they are add noise with shader to each mesh? \$\endgroup\$ – antpaw Feb 25 '18 at 21:23
  • \$\begingroup\$ Yep, you can do that using the "for 3D geometry..." link above. \$\endgroup\$ – DMGregory Feb 25 '18 at 21:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.