Why doesn't a material shader render on procedural meshes?

I have a simple class to generate a series of quads facing the camera to test volumetric rendering shaders (see (1)).

With a normal pixel shader, this works fine.

However, with a surface shader, it doesn’t seem to work at all; it only seems to find the pixels from existing geometry in the scene, not from the procedurally created meshes.

I’ve had a good read of the #pragma directives in Unity - Manual: Writing Surface Shaders, but I have no idea what I’m doing wrong here.

Anyone know what might be causing this?

It seems specifically to be related to the alpha somehow; removing the alpha pragma makes the slices render correctly, but obviously, without the alpha to make them transparent.

Here’s what the normal shader (see below (2)) renders:

And here’s what the surface shader (see below(3)) renders (notice that it render correctly in the scene view, just not the camera view):

Here are the two shaders, (2) and (3):

Shader "Shaders/Volume/SphereVolume" {
  Properties {
    _center ("Center", Vector) = (0, 0, 0, 0)
    _size ("Size", Float) = 0
  }
  SubShader {
		Tags {"Queue"="Transparent"}
		Blend SrcAlpha OneMinusSrcAlpha
		Pass {
      CGPROGRAM
      #pragma vertex vert
      #pragma fragment frag
      #pragma target 3.0

      #include "UnityCG.cginc"

      uniform float3 _center;
      uniform float _size;

      struct vertexInput {
        float4 world : POSITION;
        float4 vertex : POSITION;
        float4 uv : TEXCOORD0;
      };

      struct fragmentInput{
        float4 position : SV_POSITION;
        float4 world : TEXCOORD1;
      };

      fragmentInput vert(vertexInput i){
        fragmentInput o;
        o.position = mul(UNITY_MATRIX_MVP, i.vertex);
        o.world = mul(_Object2World, i.vertex);
        return o;
      }

      float4 frag(fragmentInput i) : COLOR {

        // Calculate the relative distance from the center point
        float x = (i.world[0] - _center[0]) * (i.world[0] - _center[0]);
        float y = (i.world[1] - _center[1]) * (i.world[1] - _center[1]);
        float z = (i.world[2] - _center[2]) * (i.world[2] - _center[1]);
        float delta = sqrt(x + y + z);
        float inner = 0f;
        if (delta < _size) {
          inner = 1f - delta / _size;
        }
        return float4(1f, 0f, 0f, inner);
      }
      ENDCG
    }
  }
}

and:

Shader "Shaders/Volume/SphereVolumeMaterial" {
  Properties {
    _center ("Center", Vector) = (0, 0, 0, 0)
    _size ("Size", Float) = 0
  }
  SubShader {
    Cull Off
    CGPROGRAM

    #pragma surface surf Lambert alpha keepalpha

    uniform float3 _center;
    uniform float _size;

    struct Input {
        float3 worldPos;
    };

    // Distance between two points
    float distance(float3 a, float3 b) {
      float x = (a[0] - b[0]) * (a[0] - b[0]);
      float y = (a[1] - b[1]) * (a[1] - b[1]);
      float z = (a[2] - b[2]) * (a[2] - b[2]);
      return sqrt(x + y + z);
    }

    // See if a value is bounded inside 'magic volume'
    // In this case; a sphere.
    bool bounded(float3 a) {
      return distance(a, _center) < _size;
    }

    // Calculate the relative distance from the center point
    void surf (Input IN, inout SurfaceOutput o) {
        if (bounded(IN.worldPos)) {
          float partial = (_size - distance(IN.worldPos, _center)) / _size;
          o.Albedo = fixed3(partial, partial, partial);
        }
    }

    ENDCG
  }
  Fallback "Diffuse"
}

and finally, here’s the code generating the mesh, which seems fine, but hey, maybe there’s something weird up with it… (1):

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

namespace Shaders.Volume {

  /// Generate a series of parallel planes relative to the camera
  [RequireComponent(typeof(MeshFilter))]
  [RequireComponent(typeof(MeshRenderer))]
  public class SimpleVolumeRenderer : MonoBehaviour {

    [Tooltip("The camera to make this volume viewable from")]
    public new UnityEngine.Camera camera;

    [Tooltip("The size of the frame to generate for this object")]
    public float size = 1f;

    [Tooltip("The number of slices to mesh")]
    public int slices = 10;
    private int slices_;

    [Tooltip("The slice interval")]
    public float sliceGap = 0.1f;

    /// Internal vertex buffer
    private Vector3[] points;

    public void Start() {
      Build();
    }

    public void Update() {
      if (slices_ != slices) {
        Build();
      }
      Rebuild();
    }

    /// Generate a quad facing the camera at the given offset
    private void BuildQuad(Vector3 origin, Vector3 up, Vector3 right, int offset) {
      points[offset * 4 + 0] = origin + right - up;
      points[offset * 4 + 1] = origin + right + up;
      points[offset * 4 + 2] = origin - right + up;
      points[offset * 4 + 3] = origin - right - up;
    }

    /// Generate new points for each quad
    public Vector3[] MeshPoints() {
      if ((points == null) || (points.Length != 4 * slices)) {
        points = new Vector3[4 * slices];
      }
      var offset = size / 2f;
      var origin = gameObject.transform.position;
      var normal = (-1f * camera.transform.forward).normalized;
      var up = camera.transform.up.normalized * offset;
      var right = Vector3.Cross(normal, up).normalized * offset;
      for (var i = 0; i < slices; ++i) {
        var o = i - slices / 2;
        var src = origin - sliceGap * o * normal;
        BuildQuad(src, up, right, i);
      }
      return points;
    }

    /// Rebuild vertex points only
    public void Rebuild() {
      MeshFilter meshFilter = gameObject.GetComponent<MeshFilter>();
      meshFilter.mesh.vertices = MeshPoints();
    }

    /// Build all details of the mesh
    public void Build() {
      if (camera == null) {
        return;
      }

      MeshFilter meshFilter = gameObject.GetComponent<MeshFilter>();

      // Generate a set of meshes
      var mesh = new Mesh();
      mesh.Clear();

      var verts = MeshPoints();
      mesh.vertices = verts;

      // Full quads for the shader to use
      var uvs = new Vector2[4 * slices];
      for (var i = 0; i < slices; ++i) {
        uvs[i * 4 + 0]  = new Vector2(0f, 0f);
        uvs[i * 4 + 1]  = new Vector2(1f, 0f);
        uvs[i * 4 + 2]  = new Vector2(1f, 1f);
        uvs[i * 4 + 3]  = new Vector2(0f, 1f);
      }
      mesh.uv = uvs;

      // Always aim at camera
      var normal = (-1f * camera.transform.forward).normalized;
      var normals = new Vector3[4 * slices];
      for (var i = 0; i < slices; ++i) {
        normals[i * 4 + 0] = normal;
        normals[i * 4 + 1] = normal;
        normals[i * 4 + 2] = normal;
        normals[i * 4 + 3] = normal;
      }
      //mesh.normals = normals;

      // A series of quads
      var triangles = new int[6 * slices];
      for (var i = 0; i < slices; ++i) {
        triangles[i * 6 + 0] = i * 4 + 2;
        triangles[i * 6 + 1] = i * 4 + 1;
        triangles[i * 6 + 2] = i * 4 + 0;
        triangles[i * 6 + 3] = i * 4 + 0;
        triangles[i * 6 + 4] = i * 4 + 3;
        triangles[i * 6 + 5] = i * 4 + 2;
      }
      mesh.triangles = triangles;

      mesh.RecalculateBounds();
      mesh.RecalculateNormals();
      mesh.Optimize();
      meshFilter.mesh = mesh;
      slices_ = slices;
    }
  }
}

The solution to this is too add a tag to mark the queue:

Shader "Shaders/Volume/SphereVolumeMaterial" {
  Properties {
    _center ("Center", Vector) = (0, 0, 0, 0)
    _size ("Size", Float) = 0
  }
  SubShader {
    Tags { "Queue" = "Transparent" } <-- Without this tag, it doesn't work.
    CGPROGRAM