11

I am trying to implement fur in Unity with the Shells technique. The Fins technique is purposely left out because I want this to run on low end mobiles (mostly Android devices) and that requires OpenGL ES 3.0 and above while Shells technique only requires OpenGL ES 2.0.

There is an example on the Shell technique based on XNA and I made an attempt to port that into Unity but it failed to work. Here is the article with the XNA project.

The XNA shader:

float4x4 World;
float4x4 View;
float4x4 Projection;

float CurrentLayer; //value between 0 and 1
float MaxHairLength; //maximum hair length

texture FurTexture;
sampler FurSampler = sampler_state
{
    Texture = (FurTexture);
    MinFilter = Point;
    MagFilter = Point;
    MipFilter = Point;
    AddressU = Wrap;
    AddressV = Wrap;
};


struct VertexShaderInput
{
    float3 Position : POSITION0;
    float3 Normal : NORMAL0;
    float2 TexCoord : TEXCOORD0;
};

struct VertexShaderOutput
{
    float4 Position : POSITION0;
    float2 TexCoord : TEXCOORD0;
};

VertexShaderOutput FurVertexShader(VertexShaderInput input)
{
    VertexShaderOutput output;
    float3 pos;
    pos = input.Position + input.Normal * MaxHairLength * CurrentLayer;

    float4 worldPosition = mul(float4(pos,1), World);
    float4 viewPosition = mul(worldPosition, View);
    output.Position = mul(viewPosition, Projection);

    output.TexCoord = input.TexCoord;
    return output;
}

float4 FurPixelShader(VertexShaderOutput input) : COLOR0
{
    return tex2D(FurSampler, input.TexCoord);
}

technique Fur
{
    pass Pass1
    {
        AlphaBlendEnable = true;
        SrcBlend = SRCALPHA;
        DestBlend = INVSRCALPHA;
        CullMode = None;

        VertexShader = compile vs_2_0 FurVertexShader();
        PixelShader = compile ps_2_0 FurPixelShader();
    }
}

The XNA C# script that controls the shader:

/// <summary>
/// This is the main type for your game
/// </summary>
public class Game1 : Microsoft.Xna.Framework.Game
{
    GraphicsDeviceManager graphics;
    SpriteBatch spriteBatch;

    public Game1()
    {
        graphics = new GraphicsDeviceManager(this);
        Content.RootDirectory = "Content";
    }

    //simple camera for use in the game
    Camera camera;
    //texture containing fur data
    Texture2D furTexture;
    //effect for fur shaders
    Effect furEffect;
    //number of layers of fur
    int nrOfLayers = 60;
    //total length of the hair
    float maxHairLength = 2.0f;
    //density of hair
    float density = 0.2f;
    Texture2D furColorTexture;

    //movement vectors
    Vector3 gravity = new Vector3(0, -1.0f, 0);
    Vector3 forceDirection = Vector3.Zero;
    //final displacement for hair
    Vector3 displacement;


    /// <summary>
    /// Allows the game to perform any initialization it needs to before starting to run.
    /// This is where it can query for any required services and load any non-graphic
    /// related content.  Calling base.Initialize will enumerate through any components
    /// and initialize them as well.
    /// </summary>
    protected override void Initialize()
    {
        // TODO: Add your initialization logic here
        camera = new Camera(this);
        Components.Add(camera);
        base.Initialize();
    }

    /// <summary>
    /// LoadContent will be called once per game and is the place to load
    /// all of your content.
    /// </summary>
    protected override void LoadContent()
    {
        // Create a new SpriteBatch, which can be used to draw textures.
        spriteBatch = new SpriteBatch(GraphicsDevice);
        //generate the geometry
        GenerateGeometry();
        //load the effect
        furEffect = Content.Load<Effect>("FurEffect");
        //create the texture
        furTexture = new Texture2D(GraphicsDevice,
                                                    256, 256, 1,
                                                    TextureUsage.None,
                                                    SurfaceFormat.Color);
        //fill the texture
        FillFurTexture(furTexture, density);
        furColorTexture = Content.Load<Texture2D>("bigtiger");
    }

    /// <summary>
    /// UnloadContent will be called once per game and is the place to unload
    /// all content.
    /// </summary>
    protected override void UnloadContent()
    {
        // TODO: Unload any non ContentManager content here
    }

    /// <summary>
    /// Allows the game to run logic such as updating the world,
    /// checking for collisions, gathering input, and playing audio.
    /// </summary>
    /// <param name="gameTime">Provides a snapshot of timing values.</param>
    protected override void Update(GameTime gameTime)
    {
        // Allows the game to exit
        if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
            this.Exit();

        // TODO: Add your update logic here

        base.Update(gameTime);
    }

    /// <summary>
    /// This is called when the game should draw itself.
    /// </summary>
    /// <param name="gameTime">Provides a snapshot of timing values.</param>
    protected override void Draw(GameTime gameTime)
    {
        forceDirection.X = (float)Math.Sin(gameTime.TotalGameTime.TotalSeconds) * 0.5f;
        displacement = gravity + forceDirection;
        furEffect.Parameters["Displacement"].SetValue(displacement);

        graphics.GraphicsDevice.Clear(Color.CornflowerBlue);

        furEffect.Parameters["World"].SetValue(Matrix.CreateTranslation(0, -10, 0));
        furEffect.Parameters["View"].SetValue(camera.View);
        furEffect.Parameters["Projection"].SetValue(camera.Projection);
        furEffect.Parameters["MaxHairLength"].SetValue(maxHairLength);
        furEffect.Parameters["FurTexture"].SetValue(furTexture);
        furEffect.Parameters["Texture"].SetValue(furColorTexture);

        furEffect.Begin();
        for (int i = 0; i < nrOfLayers; i++)
        {
            furEffect.Parameters["CurrentLayer"].SetValue((float)i / nrOfLayers);
            furEffect.CommitChanges();
            furEffect.CurrentTechnique.Passes[0].Begin();
            DrawGeometry();
            furEffect.CurrentTechnique.Passes[0].End();
        }
        furEffect.End();

        base.Draw(gameTime);
    }

    /// <summary>
    /// This functions prepares a texture to be used for fur rendering
    /// </summary>
    /// <param name="furTexture">This will contain the final texture</param>
    /// <param name="density">Hair density in [0..1] range </param>
    private void FillFurTexture(Texture2D furTexture, float density)
    {
        //read the width and height of the texture
        int width = furTexture.Width;
        int height = furTexture.Height;
        int totalPixels = width * height;

        //an array to hold our pixels
        Color[] colors;
        colors = new Color[totalPixels];

        //random number generator
        Random rand = new Random();

        //initialize all pixels to transparent black
        for (int i = 0; i < totalPixels; i++)
            colors[i] = Color.TransparentBlack;

        //compute the number of opaque pixels = nr of hair strands
        int nrStrands = (int)(density * totalPixels);

        //compute the number of strands that stop at each layer
        int strandsPerLayer = nrStrands / nrOfLayers;

        //fill texture with opaque pixels
        for (int i = 0; i < nrStrands; i++)
        {
            int x, y;
            //random position on the texture
            x = rand.Next(height);
            y = rand.Next(width);

            //compute max layer
            int max_layer = i / strandsPerLayer;
            //normalize into [0..1] range
            float max_layer_n = (float)max_layer / (float)nrOfLayers;

            //put color (which has an alpha value of 255, i.e. opaque)
            //max_layer_n needs to be multiplied by 255 to achieve a color in [0..255] range
            colors[x * width + y] = new Color((byte)(max_layer_n * 255), 0, 0, 255);
        }

        //set the pixels on the texture.
        furTexture.SetData<Color>(colors);
    }


    VertexPositionNormalTexture[] vertices;

    private void GenerateGeometry()
    {
        vertices = new VertexPositionNormalTexture[6];
        vertices[0] = new VertexPositionNormalTexture(
                                                                    new Vector3(-10, 0, 0),
                                                                    -Vector3.UnitZ,
                                                                    new Vector2(0, 0));
        vertices[1] = new VertexPositionNormalTexture(
                                                                    new Vector3(10, 20, 0),
                                                                    -Vector3.UnitZ,
                                                                    new Vector2(1, 1));
        vertices[2] = new VertexPositionNormalTexture(
                                                                    new Vector3(-10, 20, 0),
                                                                    -Vector3.UnitZ,
                                                                    new Vector2(0, 1));

        vertices[3] = vertices[0];
        vertices[4] = new VertexPositionNormalTexture(
                                                                    new Vector3(10, 0, 0),
                                                                    -Vector3.UnitZ,
                                                                    new Vector2(1, 0));
        vertices[5] = vertices[1];
    }

    private void DrawGeometry()
    {
        using (VertexDeclaration vdecl = new VertexDeclaration(
                                                                    GraphicsDevice,
                                                                    VertexPositionNormalTexture.VertexElements))
        {
            GraphicsDevice.VertexDeclaration = vdecl;
            GraphicsDevice.DrawUserPrimitives<VertexPositionNormalTexture>(PrimitiveType.TriangleList, vertices, 0, 2);
        }
    }

}



I carefully ported the both the shader and the control script line by line to Unity.

The Ported Unity shader:

Shader "Programmer/Fur Shader"
{
    Properties
    {
        _MainTex("Texture", 2D) = "white" {}
    //_TintColor("Tint Color", Color) = (1,1,1,1)
    }
        SubShader
    {
        Tags{ "Queue" = "Transparent" "RenderType" = "Transparent" }
        LOD 100
        Blend SrcAlpha One
        Blend DstAlpha OneMinusSrcAlpha
        ZWrite Off
        Cull Off

        Pass
    {
        CGPROGRAM
#pragma vertex vert
#pragma fragment frag
        // make fog work
        //#pragma multi_compile_fog

#include "UnityCG.cginc"

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

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

    struct VertexShaderInput
    {
        float3 Position : POSITION0;
        float3 Normal : NORMAL0;
        float2 TexCoord : TEXCOORD0;
    };

    struct VertexShaderOutput
    {
        float4 Position : POSITION0;
        float2 TexCoord : TEXCOORD0;
    };

    sampler2D _MainTex;
    float4 _MainTex_ST;

    //Test variable/delete after
    float4 _TintColor;

    //The variables
    float4x4 World;
    float4x4 View;
    float4x4 Projection;

    float CurrentLayer; //value between 0 and 1
    float MaxHairLength; //maximum hair length

    VertexShaderOutput vert(VertexShaderInput input)
    {
        VertexShaderOutput output;
        float3 pos;
        pos = input.Position + input.Normal * MaxHairLength * CurrentLayer;

        float4 worldPosition = mul(float4(pos, 1), World);
        float4 viewPosition = mul(worldPosition, View);
        output.Position = mul(viewPosition, Projection);

        output.TexCoord = input.TexCoord;
        return output;
    }

    float4 frag(VertexShaderOutput  i) : COLOR0
    {
        return tex2D(_MainTex,  i.TexCoord);
    }
        ENDCG
    }
    }
}

The ported Unity C# script that controls the shader:

public class Game1 : MonoBehaviour
{
    public Material material;


    public Vector3 pos = new Vector3(0f, 0.98f, -9.54f);


    //simple camera for use in the game
    private new Camera camera;
    //texture containing fur data
    public Texture2D furTexture;
    //effect for fur shaders
    //Effect furEffect;
    //number of layers of fur
    public int nrOfLayers = 40;
    //total length of the hair
    public float maxHairLength = 2.0f;
    //density of hair
    public float density = 0.2f;

    //[Space(20)]
    //public Vector3 dirWorldVal = new Vector3(0, -10, 0);

    void Start()
    {
        Initialize();
        GenerateGeometry();
    }

    public void Update()
    {
        Draw();
    }


    void Initialize()
    {

        //Initialize the camera
        camera = Camera.main;

        //create the texture
        furTexture = new Texture2D(256, 256, TextureFormat.ARGB32, false);
        furTexture.wrapModeU = TextureWrapMode.Repeat;
        furTexture.wrapModeV = TextureWrapMode.Repeat;
        furTexture.filterMode = FilterMode.Point;

        //fill the texture
        FillFurTexture(furTexture, density);

        /*XNA's SurfaceFormat.Color is ARGB.
        //https://gamedev.stackexchange.com/a/6442/98839*/


        if (material.mainTexture != null)
        {
            material.mainTexture.wrapModeU = TextureWrapMode.Repeat;
            material.mainTexture.wrapModeV = TextureWrapMode.Repeat;
            material.mainTexture.filterMode = FilterMode.Point;
        }
    }

    bool firstDraw = true;

    protected void Draw()
    {
        camera.backgroundColor = CornflowerBlue();

        Matrix4x4 worldValue = Matrix4x4.Translate(pos);
        Matrix4x4 viewValue = camera.projectionMatrix;
        // viewValue = camera.worldToCameraMatrix;
        Matrix4x4 projectionValue = camera.projectionMatrix;

        material.SetMatrix("World", worldValue);
        material.SetMatrix("View", viewValue);
        material.SetMatrix("Projection", projectionValue); //Causes object to disappear

        material.SetFloat("MaxHairLength", maxHairLength);

        if (firstDraw)
            material.SetTexture("_MainTex", furTexture);

        //furEffect.Begin();
        for (int i = 0; i < nrOfLayers; i++)
        {
            material.SetFloat("CurrentLayer", (float)i / nrOfLayers);
            DrawGeometry();
        }

        if (firstDraw)
        {
            material.mainTexture.wrapModeU = TextureWrapMode.Repeat;
            material.mainTexture.wrapModeV = TextureWrapMode.Repeat;
            material.mainTexture.filterMode = FilterMode.Point;
        }

        if (firstDraw)
            firstDraw = false;
    }

    void DrawGeometry()
    {
        Quaternion rotation = Quaternion.Euler(0, 180, 0);
        Graphics.DrawMesh(verticesMesh, pos, rotation, material, 0, camera);
    }

    private VertexPositionNormalTexture[] verticesPText;
    public Mesh verticesMesh;

    private void GenerateGeometry()
    {
        verticesPText = new VertexPositionNormalTexture[6];
        verticesPText[0] = new VertexPositionNormalTexture(new Vector3(-10, 0, 0),
                                                      -UnitZ(),
                                                       new Vector2(0, 0));
        verticesPText[1] = new VertexPositionNormalTexture(new Vector3(10, 20, 0),
                                                       -UnitZ(),
                                                       new Vector2(1, 1));
        verticesPText[2] = new VertexPositionNormalTexture(new Vector3(-10, 20, 0),
                                                       -UnitZ(),
                                                       new Vector2(0, 1));

        verticesPText[3] = verticesPText[0];
        verticesPText[4] = new VertexPositionNormalTexture(new Vector3(10, 0, 0),
                                                       -UnitZ(),
                                                       new Vector2(1, 0));
        verticesPText[5] = verticesPText[1];

        verticesMesh = VertexPositionNormalTextureToUnityMesh(verticesPText);
    }

    Mesh VertexPositionNormalTextureToUnityMesh(VertexPositionNormalTexture[] vpnt)
    {
        Vector3[] vertices = new Vector3[vpnt.Length];
        Vector3[] normals = new Vector3[vpnt.Length];
        Vector2[] uvs = new Vector2[vpnt.Length];

        int[] triangles = new int[vpnt.Length];

        //Copy variables to create a mesh
        for (int i = 0; i < vpnt.Length; i++)
        {
            vertices[i] = vpnt[i].Position;
            normals[i] = vpnt[i].Normal;
            uvs[i] = vpnt[i].TextureCoordinate;

            triangles[i] = i;
        }

        Mesh mesh = new Mesh();
        mesh.vertices = vertices;
        mesh.normals = normals;
        mesh.uv = uvs;

        mesh.triangles = triangles;
        return mesh;
    }

    private void FillFurTexture(Texture2D furTexture, float density)
    {
        //read the width and height of the texture
        int width = furTexture.width;
        int height = furTexture.height;
        int totalPixels = width * height;

        //an array to hold our pixels
        Color32[] colors = new Color32[totalPixels];

        //random number generator
        System.Random rand = new System.Random();

        //initialize all pixels to transparent black
        for (int i = 0; i < totalPixels; i++)
            colors[i] = TransparentBlack();

        //compute the number of opaque pixels = nr of hair strands
        int nrStrands = (int)(density * totalPixels);

        //fill texture with opaque pixels
        for (int i = 0; i < nrStrands; i++)
        {
            int x, y;
            //random position on the texture
            x = rand.Next(height);
            y = rand.Next(width);
            //put color (which has an alpha value of 255, i.e. opaque)
            colors[x * width + y] = Gold();
        }

        //set the pixels on the texture.
        furTexture.SetPixels32(colors);
        // actually apply all SetPixels, don't recalculate mip levels
        furTexture.Apply();
    }

    Color32 TransparentBlack()
    {
        //http://www.monogame.net/documentation/?page=P_Microsoft_Xna_Framework_Color_TransparentBlack
        Color32 color = new Color32(0, 0, 0, 0);
        return color;
    }

    Color32 Gold()
    {
        //http://www.monogame.net/documentation/?page=P_Microsoft_Xna_Framework_Color_Gold
        Color32 color = new Color32(255, 215, 0, 255);
        return color;
    }

    Color32 CornflowerBlue()
    {
        //http://www.monogame.net/documentation/?page=P_Microsoft_Xna_Framework_Color_CornflowerBlue
        Color32 color = new Color32(100, 149, 237, 255);
        return color;
    }

    public static Vector3 UnitZ()
    {
        return new Vector3(0f, 0f, 1f);
    }
}

The ported VertexPositionNormalTexture struct for Unity

public struct VertexPositionNormalTexture
{
    public Vector3 Position;
    public Vector3 Normal;
    public Vector2 TextureCoordinate;
    //public static readonly VertexDeclaration VertexDeclaration;
    public VertexPositionNormalTexture(Vector3 position, Vector3 normal, Vector2 textureCoordinate)
    {
        this.Position = position;
        this.Normal = normal;
        this.TextureCoordinate = textureCoordinate;
    }

    public override int GetHashCode()
    {
        // TODO: FIc gethashcode
        return 0;
    }

    public override string ToString()
    {
        return string.Format("{{Position:{0} Normal:{1} TextureCoordinate:{2}}}", new object[] { this.Position, this.Normal, this.TextureCoordinate });
    }

    public static bool operator ==(VertexPositionNormalTexture left, VertexPositionNormalTexture right)
    {
        return (((left.Position == right.Position) && (left.Normal == right.Normal)) && (left.TextureCoordinate == right.TextureCoordinate));
    }

    public static bool operator !=(VertexPositionNormalTexture left, VertexPositionNormalTexture right)
    {
        return !(left == right);
    }

    public override bool Equals(object obj)
    {
        if (obj == null)
        {
            return false;
        }
        if (obj.GetType() != base.GetType())
        {
            return false;
        }
        return (this == ((VertexPositionNormalTexture)obj));
    }
}

The ported Unity work is not working properly. No shells and the output image is flat.

This is the expected result in XNA (Works fine):

enter image description here

But this is what I see in Unity (no shells):

enter image description here

The final image supposed to look like the image below but I can't go on with the porting work since the basic implementation is not working properly in Unity.

enter image description here

My script public variable settings:

enter image description here

Why is the the ported Unity result flat? Did I miss anything?

EDIT:

Leo mentioned about possible backface problem because Unity uses the left-handed coordinate system while XNA uses the right-handed coordinate system.

I flipped the UnitZ() value and also tried reversing the mesh vertices but there was nothing on the screen. This is not likely the issue.

Programmer
  • 121,791
  • 22
  • 236
  • 328
  • 1
    I don't know if its causing this issue, but I think unity3d and xna use different winding order, so you are looking at the backface here. This could mean all the other shells are getting z culled. – Leo Bartkus Oct 09 '18 at 14:40
  • @LeoBartkus I know that and suspected it could be the issue but I have no idea what the proper solution is. – Programmer Oct 09 '18 at 14:47
  • If that's the problem, just reverse the order of the vertices in the mesh. And/Or make the UnitZ offset move the shells in +Z instead of -Z. – Leo Bartkus Oct 09 '18 at 15:31
  • Just revered the UnitZ and there was nothing on the screen. Also, reversed the vertices order and got the-same result. I guess that's not problem. – Programmer Oct 11 '18 at 14:02
  • 1
    Use `MaterialPropertyBlock` to set shader uniforms. From [Graphics.DrawMesh](https://docs.unity3d.com/ScriptReference/Graphics.DrawMesh.html) doc: "_Because DrawMesh does not draw mesh immediately, modifying material properties between calls to this function won't make the meshes pick them up. If you want to draw series of meshes with the same material, but slightly different properties, use MaterialPropertyBlock parameter_" – Pluto Oct 12 '18 at 06:44
  • @Pluto With MaterialPropertyBlock, it improved but still looks nothing like a hair strand. Currently, it looks like [this](https://i.imgur.com/ycy16um.png) but [this](https://i.stack.imgur.com/8VDBk.jpg) is what it should look like. – Programmer Oct 12 '18 at 08:52
  • @Programmer Try commenting out `Blend..` (both), `ZWrite off` and `Cull off` and in the fragment shader add `float4 col = tex2D(_MainTex, i.TexCoord); clip (col.a - 0.001); return col;`. This uses alpha clipping to discard fragments with alpha 0 and uses the depth buffer to draw things correctly. I think the problem with `Graphics.DrawMesh` here is that it doesn't draw the meshes in order. If you dont plan on using alpha blending this should be good enough, but if you do than I think you have to use `Graphics.DrawMeshNow` or even better `CommandBuffer.DrawMesh`. – Pluto Oct 12 '18 at 09:58
  • @Programmer With Graphics.DrawMeshNow the code should be `foreach ( Layer ) { furMaterial.SetPass(0); furMaterial.SetFloat(..); Graphics.DrawMeshNow(..); }`. And another thing for default alpha blending just use `Blend SrcAlpha OneMinusSrcAlpha` I believe that is the blend state in the xna fx file. – Pluto Oct 12 '18 at 10:14
  • Will change the code and let you know – Programmer Oct 12 '18 at 10:17
  • Problem with `Graphics.DrawMeshNow` is that it has to be used in the `OnPostRender` function. This requires re-writing the whole code. I will stick with `Graphics.DrawMesh` but if your first suggestion doesn't work, I will re-write it to work with `Graphics.DrawMeshNow` in the `OnPostRender` function – Programmer Oct 12 '18 at 10:27
  • As I said you can also use command buffers. Check out Unity's [sample project](https://docs.unity3d.com/Manual/GraphicsCommandBuffers.html) for more details. – Pluto Oct 12 '18 at 10:42
  • @Pluto I can't use that. Notice that I mentored OpenGL ES 2.0 support in my question. I am targeting low-end devices. `command buffers` feature requires OpenGL ES 3.0 and more latest OpenGL version and the original article for this is also targeting low end devices. I am trying my best to avoid using any API that would break that. – Programmer Oct 12 '18 at 10:45
  • I think command buffers work on `OpenGL ES 2.0`. Try a simple example to test it out. – Pluto Oct 12 '18 at 10:50
  • @Pluto Thanks for your suggestions. MaterialPropertyBlock did indeed solve this. For som reason, I had to restart Unity to notice that. It's just one of those things where restarting fixes an issue. – Programmer Oct 13 '18 at 11:53

1 Answers1

13

Unity is doing a batch optimization on the material. You can see this in the frame debugger. Each DrawGeometry call is using the same value for CurrentLayer. You need to use a propertyblock for each call to DrawMesh. Setting a new material causes some flickering.

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

namespace foo {
public class FurBehavior : MonoBehaviour
{
    public Material material;


    public Vector3 pos = new Vector3(0f, 0.98f, -9.54f);


    //simple camera for use in the game
    private new Camera camera;
    //texture containing fur data
    public Texture2D furTexture;
    //effect for fur shaders
    //Effect furEffect;
    //number of layers of fur
    public int nrOfLayers = 40;
    //total length of the hair
    public float maxHairLength = 2.0f;
    //density of hair
    public float density = 0.2f;

    //[Space(20)]
    //public Vector3 dirWorldVal = new Vector3(0, -10, 0);

    void Start()
    {
        this.transform.position = new Vector3(0f, 0.98f, -9.54f);
        this.transform.rotation = Quaternion.Euler(0, 180, 0);
        Initialize();
        GenerateGeometry();
    }

    public void Update()
    {
        Draw();

    }


    void Initialize()
    {

        //Initialize the camera
        camera = Camera.main;

        //create the texture
        furTexture = new Texture2D(256, 256, TextureFormat.ARGB32, false);
        furTexture.wrapModeU = TextureWrapMode.Repeat;
        furTexture.wrapModeV = TextureWrapMode.Repeat;
        //furTexture.filterMode = FilterMode.Point;

        //fill the texture
        FillFurTexture(furTexture, density);

        /*XNA's SurfaceFormat.Color is ARGB.
        //https://gamedev.stackexchange.com/a/6442/98839*/


        if (material.mainTexture != null)
        {
            material.mainTexture.wrapModeU = TextureWrapMode.Repeat;
            material.mainTexture.wrapModeV = TextureWrapMode.Repeat;
           // material.mainTexture.filterMode = FilterMode.Point;
        }
    }

    bool firstDraw = true;

    protected void Draw()
    {
        var pos = this.transform.position;

        camera.backgroundColor = CornflowerBlue();

        Matrix4x4 worldValue = Matrix4x4.Translate(pos);
        Matrix4x4 viewValue = camera.projectionMatrix;
        // viewValue = camera.worldToCameraMatrix;
        Matrix4x4 projectionValue = camera.projectionMatrix;

        material.SetMatrix("World", worldValue);
        material.SetMatrix("View", viewValue);
        material.SetMatrix("Projection", projectionValue); //Causes object to disappear

        material.SetFloat("MaxHairLength", maxHairLength);

        //if (firstDraw)
            material.SetTexture("_MainTex", furTexture);

        //furEffect.Begin();
        for (int i = 0; i < nrOfLayers; i++)
        {
            var propertyBlock = new MaterialPropertyBlock();

            var layer = (float)i / (float)nrOfLayers;
            propertyBlock.SetFloat("CurrentLayer", layer);
            propertyBlock.SetFloat("MaxHairLength", maxHairLength);
            propertyBlock.SetColor("_TintColor", new Color(layer, layer, layer, layer));
            DrawGeometry(propertyBlock);
        }

        if (firstDraw)
        {
            material.mainTexture.wrapModeU = TextureWrapMode.Repeat;
            material.mainTexture.wrapModeV = TextureWrapMode.Repeat;
            material.mainTexture.filterMode = FilterMode.Point;
        }

        if (firstDraw)
            firstDraw = false;
    }

    void DrawGeometry(MaterialPropertyBlock props)
    {
        var rot = Quaternion.Euler(0, 180, 0);
        Graphics.DrawMesh(verticesMesh, pos, rot, material, 0, camera, 0, props);
    }

    private VertexPositionNormalTexture[] verticesPText;
    public Mesh verticesMesh;

    private void GenerateGeometry()
    {
        var UnitZ = new Vector3(0, 0, 1);
        var verticesPText = new VertexPositionNormalTexture[6];
        verticesPText[5] = new VertexPositionNormalTexture(new Vector3(-10, 0, 0),
                                                    -UnitZ,
                                                    new Vector2(0, 0));
        verticesPText[4] = new VertexPositionNormalTexture(new Vector3(10, 20, 0),
                                                    -UnitZ,
                                                    new Vector2(1, 1));
        verticesPText[3] = new VertexPositionNormalTexture(new Vector3(-10, 20, 0),
                                                    -UnitZ,
                                                    new Vector2(0, 1));

        verticesPText[2] = verticesPText[5];
        verticesPText[1] = new VertexPositionNormalTexture(new Vector3(10, 0, 0),
                                                    -UnitZ,
                                                    new Vector2(1, 0));
        verticesPText[0] = verticesPText[4];

    }

    Mesh VertexPositionNormalTextureToUnityMesh(VertexPositionNormalTexture[] vpnt)
    {
        Vector3[] vertices = new Vector3[vpnt.Length];
        Vector3[] normals = new Vector3[vpnt.Length];
        Vector2[] uvs = new Vector2[vpnt.Length];

        int[] triangles = new int[vpnt.Length];

        //Copy variables to create a mesh
        for (int i = 0; i < vpnt.Length; i++)
        {
            vertices[i] = vpnt[i].Position;
            normals[i] = vpnt[i].Normal;
            uvs[i] = vpnt[i].TextureCoordinate;

            triangles[i] = i;
        }

        Mesh mesh = new Mesh();
        mesh.vertices = vertices;
        mesh.normals = normals;
        mesh.uv = uvs;

        mesh.MarkDynamic();


        mesh.triangles = triangles;
                    mesh.UploadMeshData(false);
        return mesh;
    }

    private void FillFurTexture(Texture2D furTexture, float density)
    {
        //read the width and height of the texture
        int width = furTexture.width;
        int height = furTexture.height;
        int totalPixels = width * height;

        //an array to hold our pixels
        Color32[] colors = new Color32[totalPixels];

        //random number generator
        System.Random rand = new System.Random();

        //initialize all pixels to transparent black
        for (int i = 0; i < totalPixels; i++)
            colors[i] = TransparentBlack();

        //compute the number of opaque pixels = nr of hair strands
        int nrStrands = (int)(density * totalPixels);

        //fill texture with opaque pixels
        for (int i = 0; i < nrStrands; i++)
        {
            int x, y;
            //random position on the texture
            x = rand.Next(height);
            y = rand.Next(width);
            //put color (which has an alpha value of 255, i.e. opaque)
           // colors[x * width + y] = new Color32((byte)255, (byte)x, (byte)y, (byte)255);
           colors[x * width + y] = Gold();
        }

        //set the pixels on the texture.
        furTexture.SetPixels32(colors);
        // actually apply all SetPixels, don't recalculate mip levels
        furTexture.Apply();
    }

    Color32 TransparentBlack()
    {
        //http://www.monogame.net/documentation/?page=P_Microsoft_Xna_Framework_Color_TransparentBlack
        Color32 color = new Color32(0, 0, 0, 0);
        return color;
    }

    Color32 Gold()
    {
        //http://www.monogame.net/documentation/?page=P_Microsoft_Xna_Framework_Color_Gold
        Color32 color = new Color32(255, 215, 0, 255);
        return color;
    }

    Color32 CornflowerBlue()
    {
        //http://www.monogame.net/documentation/?page=P_Microsoft_Xna_Framework_Color_CornflowerBlue
        Color32 color = new Color32(100, 149, 237, 255);
        return color;
    }

    public static Vector3 UnitZ()
    {
        return new Vector3(0f, 0f, 1f);
    }
}
}

I also modified the shader to visualize the shells.

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 
'UnityObjectToClipPos(*)'

Shader "Programmer/Fur Shader"
{
Properties
{
    _MainTex("Texture", 2D) = "white" {}
_TintColor("Tint Color", Color) = (1,1,1,1)
}
SubShader
{
    Tags{ "Queue" = "Transparent" "RenderType" = "Transparent" }
    LOD 100
    //Blend SrcAlpha One
    //Blend DstAlpha OneMinusSrcAlpha
    Blend SrcAlpha OneMinusSrcAlpha
    ZWrite Off
    Cull Off

    Pass
    {
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
                // make fog work
                //#pragma multi_compile_fog

        #include "UnityCG.cginc"

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

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

        struct VertexShaderInput
        {
            float3 Position : POSITION0;
            float3 Normal : NORMAL0;
            float2 TexCoord : TEXCOORD0;
        };

        struct VertexShaderOutput
        {
            float4 Position : POSITION0;
            float2 TexCoord : TEXCOORD0;
            float4 Tint: COLOR1;
        };

        sampler2D _MainTex;
        float4 _MainTex_ST;

        //Test variable/delete after
        float4 _TintColor;

        //The variables
        float4x4 World;
        float4x4 View;
        float4x4 Projection;

        float CurrentLayer; //value between 0 and 1
        float MaxHairLength; //maximum hair length

        VertexShaderOutput vert(VertexShaderInput input)
        {
            VertexShaderOutput output;
            float3 pos;
            pos = input.Position + input.Normal * MaxHairLength * CurrentLayer;

            //float4 worldPosition = mul(float4(pos, 1), World);
            //float4 viewPosition = mul(worldPosition, View);
            output.Position = UnityObjectToClipPos(pos);

            output.TexCoord = input.TexCoord;
            output.Tint = float4(CurrentLayer, CurrentLayer, 0, 1);
            return output;
        }

        float4 frag(VertexShaderOutput  i) : COLOR0
        {
            float4 t = tex2D(_MainTex,  i.TexCoord) * i.Tint;
            return t;//float4(t, i.x, i.y, 1);
        }
        ENDCG
    }
}

}

Here's what it looks like after messing with the parameters and moving the camera a bit.

Looking at it from around {0, 0, -10}

Leo Bartkus
  • 1,925
  • 13
  • 17
  • Yep, I got it working. I made a few other changes so that I could interact with it in the inspector and see the different layers. I'll update to include all of my changes. – Leo Bartkus Oct 12 '18 at 14:02
  • Btw I had to set the quad mesh and the material in the inspector for this to even render at all. – Leo Bartkus Oct 12 '18 at 14:29
  • The built in quad just re-uses vertices along the quad diagonal, its not the problem. And in your images, just back up the camera. This fur shells technique is from over 15 years ago, and it really does look that bad up close. – Leo Bartkus Oct 12 '18 at 15:32
  • This fixed the issue. Restarting Unity again seems to have fixed the quad shaped problem too. Your update to `UnityObjectToClipPos` fixed camera rotation and translation issue which I didn't mention. I wonder why without `UnityObjectToClipPos`, moving and rotating the camera always made the stands disappear or look weird. Thanks for your solution. – Programmer Oct 13 '18 at 11:57
  • Just resumed and finished porting the whole project. The final result looks funny and not what I expected. I suspect the shader and unfortunately not a shader guru. I will create new question with bounty if the issue still persist by tomorrow. – Programmer Oct 19 '18 at 15:53