3D Printer Shader Effect – Part 2

This is the second part of the tutorial that will recreate the 3D printer effect seen in games such as Astroneer and Planetary Annihilation.

a1

This is a two part tutorials:

A link to download the Unity package (code, shader and 3D models) is provided at the end of the tutorial.

Introduction

The first part of this tutorial shown how to render part of an object with an unlit lighting model (picture below). There are few other things that are yet to be achieved to fully recreate the beautiful style seen in Astroneer.

03

Cutting the Geometry

The easiest effect to add to our shader is to stop drawing the upper part of the geometry. The keyword discard can be used to arbitrarily prevent a pixel from being drawn in a shader. We can use it to ensure that only a rim around the top part out model is drawn:

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

	...
}

It is important to remember that this potentially leaves “holes” in out geometry. You should disable face culling, so that even the back of an object can be fully drawn.

Cull Off
04

Now, the most striking thing is that the object looks hollow. This is not just an impression: all 3D models are actually hollow. What we want to convey, however, is the illusion that the object is actually solid. This can be done easily by colouring the inside of an object with the same unlit shader. The object is still hollow, but it will be perceived as full.

To achieve this, we simply colour the triangles that are facing back to the camera. If you are unfamiliar with vector algebra, this might seen a rather complex condition to achieve. In reality, it can be done quite easily using the dot product. The dot product between two vectors indicates how “aligned” they are. Which is directly related with their angle. When the dot product between two vectors is negative, it means there is more than 90 degrees of separation between them. We can test for our original condition by taking the dot product between the view direction of the camera (viewDir in a surface shader) and the normal of a triangle. If it is negative, it means that the triangle is not facing the camera. Hence, we are seeing his “back” side; we can then render it with a solid colour.

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 following pictures. On the left, the “back geometry” is rendered in red. When we use the same colour for the top part of the object, it doesn’t look hollow anymore.

05

Wobbly Effect

pa2

If you have played Planetary Annihilation, you know that the 3D printer shader it uses has a gentle wobbly effect. We can add this as well, by adding some noise to the world position of the pixels we are drawing. This can be achieved either with a noise texture, or by using some continuous, periodic function. In the piece of code below, I have use a sinusoid function with some 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 have been tweaked manually until I obtained a pleasant wobbly effect.

07

Animation

The final part of this effect is the animation. This is achieved simply by changing the _ConstructY parameter of the material. The shader will take care of the rest. You can control the speed of the effect either via code, or using an animation curve. The former has the advantage that you can fully customise 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);
    }
}
08

As a final note, the model used in this picture looks hollow for few seconds because the bottom part of the boosters is not closed. Hence, it is actually hollow.

Conclusion

This concludes the 3D printer shader effect.

Patreon You can download the Unity project for this tutorial on Patreon.

A big thanks goes to the guys at System Era, and in particular to Jacob Liechty.

Comments

16 responses to “3D Printer Shader Effect – Part 2”

  1. Really awesome tutorial!! One question though.. How can I get rid of some weird shadows show up? And the overall shadow of the object as well… Thank you in advance!!

  2. Thanks for the tut, but I don’t know why you’re using world position instead of local. Local is way more flexible. Would have also been nice to have the full working shader available since it takes a bit of work to try to piece together all these puzzle pieces of code.

    1. Hey! Thank you!
      How would you have used local position in this context?

      1. Alex Cole avatar
        Alex Cole

        I must admit local space would make alot more sense. what if the 3d printer was up on a hill, the world space values to make the object appear would now be different. If it were local space then the same values would be used where ever the object appears in the game world.

        1. Alex Cole avatar
          Alex Cole

          if fact the example gif shows the 3d printer printing at angle, if world space was used the print “line|” would still be horizontal rather than at a angle..Local space would solve this

          1. Hey!
            Totally agree with this! :p

  3. Ben Golus avatar

    I would recommend using the VFace semantic if possible as using the per pixel interpolated normal will get you false positives.

    Secondly you’d be better off manipulating the albedo and emissive in the surf function rather than using a custom lighting function as it’ll continue to work in deferred, and won’t make the glowing areas brighter when using more than one light.

    Also the shader source for the PA shaders are all easy to access and look at, they’re just in a folder called shaders as text files. You can see all my horrible code and how many more layers of stuff I did for the PA shader. 🙂

    1. HEY Ben! 🙂
      Glad you made you’ve found this post! 🙂
      This is a nice introduction to the build effect, but is far from being perfect (or even optimised!).

      I didn’t know the shaders in PA were “available”. I guess I know how I’ll spend my evening. 🙂

      Thank you very much for your comment. And please, let me know if you’re coming to London anytime soon. I’d be super happy to have a chat! 🙂

  4. Hi, the effect is amazing, thanks for the tutorial but am I missing something or _ConstructY has to Range from the world position of the bottom of the object to the top of it in order to color it ? So do we have to manually enter those values in the shader in order to make it work ? Thanks again for the amazing work !

    1. Hey! Yes, indeed that needs to be changed manually!
      You can make a script that does that automatically, depending on the size of your 3D printer! 😀

  5. Hey Alan,
    I already followed the tutorial the other day and while it works pretty nicely I wanted to ask if you have any suggestion on how to get rid of the slight red fringe from the backfaces after the constructY value passed the entire object:
    https://dl.dropboxusercontent.com/u/13889971/3dprintshadereffect.gif
    greetings
    Julian

    1. Hey Julian!
      I suspect this is due to floating point errors with the dot product! Have you tried using a small margin of error? Something like:

      if (dot(s.Normal, viewDir) < 0 + margin) If you play with the value, you might get rid of the red fringe. Let me know if that works! 🙂

      1. This shader won’t work as expeccted even if the geometry was closed on the bottom, let alone overlapping geometry as in Julians example.

        The problem is that the re-interpreted backface is still using the original depth, so the Z-test is going to yield incorrect results.

        Fixing this requires to write to the depth buffer explicitly for the reinterpreted backfaces.

        1. That it can’t work with overlapping geometry was clear from the beginning, sure no problem.

          I believe Ext3h might be right in the end but the margin solution Alan suggested does indeed solve the issue as you can animate it to a slightly negative value towards the end of the animation and bam looks fine. You even might get an even more interesting effect moving it to a slightly positive value at the start of the construction effect.
          thx

  6. […] 3D Printer Shader Effect – Part 2 […]

Leave a Reply

Your email address will not be published. Required fields are marked *