Main menu

Working on plugins for 3ds Max has lead to some very interesting realizations about the product. In particular, it has a number of functions and settings for converting units of length, allowing you to easily build scenes measured in leagues, fathoms, furlongs, or light-years. Internally, it stores all distances in inches and does the conversion when it displays them (probably — it can actually be changed to use a different internal measurement as well to make things more confusing). Furthermore, the fundamental unit of time in 3ds Max is the tick, 1/4800th of a second. The tick was so chosen because it evenly divides both 24 and 30 frames per second and because it’s annoying.

So, in summary we have the tick as a unit of time and the probably-inch as a unit of length. The problem is we have no measures for any other quantities, nor can we without a unit of mass. Since it is absolutely essential to do physical/engineering calculations in 3ds Max, I propose the following: adopt the ubiquitous teapot as a fundamental unit of mass. To be specific, we will use the standard Utah Teapot full of standard Utah Tea. Extensive research places this between 20 and 30 oz so for the sake of argument we’ll call it 710 grams. If you are from the Mountain View Computer History Museum and are unfortunate enough to stumble upon this, could you verify this for me?.

From these quantities, we can easily define a coherent system of measures as follows:

Quantity

Unit

Derivation

Notes

Length

Probably-Inch

Fundamental unit

Defined as being an inch if 3ds Max decides it should be so.

Time

Tick

Fundamental unit

1/4800th of a second

Mass

Teapot

Fundamental unit

One standard Utah teapot full of standard Utah tea.

Velocity

Tinch

1 probably-inch per tick

Square time

Tock

Tick2

It just makes sense

Acceleration

Tonch

1 probably-inch per tock

i.e. 1 tonch = 1 tinch per tick

Force

Gigadormouse

1 probable teapot-inch per tock

A dormouse is a creature that lives in a teapot. If you collected 1 billion of them, they might conceivably exert this much force (about 1.2 giganewtons or 269 million pounds of force).

Power

Gigadormousepower

1 probable dormouse-inch per tick

91 horses are equivalent to one megadormouse.

Pressure

1 Gigadormouse per probable square inch

Understandably, this is also a measure of stress.

Density

1 teapot per probable cubic inch

This one is rather hard to visualize.

For the Greater Good of Humanity Itself, her are some interesting measurements:

A huge amount of effort has been made in 3D video games to produce “realistic” looking graphics. The idea is that you should feel like you’re playing a movie and the rendering should look like it was filmed with a camera. While there are some exceptions, other artistic styles — such as trying to look like a cartoon, a painting, a drawing, etc. — used to be largely ignored, or at least restricted to 2D games. Thankfully, this is changing. Especially so in the indy crowd, but also in big budget games like Team Fortress 2. This is great for graphics programmers to explore different rendering methods and lets a shader become part of an overall creative goal instead of a simulation of a camera.

For my game Space Rocks, I decided that I wanted it to look like the cover of a 60’s science fiction novel: illustrated, and somewhat cartoonish, but at the same time not cell shaded and outlined like a comic book or a Disney cartoon. As I mentioned, Valve accomplished something like this in TF2. What’s more they published a very good paper on their work.

This is a tutorial on how to implement this kind of shader in Unity3D. The complete shader source is attached at the end of the document. If you’ve never done any shader before, you might want to find try a few simpler tutorials first. However if you know a bit about shading languages, but are perhaps not familiar with Unity, this should give you a good introduction. The complete source is given at the end of this page.

Paperwork: the bits that make up a Unity shader

We will be making a Unity3D “Surface Shader”. This isn’t so much a shader as a template from which Unity will create numerous vertex and fragment shaders for different rendering paths. To create a surface shader we specify a lighting model (in this case a custom one called
LightingNPR) and a function that prepares the surface for lighting,
surf. We will use a normal map (bump map) though it should be removed where it is not needed. Here’s our starting point:

Beginnings of a Shader

C

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

Shader"Jonatron/NPR (Example)"

{

Properties

{

_MainTex("Main Texture",2D)="white"{}// These properties must have the same names as in standard Unity

_BumpMap("Bump Map",2D)="normal"{}// shaders in order for fallback shaders to correctly work.

_Color("Color",Color)=(1.0,1.0,1.0,1.0)

}

SubShader

{

Tags{"RenderType"="Opaque"}// This allows Unity to intelligently substitute this shader when needed.

// This is where we prepare the surcace for lighting by propagating a SurfaceOutput structure

half4c=tex2D(_MainTex,IN.uv_MainTex);// Sample the texture

o.Albedo=_Color.rgb*c;// Modulate by main colour

o.Alpha=1.0;// No alpha in this shader

// Apply bump map

o.Normal=UnpackNormal(tex2D(_BumpMap,IN.uv_MainTex));

}

ENDCG

}

FallBack"Diffuse"// Shader to use if the user's hardware cannot incorporate this one

}

The
surf function is pretty standard, but we need to fill in
LightingNPR .

The basics: Lambert’s model

Even if you have never heard of “Lambertian” shading, you’ve undoubtedly seen this if ever touched shaders before. It is the basis of almost all shaders and is used to compute diffuse illumination, \(I_d\), by taking the cosine of the angle between the surface’s normal vector, \(\hat{\mathbf{n}}\) and a unit vector towards the light \(\hat{\mathbf{l}}\). This makes sense because when the surface directly faces the light this angle will be zero and it’s cosine 1; if the surface is at right angle to the light then the cosine will be 0. This cosine is trivial to compute as it is simply the dot product of the two vectors: \[I_d = \max\{\hat{\mathbf{n}}\cdot\hat{\mathbf{l}}, 0\}\] Let’s add this to our lighting function, and include the surface colour, light colour, and attenuation:

This produces a smooth, realistic, and rather boring approximation of a matte surface, as illustrated by Suzanne the Blender Monkey:

Lambertian Diffuse

Closer to illustaion: Gooch shading

Lambert’s model is used to directly compute the brightness of a surface. However, Gooch observed that artists often use other cues in addition to just light and dark to show illumination and shading. Specifically, warm colours (red, yellows, and oranges) show illumination and cool colours (blues, teals, and purples) show shading. We can incorporate these principles very easily using a light ramp: a one dimensional colour gradient imported into our shader as a texture.

3

4

5

6

7

8

9

Properties

{

_MainTex("Main Texture",2D)="white"{}// These properties must have the same names as in standard Unity

_BumpMap("Bump Map",2D)="normal"{}// shaders in order for fallback shaders to correctly work.

_Ramp("Ramp",2D)="white"{}

_Color("Color",Color)=(1.0,0.8,0.2,1.0)

}

We still compute Lambert’s diffuse term, but instead of using it as a measure of lightness, we use it as a position in the light ramp:

When we combine this with a ramp from a dark, cool colour to a warm, bright colour, we get a result much closer to an artistic rendering:

Simple Gooch cool/warm light ramp

This is an incredibly versatile tool for both illustrative and photorealistic shading. It can be used to produce some very unusual highlights:

An unusual light ramp

And it can even be used to create a cell-shaded effect if the texture contains only two colours and uses no filtering:

A two-pixel light ramp used for cell shading

Controlling the transition: warped Lambert

The light ramp gives us excellent control, but for that final touch we want some finer control over how the shader traverses the ramp. To do this, we add a scale \(\alpha\), bias \(\beta\), and exponent \(\gamma\) to Lambert’s term: \[I_d = \left(\alpha(\max\{\hat{\mathbf{n}}\cdot\hat{\mathbf{l}},0\}) + \beta\right)^\gamma\]

3

4

5

6

7

8

9

10

11

12

Properties

{

_MainTex("Main Texture",2D)="white"{}// These properties must have the same names as in standard Unity

_BumpMap("Bump Map",2D)="normal"{}// shaders in order for fallback shaders to correctly work.

This is called the “warped” diffuse model. By varying the scale, bias, and exponent parameters we can control the size of the shaded area, the total amount of difference between dark and light, and the sharpness of transition:

Scale: 0.5, bias: 0.5, exponent: 2

Scale: 0.5, bias: 0.6, exponent: 4

Defining edges

The shader still lacks some indication of the object’s edge. A common way to make the edges stand out more is to add a solid outline, but this clashes somewhat with the smoothness of the shading so far. Instead, we will use rim lighting. Similarly to Lambert’s, we compute this by looking at the cosine of the angle between the surface normal and the camera. Again, we can calculate the cosine using a dot product:

\[I_r = 1 – \max\{\hat{\mathbf{v}}\cdot\hat{\mathbf{n}},0\}\]

This will produce a smooth transition from the edge of the object to it’s face, regardless of the camera’s orientation. In order to make the transition a little sharper, we raise the whole thing to a power:

By varying the colour we can also produce interesting effects such as the alien glow on the asteroids in Space Rocks:

Some Space Rocks with coloured rim lights to create an alien-looking effect.

Adding shininess

For shiny, glossy objects, we need a specular highlight. This is used to show a “reflection” of the light source in the surface. We will use Phong’s model as it is simple and widely used. Blinn’s model could be used and offers slightly better performance under some circumstances. To compute Phong’s specular highlight we need the vector \(\mathbf{\hat{r}}\) of the light direction reflected about the surface normal. This can be computed as \(\mathbf{\hat{r}} = 2(\mathbf{\hat{l}}\cdot\mathbf{\hat{n}})\mathbf{\hat{n}} – \mathbf{\hat{l}}\). However, Cg has a built-int
reflect function. We compute the final highlight as the angle between the relfected vector and the camera. We raise the result to a “shininess” power to sharpen it (sometimes called the “Phong exponent”): \[I_s = (\hat{\mathbf{r}}\cdot\hat{\mathbf{v}})^p\]

Rim lighting and specular only. The diffuse colour has been changed from other images to accommodate the increase in brightness added by other light terms.

Rim lighting and specular only

Results

This shader is incredibly versatile. It easily accommodated the visual style I needed for Space Rocks and seems quite versatile. It can, of course, also be used to render in TF2’s style.

Our intrepid hero’s spacecraft.

A sinister alien.

Complete Shader

Complete Shader

C

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

Shader"Jonatron/NPR"

{

Properties

{

_MainTex("Main Texture",2D)="white"{}// These properties must have the same names as in standard Unity

_BumpMap("Bump Map",2D)="normal"{}// shaders in order for fallback shaders to correctly work.

_Ramp("Ramp",2D)="white"{}

_Color("Color",Color)=(1.0,1.0,1.0,1.0)

_DiffuseScale("Diffuse Scale",Float)=1

_DiffuseBias("Diffuse Bias",Float)=0

_DiffuseExponent("Diffuse Exponent",Float)=1

_RimColor("Rim Color",Color)=(0.26,0.19,0.16,0.0)

_RimPower("Rim Power",Range(0.5,8.0))=3.0

_SpecColor("Specular Color",Color)=(0.0,0.0,0.0,1.0)

_SpecPower("Specular Power",Range(0.5,128.0))=3.0

}

SubShader

{

Tags{"RenderType"="Opaque"}// This allows Unity to intelligently substitute this shader when needed.

Hello! I am Jonatron! I am a graphics and game programming in Victoria, BC, Canada and I will be populating this website with graphicsy/gamey/bloggy stuff. To launch it off, I’ve uploaded a release candidate of an asteroid blasting arcade game: Space Rocks. Play and enjoy.