Search Unity

Move Eyes with Shaders

Discussion in 'Shaders' started by ogr021, Feb 24, 2016.

  1. ogr021

    ogr021

    Joined:
    Oct 9, 2015
    Posts:
    13
    Hi everyone, I am very new on shaders in unity, actually I am working with some tutorials on shaders but it is very difficult for me to understand the code, but this is my problema and I hope I can put it here. I made a texture to créate a toon effect on a toon link model, and I want to create the effect of eye movement with a mask applied to the same shader. Actually I used the standard asset, where I put the eye with an alpha texture, and a secondary mask that represents the putil of the eye. I use the UV offset coordinates to move the eyes, like you see in the image:

    And I made the code for the toon shader as shown here:
    Code (CSharp):
    1. Properties {
    2.         _MainTex                 ("Base (RGB)", 2D)                             = "white" {}
    3.         _BrightColor            ("Bright Color", Color)                     = (1, 0, 0)
    4.         _Threshold1                ("Threshold Bright to Dark", range(0,1))     = 0.2
    5.         _DarkColor                ("Dark Color", Color)                         = (0, 1, 0)
    6.         _Threshold2                ("Threshold Middle to Dark", range(0,1))     = 0.9
    7.         _TransitionTexture        ("Transition Texture", 2D)                     = "white" {}
    8.         _TransitionTextureSize    ("Transition Texture Size", range(0.1, 50)) = 1
    9.        
    10.         _BrightTexture            ("Bright Color Texture", 2D)                 = "white"{}
    11.         _BrightTextureSize        ("Bright Texture Size", range(0.1,50))         = 1
    12.         _BrightTextureIntensity    ("Bright Texture Intensity", range(0.0,1))     = 0.5
    13.        
    14.         _DarkTexture            ("Dark Color Texture", 2D)                     = "white"{}
    15.         _DarkTextureSize        ("Dark Texture Size", range(0.1,50))         = 1
    16.        
    17.         _RimColor                ("Rim Color", Color)                         = (1, 0, 0)
    18.         _RimPower                ("Rim Position", range(0,3))                 = 1
    19.         _RimStrength            ("Rim Strength", range(0,1))                 = 1
    20.        
    21.     }
    22.    
    23.     SubShader {
    24.         Tags { "RenderType"="Opaque" }
    25.         LOD 200
    26.        
    27.         CGPROGRAM
    28.         // Use the customize Lighting Color named "Cartoon"
    29.         #pragma surface surf Cartoon
    30.  
    31.         sampler2D     _MainTex;
    32.         sampler2D     _TransitionTexture;
    33.         half4         _BrightColor;
    34.         half4         _DarkColor;
    35.         half         _Threshold1;
    36.         half         _Threshold2;
    37.         half         _TransitionTextureSize;
    38.        
    39.         half         _DarkTextureSize;
    40.         half         _BrightTextureSize;
    41.         half         _BrightTextureIntensity;
    42.         sampler2D     _BrightTexture;
    43.         sampler2D     _DarkTexture;
    44.        
    45.         half4         _RimColor;
    46.         half         _RimPower;
    47.         half         _RimStrength;
    48.        
    49.         struct Input {
    50.             float2 uv_MainTex;
    51.             float3 viewDir;
    52.         };
    53.        
    54.         struct SurfaceOutputCustom{
    55.             fixed3 Albedo;
    56.             fixed3 Normal;
    57.             fixed3 Emission;
    58.             half Specular;
    59.             fixed Gloss;
    60.             fixed Alpha;
    61.             fixed viewFallof;
    62.             half2 UV;
    63.         };
    64.  
    65.         // Return the light color Vector4 component (Red, Green, Blue, Alpha)
    66.         half4 LightingCartoon(SurfaceOutputCustom s, half3 dir, half attend){
    67.             // Light direction vector can take only values between 0-1
    68.             dir = normalize(dir);
    69.             // Calculate the product between the normal surface and the light direction vectors
    70.             half NdotL = saturate( dot (s.Normal, dir));
    71.             half4 darkColor = _DarkColor * tex2D(_DarkTexture, s.UV * _DarkTextureSize);
    72.             half4 brightColor = _BrightColor * ( tex2D(_BrightTexture, s.UV * _BrightTextureSize) * _BrightTextureIntensity +
    73.             (1-_BrightTextureIntensity));
    74.             // Calculate shadows according to the normal surface and the light direction vectors
    75.             half3 ShadowColor = NdotL < _Threshold1 ? darkColor : NdotL < _Threshold2 ?
    76.             lerp(_DarkColor, _BrightColor, tex2D(_TransitionTexture, s.UV * _TransitionTextureSize)) : brightColor;
    77.             // Return a color based on the product and the light influence color on the surface
    78.             half4 c;
    79.             c.rgb = ((NdotL * 0.4f)+0.6f) * s.Albedo * _LightColor0 * ShadowColor;
    80.             c.a = s.Alpha;
    81.             return c;
    82.         }
    83.  
    84.         void surf (Input IN, inout SurfaceOutputCustom o) {
    85.             half4 c = tex2D (_MainTex, IN.uv_MainTex);
    86.             o.UV = IN.uv_MainTex;
    87.            
    88.             half NdotView = 1 - dot(normalize(IN.viewDir), o.Normal);
    89.            
    90.             NdotView = pow(NdotView, _RimPower);
    91.            
    92.             o.Albedo = c * ( 1 - NdotView * _RimStrength);
    93.             o.Alpha = c.a;
    94.         }
    95.         ENDCG
    96.     }
    97.     FallBack "Diffuse"
    98. }
    My questions are:
    1.- How can I get access on a script to the UV offset coordinates of the SECONDARY MAPS on the material that I applied on the eyes?
    2.- Is there a way to calculate the angles that the eyes can see on a world space? and transform it to offset values on a range of -0.1 to 0.1, I was thinking of a Mathf.Clamp but the angles are the problem, maybe with a linear relation :/.
    3.- I post the code that I used to create the custom toon shader for the materials but for the eyes I used the standard shader, but I want to add only the part of the Secondary Maps to my shader, what line of codes I must add to make it work on my shader?, because I tried to copy the code from the standard shader but it didn't work.

    Thanks for your answers :)
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,348
    This is an excellent tutorial for getting to quickly understand how the different elements of shaders actually work.
    http://www.alanzucconi.com/2015/06/10/a-gentle-introduction-to-shaders-in-unity3d/
    I highly recommend reading all 5 parts.

    Now on to moving the eyes, you absolutely can access the UVs from script. Just use http://docs.unity3d.com/ScriptReference/Material.SetTextureOffset.html

    How you would use this would be to get the rendered component for your object (SkinnedMeshRenderer in your case) and do something like:
    Vector2 eyeOffset = new Vector2(-0.3f, 0.0f);
    rendererComponent.material[eyeIndex].SetTextureOffset("_DetailAlbedoMap", eyeOffset);

    That might not be the right texture to use, that's just my best guess, unfortunately you'll need to dig through the Unity shader source to find out which texture it's using if that doesn't work.

    As for getting the correct angle to look you'll need to fudge this a little since this is all about what feels right rather than anything that's physically correct. The easiest way I can think of to do this is use http://docs.unity3d.com/ScriptReference/Transform.InverseTransformPoint.html to get the head relative position of an object or point you want to look at, then do two dot products to get how far left or right and above or below that point is. Then take those values and scale / clamp the output until it looks good. Alternatively you could add a dummy transform to the head bone and use lookTransform.LookAt(target.transform, headTransform.up); and use that transform's euler angles to scale / clamp.
     
    ArachnidAnimal likes this.
  3. ogr021

    ogr021

    Joined:
    Oct 9, 2015
    Posts:
    13
    Hey thank you for that tutorial, I was looking for a great tutorial that shows about shaders because on internet there are plenty of them but they doesn't explain the basics and the unity tutorials are very simple. Actually I was digging into the standard shader of unity and I found this:

    Code (CSharp):
    1.     Properties
    2.     {
    3.         _Color("Color", Color) = (1,1,1,1)
    4.         _MainTex("Albedo", 2D) = "white" {}
    5.        
    6.         _Cutoff("Alpha Cutoff", Range(0.0, 1.0)) = 0.5
    7.  
    8.         _Glossiness("Smoothness", Range(0.0, 1.0)) = 0.5
    9.         [Gamma] _Metallic("Metallic", Range(0.0, 1.0)) = 0.0
    10.         _MetallicGlossMap("Metallic", 2D) = "white" {}
    11.  
    12.         _BumpScale("Scale", Float) = 1.0
    13.         _BumpMap("Normal Map", 2D) = "bump" {}
    14.  
    15.         _Parallax ("Height Scale", Range (0.005, 0.08)) = 0.02
    16.         _ParallaxMap ("Height Map", 2D) = "black" {}
    17.  
    18.         _OcclusionStrength("Strength", Range(0.0, 1.0)) = 1.0
    19.         _OcclusionMap("Occlusion", 2D) = "white" {}
    20.  
    21.         _EmissionColor("Color", Color) = (0,0,0)
    22.         _EmissionMap("Emission", 2D) = "white" {}
    23.        
    24.         _DetailMask("Detail Mask", 2D) = "white" {}
    25.  
    26.         _DetailAlbedoMap("Detail Albedo x2", 2D) = "grey" {}
    27.         _DetailNormalMapScale("Scale", Float) = 1.0
    28.         _DetailNormalMap("Normal Map", 2D) = "bump" {}
    29.  
    30.         [Enum(UV0,0,UV1,1)] _UVSec ("UV Set for secondary textures", Float) = 0
    31.  
    32.  
    33.         // Blending state
    34.         [HideInInspector] _Mode ("__mode", Float) = 0.0
    35.         [HideInInspector] _SrcBlend ("__src", Float) = 1.0
    36.         [HideInInspector] _DstBlend ("__dst", Float) = 0.0
    37.         [HideInInspector] _ZWrite ("__zw", Float) = 1.0
    38.     }
    And that on properties, the "_DetailAlbedoMap" is the option I am looking, and I was thinking of adding it to my shader, but I found that the property doesn't seem to make any difference, and digging into the code I cannot relation that property with any of the functions, but thanks for the tutorials and I will watch all of them.
     
  4. ogr021

    ogr021

    Joined:
    Oct 9, 2015
    Posts:
    13
    Sorry for re-open this threat but I still working on this kind of toon shader and I was wondering on how to render the eyebrows in front of the hair but not rendering in front of the face?
     
  5. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,348
    Well, if you're trying to match Wind Waker, the eyes and eyebrows draw over the hair.

    However a can think of a few ways to do this, and they all require custom shaders of some kind.

    1: Modify depth output / camera z push.
    Basic idea is to artificially push the surfaces towards the camera by some fixed amount to make them draw on over the hair. The problem is doing this properly requires some semi-tricky math. However it only needs to be done for the shader used for the eyebrows (and eyes) to work, everything else in the scene can use normal shaders.

    2: Stencil
    This requires using a special shader on the hair and on the eyebrows, and very specific rendering order (using the shader or material queue). Step one, render the body. Step two, render the eyes / eyebrows using a shader that also writes to the stencil with a specific value (like the value 1). Step 3, render the hair with a shader that doesn't render if the stencil has been written to. The problem with this technique is you can't use alpha blending to shape the eyes or eyebrows, you have to use alpha clip or mesh shapes.

    3: ZTest Always
    There's two ways to go about this. The most simple way is to render the eyebrows using a shader using ZTest Always and that's it, but then it'll render over everything that's been rendered before. The slightly more in-depth method is to render the body first, before anything else. Then render the eyes and eyebrows using ZTest Always and ZWrite Off. Then render everything else in the scene. You can do this fairly easily using material queues again, and only the eyebrows and eyes need the special shader.

    The last two methods have the problem of not working easily with multiple characters on screen. The last method can overcome that by rendering each character and their eyes in the order described above individually, so like body, eyes, body, eyes, body, eyes, etc. drawing them back to front (far away first).
     
    DHein likes this.
  6. ogr021

    ogr021

    Joined:
    Oct 9, 2015
    Posts:
    13
    So by the methods you mention, I think the first one is the best way to achieve this for multiple characters, like in wind waker. I am thinking that the math you mention could involve a dot product between the camera direction and the normal direction of the meses that represents the eyes and eyebrows, but do you think that this will work when the camera is located above the character?, because in wind waker the eyebrows can be seen even watching the character above its head
     
  7. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,348
    No dot product at all actually.
    Code (CSharp):
    1. // get camera relative position
    2. float4 viewPos = mul(UNITY_MATRIX_MV, v.vertex);
    3. // get camera to object direction vector, scaled so the z is one
    4. float3 viewOffset = viewPos.xyz / viewPos.z;
    5. // subtract offset * material parameter amount
    6. viewPos.xyz -= viewOffset * _OffsetTowardsCamera;
    7. // transform into clip space, and we're done
    8. o.pos = mul(UNITY_MATRIX_P, viewPos);
    The hard part with this method is getting the right distance to push. Too far and you'll start seeing eyebrows through walls, or other characters. Not far enough and it'll occasionally get clipped by the hair. The easiest way to get the right distance is probably to just play with the number manually by putting the camera in a position where the hair is furthest from the face and try to get the smallest distance you can.

    Now I said "tricky math" in my previous post, and the above obviously isn't that much shader code, but there's two reasons for that. One is using viewPos.xyz / viewPos.z vs normalize(viewPos.xyz) is non-obvious as the later will work most of the time, then sometimes look completely wrong when the camera gets too close. The other is it would be more efficient to modify the final o.pos value directly to potentially remove the additional mul(), but I can't remember the math for that off the top of my head. :p

    There's also one other "gotcha" with this technique. If you do close up camera views the eyes might disappear completely as they're actually closer to the camera so they might get near clipped. Writing to depth from the fragment shader can get around that somewhat, as can clamping the push to the near clip plane, but the depth modification requires the same math-I-can't-remember as the o.pos version and clamping the distance in the vertex shader leads to visual anomalies with UV coordinates. :p

    Lastly I might have the "viewPos.xyz -= viewOffset ..." wrong, it might need a +=
     
    Last edited: Feb 27, 2017
  8. colin299

    colin299

    Joined:
    Sep 2, 2013
    Posts:
    181
    If you want the eye to render correctly over the hair, and also can be blocked by anything infront the character.
    Try offset the z output in vertex shader by world unit, it works for me as my character's hair almost do not move, so the result is accurate even I put a mask on the character.

    The hard part is: you need some amount of math to convert (near plane,far plane, world unit) into a correct Z output offset as the Z is non linear accoss different distance to camera. Luckily the calculate only need to calculate once per eye vertex only, so you should not need to worry about performance problem.

    see if the "offset Z by world unit" trick works for you, this is my example result of the trick I mentioned above.(sorry the video didn't record the case where the eye is blocked by something, but it works)
     
  9. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,348
    So you're converting the clip space position z to linear, doing the offset, and converting back to clip space, yes? Something like:
    o.pos = UnityObjectToClipPos(v.vertex);
    float linearZ = LinearEyeDepth(o.pos.z / o.pos.w);
    linearZ += _Offset;
    o.pos.z = LinearToEyeDepth(linearZ) * o.pos.w;


    Could you share your shader code for "LinearToEyeDepth()", this is one thing I've never been able to get the math for right.
     
  10. colin299

    colin299

    Joined:
    Sep 2, 2013
    Posts:
    181
    This code is in our helper .cginc file for a long time.Not used for years, not sure if it still works, the math maybe totally wrong, but it works for me, you can try it. Wonder if there are better solution...please post it if you know it!
    Code (CSharp):
    1. //For modify any vertex shader's Zbuffer write value
    2.  
    3. //warning: only work for directx like platform(editor,metal), any opengles will fail - https://docs.unity3d.com/Manual/SL-PlatformDifferences.html
    4. //directx clip space depth range is near=0~far=1, where opengl clip space depth range is near=-1~far=1.
    5.  
    6. inline half C4CatCalculateZOffsetToVertexShaderOutputZ(half4 worldSpaceVertPos,half WorldUnitZOffsetNeededIn)
    7. {
    8.     half z = distance(worldSpaceVertPos, _WorldSpaceCameraPos);
    9.     half near = _ProjectionParams.y; //nearPlane
    10.     half far = _ProjectionParams.z; //farPlane
    11.  
    12.     //Use WorldUnit offset(linear) to calculate needed Depth Buffer offset(non-linear)
    13.     //float offsetNeeded = z*(1/z-1/(z-_offset))*(far*near/(far-near));
    14.     half depthBufferOffset = (1 - z / (z - WorldUnitZOffsetNeededIn)) * (far*near / (far - near)); //Simplfy
    15.     return depthBufferOffset;
    16. }
    //------------------------------------------------
    //using the function, where o.pos will be the SV_POSITION output of vertex shader
    Code (CSharp):
    1.         #if zOffset_ON
    2.                 half4 worldSpacePos = mul(unity_ObjectToWorld, i.vertex);
    3.                 o.pos.z += C4CatCalculateZOffsetToVertexShaderOutputZ(worldSpacePos, _Zoffset);
    4.         #endif
     
    Last edited: Mar 28, 2017
  11. ogr021

    ogr021

    Joined:
    Oct 9, 2015
    Posts:
    13
    Thanks for your answers. The method of pushing the eyes facing the camera looks great, but what is the difference with moving the eyes with world unit and the other method above. Both pushes the eyes in front of the hair. And what is the helper .cginc that contains that function?
     
  12. awesomedata

    awesomedata

    Joined:
    Oct 8, 2014
    Posts:
    1,419
    This topic is really great. Glad it has gotten some traction!


    @colin299
    I've been looking for a shader that does almost exactly what you did in that video. Would you mind sharing your shader code so I can study it? I can't find a decent-looking toon shader with nice lines like that anywhere to study.

    Also, not to derail the topic, but I have to ask, what method and program did you use to alter the normals on your character in that video? I can't seem to find a reliable way to do this step so that you can see how the character shading looks from any angle.


    @ogr021
    From what I gather after a single quick glance at the code, @colin299's solution seems to use roundabout the exact position in world space as the eye would be offset from the face toward the camera, no matter what angle the camera is looking at it (and with just enough depth toward the camera to get past the hair) and then calculates against the camera's near/far planes so that it doesn't get clipped and can still be covered up with other geometry.

    I would totally love to check out the full shader code if @colin299 would be awesome enough to share it with us though!
     
  13. ogr021

    ogr021

    Joined:
    Oct 9, 2015
    Posts:
    13
    Hi everyone.
    Yes, this post is really interesting to achieve the results actually it involves more math tan we expected.
    I was testing with the things you tell me and this was my result:


    In the image the eyebrow now looks in front of the hair. Also I am working on this shader to draw the lit and unlit light correctly on the direction of the lights. I was having problems with the ZOffset, for the user @colin299, on the code you passed I have to correct the additive sign to substract, when we want to apply the Z offset after converting it to clip space position, and make sure you apply after you convert the vertex position into clip space position.