I'm trying to create a water shader, and at this point in development I'm using a Photoshop-generated cloud texture as a map for vertex displacement.

The displacement itself works just fine, but there is no shading on the mesh.

In Game view:

Game View Screenshot

In Scene view:

Scene View Screenshot

Without the Point Light, the whole mesh is just a grey blob with detectable shapes only at the edges, where the color intersects with the grey-brown of the background:

Game view without Point Light

Remarks & Code

Because I need more vertices than the default Plane object has, I decided to generate the mesh inside a script. To do this, I followed Brackeys' tutorial on procedural terrain generation; the mesh generation part.

This includes generating the vertices, the triangles, and the UVs. Here is my code:

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

public class PlaneMeshGenerator : MonoBehaviour {
    public Vector2 squareCount;
    public int resolution = 4;

    [HideInInspector] public Mesh mesh;

    private Vector3[] vertices;
    private int[] triangles;
    private Vector2[] uv;

    private int xSize;
    private int zSize;

    private void Awake() {
        mesh = new Mesh();
        GetComponent<MeshFilter>().mesh = mesh;

    private void Start() {
        xSize = (int)squareCount.x;
        zSize = (int)squareCount.y;


    private void GenerateMeshData() {
        xSize = (int)squareCount.x;
        zSize = (int)squareCount.y;

        vertices = new Vector3[(xSize + 1) * (zSize + 1)];
        triangles = new int[(xSize * zSize * 6)];
        uv = new Vector2[(xSize + 1) * (zSize + 1)];

        #region Generate the Vertices
        for (int i = 0, z = -(zSize / 2); z < zSize / 2 + 1; z++) {
            for (int x = -(xSize / 2); x < xSize / 2 + 1; x++) {
                vertices[i++] = new Vector3(x / (float)resolution, 0f, z / (float)resolution);

        #region Generate the Triangles
        int vert = 0;
        int tris = 0;

        for (int z = 0; z < zSize; z++) {
            for (int x = 0; x < xSize; x++) {
                triangles[tris + 0] = vert;
                triangles[tris + 1] = vert + xSize + 1;
                triangles[tris + 2] = vert + 1;
                triangles[tris + 3] = vert + 1;
                triangles[tris + 4] = vert + xSize + 1;
                triangles[tris + 5] = vert + xSize + 2;

                vert += 1;
                tris += 6;
            vert += 1;

        #region Generate the UVs
        for (int i = 0, z = 0; z < zSize + 1; z++) {
            for (int x = 0; x < xSize + 1; x++) {
                uv[i++] = new Vector2((float)x / xSize, (float)z / zSize);

    private void UpdateMesh() {
        Mesh mesh = this.mesh;

        mesh.vertices = vertices;
        mesh.triangles = triangles;
        mesh.uv = uv;


(You can disregard the resolution variable. It just resizes the mesh so I can get more vertices for the same size.)

And here is the shader code:

Shader "Custom/Water"
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
        _CloudTex ("Clouds Texture", 2D) = "white" {}
        _Amount ("Displacement Amount", Range(0.1, 5)) = 1.0
        Tags { "RenderType"="Opaque" }
        LOD 200

        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf Standard fullforwardshadows vertex:vert

        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0
        #include "UnityCG.cginc"

        sampler2D _MainTex;
        sampler2D _CloudTex;

        struct Input
            float2 uv_MainTex;

        half _Glossiness;
        half _Metallic;
        fixed4 _Color;
        half _Amount;

        // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
        // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
        // #pragma instancing_options assumeuniformscaling
            // put more per-instance properties here

        void vert(inout appdata_full v, out Input o) {
            fixed height = tex2Dlod(_CloudTex, float4(v.texcoord.xy, 0.0, 0.0)).r;
            v.vertex.xyz += v.normal * height * _Amount;

        void surf (Input IN, inout SurfaceOutputStandard o)
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            // Metallic and smoothness come from slider variables
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
    FallBack "Diffuse"

My Guess

At first I thought the normals had been calculated incorrectly, but I don't think that's the case now. For one, applying an image texture works perfectly fine. And as you can see, I'm using the vertex normals in my shader code for displacement, and that works fine as well.

Cast and Receive Shadows is on, and the Directional Light's culling mask is set to "Everything".

The shader was created as a Standard Surface Shader, not Unlit, so that shouldn't be a problem.

A while ago, I followed the aforementioned tutorial to the fullest, and I didn't have this problem there. The only difference I can see here is that I took the displacement code from the C# script to the shader, meaning I used a different material. However, I don't know how to fix this without reverting to doing this in the script, which I can't do here.

Said old proc. gen. terrain:

Procedural Terrain Generation example GIF


I don't know what Shadowmasks and Shadow Cascades are, but I took pictures anyway, in case they're relevant":

Shadow Cascasdes screenshot

Shadowmask screenshot


The normals are not getting recalculated after the vertex displacement in the shader, which means that the shading is still based on the normals generated during the plane mesh generation.

Recalculating normals in a displacement shader is a whole other problem since you can't simple call something equivalent of mesh.RecalculateNormals();.

You can for instance use central differencing to calculate the normals after a texture displacement in a shader. You can see an example of this in the following link and its subsequential links.

  • \$\begingroup\$ I used v.vertex.xyz += v.normal * height * _Amount; to displace the vertices; do they really need recalculating anyway? After all, the displacement was done along the normal vectors' axes. \$\endgroup\$ – verified_tinker Jun 9 '20 at 12:00
  • \$\begingroup\$ Right, but when you displace a vector upward, the triangle it's connected to tilts sideways. If you don't change the normal, it's still pointing straight up, no longer perpendicular to the triangles around it. \$\endgroup\$ – DMGregory Jun 9 '20 at 12:08
  • \$\begingroup\$ @DMGregory I see. That makes sense! So, long story short, what I need to do is sample the surrounding pixels, right? I think that was done by using _[TexName]_ST? \$\endgroup\$ – verified_tinker Jun 9 '20 at 13:01

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.