📜 ⬆️ ⬇️

Creating a 3D printer shader effect

In this tutorial, we will recreate the effect of a 3D printer used in games such as Astroneer and Planetary Annihilation . This is an interesting effect, showing the process of creating an object. Despite the apparent simplicity, there are many far from trivial difficulties.



Introduction: first try


To recreate this effect, let's start with something simpler. For example, with a shader that colors an object differently depending on its position. To do this, you need to access the position of the pixels drawn in the world. This can be done by adding the worldPos field to the Unity 5 surface shader's Input structure.

 struct Input { float2 uv_MainTex; float3 worldPos; }; 

You can then use the Y position in the world as a surface function to change the color of the object. This can be achieved by changing the Albedo property in the SurfaceOutputStandard structure.
')
 float _ConstructY; fixed4 _ConstructColor; void surf (Input IN, inout SurfaceOutputStandard o) { if (IN.worldPos.y < _ConstructY) { fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color; o.Albedo = c.rgb; o.Alpha = ca; } else { o.Albedo = _ConstructColor.rgb; o.Alpha = _ConstructColor.a; } o.Metallic = _Metallic; o.Smoothness = _Glossiness; } 

The result is a first approximation of the Astroneer effect. The main problem is that the shaded display is still performed for the color part.

image

Unlit surface shader


In the previous tutorial of PBR and Lighting Models, we studied how to create custom lighting models for surface shaders. An unlit shader always creates the same color, regardless of ambient light and viewing angle. You can implement it as follows:

 #pragma surface surf Unlit fullforwardshadows inline half4 LightingUnlit (SurfaceOutput s, half3 lightDir, half atten) { return _ConstructColor; } 

His only task is to return a single solid color. As we can see, it refers to SurfaceOutput , which was used in Unity 4. If we want to create our own lighting model that works with PBR and global illumination, then we need to implement a function that receives SurfaceOutputStandard as input. In Unity 5, the following function is used for this:

 inline half4 LightingUnlit (SurfaceOutputStandard s, half3 lightDir, UnityGI gi) { return _ConstructColor; } 

The gi parameter here refers to the global illumination (global illumination), but in our unlit shader it does not perform any tasks. This approach works, but it has a big problem. Unity does not allow the surface shader to selectively change the lighting function. We cannot apply standard Lambert lighting to the lower part of the object and at the same time make the upper part unlit. You can assign a single lighting function for the entire object. We have to change the way the object is rendered depending on its position.

image

Pass the parameters of the lighting function


Unfortunately, the lighting function does not have access to the position of the object. The easiest way to provide this information is to use a boolean variable ( building ), which we define as a surface function. This variable can be checked by our new lighting function.

 int building; void surf (Input IN, inout SurfaceOutputStandard o) { if (IN.worldPos.y < _ConstructY) { fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color; o.Albedo = c.rgb; o.Alpha = ca; building = 0; } else { o.Albedo = _ConstructColor.rgb; o.Alpha = _ConstructColor.a; building = 1; } o.Metallic = _Metallic; o.Smoothness = _Glossiness; } 

Extend the standard lighting function


The last problem we have to face is quite complicated. As I explained in the previous section, we can use building to change the way lighting is calculated. The part of the object that is currently being constructed will be unlit, and the rest will be correctly calculated lighting. If we want our material to use PBR, we cannot rewrite all the code for photorealistic lighting. The only reasonable solution is to call the standard lighting function, which is already implemented in Unity.

In the traditional standard surface shader, the #pragma that defines the use of the PBR lighting function is as follows:

 #pragma surface surf Standard fullforwardshadows 

By the standards of the Unity name, it is easy to see that the function used should be called LightingStandard . This feature is in the UnityPBSLighting.cginc file, which you can connect if necessary.

We want to create our own lighting function called LightingCustom . Under normal circumstances, it simply calls the standard Unite PBR function called LightingStandard . However, if necessary, it uses the previously defined LightingUnlit .

 inline half4 LightingCustom(SurfaceOutputStandard s, half3 lightDir, UnityGI gi) { if (!building) return LightingStandard(s, lightDir, gi); // Unity5 PBR return _ConstructColor; // Unlit } 

To compile this code, Unity 5 needs to define another function:

 inline void LightingCustom_GI(SurfaceOutputStandard s, UnityGIInput data, inout UnityGI gi) { LightingStandard_GI(s, data, gi); } 

It is used to calculate the extent to which the illumination affects the global illumination, but is not necessary for the purposes of our tutorial.

The result will come out exactly as we need:

image

In this first part, we learned how to use two different lighting models in one shader. This allowed us to render one half of the model using PBR, and the other to leave unlit. In the second part, we will complete this tutorial and show how to animate and improve the effect.

We cut off the geometry


The easiest way to add to our shader is the effect of stopping rendering the upper part of the geometry. To cancel drawing of an arbitrary pixel in the shader, you can use the discard keyword. With it, you can draw only the border around the top of the model:

 void surf (Input IN, inout SurfaceOutputStandard o) { if (IN.worldPos.y > _ConstructY + _ConstructGap) discard; ... } 

It is important to remember that this can leave “holes” in our geometry. It is necessary to turn off the clipping of the edges, so that the reverse side of the object is completely rendered.

 Cull Off 

image

Now we are most uncomfortable with the fact that the object looks hollow. This is not just a feeling: in essence, all 3D models are hollow. However, we need to create the illusion that the object is actually solid. This can be easily achieved by painting the object from the inside with the same unlit shader. The object is still hollow, but perceived to be filled.

To achieve this, we simply color the triangles that are directed to the camera on the reverse side. If you are unfamiliar with vector algebra, this may seem rather complicated. In fact, this can be quite easily achieved using a scalar product . The scalar product of two vectors shows how “aligned” they are. And this is directly related to the angle between them. When the scalar product of two vectors is negative, the angle between them is more than 90 degrees. We can test our initial condition by taking the dot product between the camera's gaze direction ( viewDir in the surface shader) and the triangle normal. If it is negative, then the triangle is turned away from the camera. That is, we see his "wrong side" and can render it in solid color.

 struct Input { float2 uv_MainTex; float3 worldPos; float3 viewDir; }; void surf (Input IN, inout SurfaceOutputStandard o) { viewDir = IN.viewDir; ... } inline half4 LightingCustom(SurfaceOutputStandard s, half3 lightDir, UnityGI gi) { if (building) return _ConstructColor; if (dot(s.Normal, viewDir) < 0) return _ConstructColor; return LightingStandard(s, lightDir, gi); } 

The result is shown in the images below. On the left, "purlid geometry" is rendered in red. If you use the color of the top of the object, the object no longer looks hollow.

image

Wavy effect


image

If you played Planetary Annihilation, then you know that the shader of the 3D printer uses the effect of a slight waviness. We can also implement it by adding a bit of noise to the position of the pixels drawn in the world. This can be achieved either by texture noise, or by using a continuous periodic function. In the code below, I use a sine wave with arbitrary parameters.

 void surf (Input IN, inout SurfaceOutputStandard o) { float s = +sin((IN.worldPos.x * IN.worldPos.z) * 60 + _Time[3] + o.Normal) / 120; if (IN.worldPos.y > _ConstructY + s + _ConstructGap) discard; ... } 

These parameters can be corrected manually for a beautiful wavy effect.

image

Animation


The last part of the effect is animation. It can be obtained by simply adding the _ConstructY parameter to the material. The shader will take care of the rest. You can control the speed of the effect either by code, or by using an animation curve. In the first version, you can fully control its speed.

 public class BuildingTimer : MonoBehaviour { public Material material; public float minY = 0; public float maxY = 2; public float duration = 5; // Update is called once per frame void Update () { float y = Mathf.Lerp(minY, maxY, Time.time / duration); material.SetFloat("_ConstructY", y); } } 

image

I note at the end that the model used in this image looks hollow for a few seconds, because the bottom of the accelerators is unlocked. That is, the object is actually hollow.

[You can download the Unity package (code, shader, and 3D models) by supporting the author of the original article with ten dollars to Patreon.]

Source: https://habr.com/ru/post/331576/


All Articles