8.1 Shaders for Artists

Phase 8 · Technical Art · Study time: 30–50 h

Authoring materials and shader effects with node graphs and light HLSL, focused on visual results rather than low-level graphics APIs.

1. What a technical artist actually does

A technical artist (often shortened to "TA") sits between the art team and the programming team. Artists model, texture, and animate; programmers write gameplay code and engine systems. The technical artist's job is to build the look of the game — water, fire, magic, dissolving enemies, holograms, glowing rims — without writing a full rendering engine from scratch.

The tool that makes this possible is a node graph: a visual editor where you connect small boxes (called nodes) with wires instead of typing lines of code. In Unity the built-in tool is called Shader Graph. A popular third-party alternative is Amplify Shader Editor. Both work the same way: you wire nodes together, and the tool generates real GPU code behind the scenes.

That generated code is written in HLSL (High-Level Shading Language) — the language GPUs actually run. You do not need to write HLSL by hand to make most of the effects in this lesson. But knowing roughly what HLSL a node produces makes you a much stronger technical artist, because you can read other people's shaders, debug weird visual bugs, and drop into a Custom Function node when a graph alone cannot do what you need. That is why every recipe in this lesson shows you both: the node graph AND the plain HLSL it is equivalent to.

a technical artist's workflow [ you, dragging nodes in the editor ] | v +----------------------+ | node graph | (Shader Graph / Amplify) | o--o o--o | | \ \ / / | | o--MASTER--o | +----------------------+ | v (the tool compiles this for you) +----------------------+ | generated HLSL | (real GPU code, C-like syntax) +----------------------+ | v +----------------------+ | GPU renders it | --> pixels on screen +----------------------+

Nothing here is magic. A node graph is a drawing tool for writing code. Every node is a small piece of math or a texture lookup, and every wire is a value flowing from one calculation into the next. Once you can read a graph as "math flowing left to right," shaders stop being scary.

Tip If you already know C or C# from earlier lessons, HLSL will feel very familiar: it has float, float2, float3, float4, functions, if, loops, and math operators. The main difference is that a shader runs once per vertex or once per pixel, thousands of times in parallel, instead of once from a single main().

2. Node graphs 101 — anatomy of a graph

Every node graph, no matter the tool, is built from the same three ingredients:

Every graph ends at one special node: the Master node (split into a Fragment and a Vertex stack in newer Shader Graph, or a single output node in Amplify). This is where you answer the questions the renderer asks about each pixel: "what color is this," "which way does its surface normal point," "how metallic and how smooth is it," "does it glow," "is it see-through."

+-----------------+ | Sample Tex 2D | | UV --> o | | Tex --> o RGBA o---+ +-----------------+ | v +-------------------+ | MASTER NODE | | Base Color o | <-- wire lands here | Normal o | | Metallic o | | Smoothness o | | Emission o | | Alpha o | +-------------------+

That tiny graph above is a complete, working shader. It says: "look up a pixel color from this texture, using these UVs, and use that as the Base Color." Nothing else is plugged in, so the engine uses sensible defaults for Metallic, Smoothness, and the rest. This is the same idea as a C# method that only fills in the parameters it cares about and leaves the others at their default values.

Here is the HLSL a graph like this actually compiles down to, simplified:

// Simplified fragment shader — roughly what the graph above generates
Texture2D _MainTex;
SamplerState sampler_MainTex;

float4 Frag(float2 uv : TEXCOORD0) : SV_Target
{
    float4 baseColor = _MainTex.Sample(sampler_MainTex, uv);
    return baseColor; // becomes the Base Color the renderer lights and shades
}

Every node you drag into Shader Graph turns into a line or two like this. Learning shaders through node graphs and learning shaders through HLSL are the same skill — the graph just draws the data flow for you.

Tip In Shader Graph you can view the generated code from the graph's context menu ("View Generated Shader"). Reading it after you build a graph is one of the fastest ways to learn what each node is really doing.

3. Textures and UV space — sampling a texture

A texture is just an image stored in memory — a grid of pixels, each with a color (and often extra channels like transparency). To put that image onto a 3D mesh, every vertex of the mesh stores an extra pair of numbers called UV coordinates (the letters U and V are used instead of X and Y so they are not confused with 3D position). UV values normally range from 0 to 1, no matter how big the actual texture is in pixels.

V 1 +-------------------+ | | | texture | | image | | | 0 +-------------------+ 0 1 U (0,0) = bottom-left corner of the texture (1,1) = top-right corner of the texture (0.5,0.5) = dead center

The node (or HLSL function) that reads a color out of a texture at a given UV is called sampling. In Shader Graph it is the Sample Texture 2D node: plug in a Texture2D and a UV (Vector2), and it outputs an RGBA color, plus separate R, G, B, A outputs for when you only need one channel.

// HLSL equivalent of "Sample Texture 2D"
Texture2D _Tex;
SamplerState sampler_Tex; // the sampler decides filtering (blurry vs blocky) and wrap mode

float4 SampleMyTexture(float2 uv)
{
    return _Tex.Sample(sampler_Tex, uv);
}

Two sampler settings matter a lot for art: Filter (Point = blocky/pixelated, Bilinear/Trilinear = smooth) and Wrap Mode (Repeat = the texture tiles forever past 0 and 1, Clamp = it stretches its edge pixels forever, useful for UI or decals).

Common mistake Forgetting that UVs are per-mesh, not per-world. Two objects both sampling UV (0.5, 0.5) are each reading the middle of their own texture space — moving an object in the world does not change its UVs. A texture that should react to world position instead (like a puddle that stays put while a character walks over it) needs a different technique, sampling by world position, which is outside this lesson.

4. UV manipulation — tiling, offset, and panning

Once you can sample a texture, the next skill is bending the UVs themselves before the sample happens. Three moves cover almost everything an artist needs:

UV --> [multiply by Tiling] --> [add Offset] --> [Sample Texture] --> color Panning adds one more step before the sample: Time --> [multiply by Speed] --> [add to UV] --> [Sample Texture] --> color
// HLSL: tiling + offset + panning combined
float2 PanUV(float2 uv, float2 tiling, float2 offset, float2 speed, float time)
{
    float2 tiled  = uv * tiling + offset;   // Shader Graph's "Tiling And Offset" node
    float2 panned = tiled + speed * time;   // scroll over time
    return panned;
}

Picture a water texture sampled with UV between (0,0) and (1,1). With speed = (0.1, 0), after 10 seconds the sample point has drifted by 1.0 in U — a full texture width — so the pattern has scrolled once and quietly wrapped back around. This only looks seamless if Wrap Mode is set to Repeat, and the source texture was painted so its edges line up (a "tileable" texture).

time = 0s time = 3s time = 6s +----------+ +----------+ +----------+ | ~ ~ ~ | | ~ ~ ~ | |~ ~ ~ | | wave art | scrolling --> | wave art | scrolling --> | wave art | | ~ ~ ~ | | ~ ~ ~ | |~ ~ ~ | +----------+ +----------+ +----------+ the pattern slides in one direction every frame, then wraps seamlessly
Tip Panning two texture samples (like two cloud layers) at slightly different speeds and directions, then blending them, reads as much more natural motion than panning one layer fast. This trick reappears in the water/lava recipe below.

5. Blending two things — Lerp

Lerp stands for linear interpolation. It takes two values A and B and a blend factor T that runs from 0 to 1, and returns a value that slides smoothly from A (at T=0) to B (at T=1).

// Lerp works on floats, colors, and vectors — same formula every time
float Lerp(float a, float b, float t)
{
    return a + (b - a) * t;
}
// t = 0    returns a
// t = 0.5  returns exactly halfway between a and b
// t = 1    returns b
A = red B = blue t=0.0 [RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR] pure A t=0.25 [RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRBBBBBBBB] mostly A t=0.5 [RRRRRRRRRRRRRRRRRRRRBBBBBBBBBBBBBBBBBB] half and half t=0.75 [RRRRRRRRBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB] mostly B t=1.0 [BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB] pure B

In Shader Graph, the Lerp node takes A, B, T and outputs the blended value. T almost never comes from nowhere — it usually comes from another texture (a mask), from a Fresnel node, from noise, or from a slider the artist exposes as a material property. Every recipe later in this lesson uses Lerp at least once, because "blend between two things using a mask" is the single most common operation in shader art.

6. Masks and edges — Step and Smoothstep

Lerp blends smoothly across the whole 0..1 range. Sometimes you want a hard cutoff instead — a clean line where one side is fully one thing and the other side is fully something else. That is what Step does.

// Step: a hard on/off switch
float Step(float edge, float x)
{
    return (x < edge) ? 0.0 : 1.0;
}

Step(0.5, x) returns 0 for every x below 0.5, and 1 for every x at or above 0.5. Feed that result into a Lerp's T, or use it directly as a mask, and you get a razor-sharp edge instead of a gradient.

Smoothstep is Step's softer cousin: instead of one edge value, it takes two — edge0 and edge1 — and eases smoothly between them instead of snapping.

// Smoothstep: an eased ramp between edge0 and edge1
float Smoothstep(float edge0, float edge1, float x)
{
    float t = saturate((x - edge0) / (edge1 - edge0)); // saturate = clamp to 0..1
    return t * t * (3.0 - 2.0 * t); // the "ease" curve — flat at both ends
}
output 1 | ______________ | / | | smoothstep(0.4, 0.6, x) -- eased | / 0 |______________/ +--------------------------------------- x 0 0.4 0.5 0.6 1 step(0.5, x) instead jumps instantly at x = 0.5: 1 | _________________ | | 0 |_________________| +--------------------------------------- x 0 0.5 1

Where these show up: a dissolve edge needs a thin band that glows right at the cutoff line — you get that band by combining two smoothsteps, or by using Smoothstep with a very narrow edge0/edge1 gap. A cel-shaded (toon) light band uses Step or Smoothstep on the lighting value instead of a smooth gradient, to get flat color bands. A water shoreline foam line uses Step on "distance from the shore" to decide where foam starts.

Tip In Shader Graph both nodes are literally called Step and Smoothstep. If a graph looks too "flat plastic," that is often a Step being used where a Smoothstep (or a small-range Smoothstep) would look softer and less jagged.

7. Remap — fitting one range into another

A lot of shader inputs do not naturally come in the range you need. A noise texture samples out values from 0 to 1, but you might need -1 to 1 to push a normal map both left and right. A mask might only ever reach 0.2 to 0.8 in practice, and you want to stretch it to use the full 0..1 range for more contrast. Remap does exactly this: it takes a value inside an input range and rescales it into an output range, keeping its relative position.

// Remap: rescale x from [inMin, inMax] into [outMin, outMax]
float Remap(float x, float inMin, float inMax, float outMin, float outMax)
{
    float t = (x - inMin) / (inMax - inMin); // where x sits inside the input range, as 0..1
    return outMin + t * (outMax - outMin);   // place that same relative position in the output range
}

// Example: Remap(0.75, 0.0, 1.0, -1.0, 1.0)
//   t = (0.75 - 0) / (1 - 0) = 0.75
//   result = -1 + 0.75 * (1 - (-1)) = -1 + 1.5 = 0.5

In Shader Graph this is the Remap node, with inputs In, In Min Max, and Out Min Max. You will reach for it constantly: brightening a dim mask, converting a 0..1 noise into a -1..1 displacement, or squeezing a Fresnel result into a narrower band so a rim glow looks tighter.

8. Time and noise — animation and organic variation

Two more building blocks finish the toolkit. The Time node outputs the number of seconds since the scene started (Shader Graph also gives you pre-scaled versions for slow or fast effects). Multiply Time by a speed and feed it anywhere a number is expected — UV offset, a rotation angle, a color's brightness — and that value now changes every frame, which is what makes an effect feel alive instead of a still image.

Noise nodes (Shader Graph ships a Simplex Noise node; Perlin Noise is the classic equivalent) generate a grayscale pattern of smooth, organic blobs from a UV input — no texture asset needed. Unlike pure random static, noise is continuous: nearby UV positions produce similar values, so it looks like natural variation (clouds, dissolve patterns, skin pores) instead of TV snow.

noise output (0 = black, 1 = white), sampled over a UV grid: 0.1 0.3 0.6 0.8 0.9 0.2 0.4 0.7 0.9 0.7 0.4 0.6 0.8 0.6 0.4 <- smooth blobs, neighbors are close in value 0.6 0.8 0.6 0.3 0.2 0.8 0.7 0.4 0.2 0.1 compare to pure random noise, where neighbors have NO relationship: 0.9 0.1 0.7 0.2 0.9 0.2 0.8 0.1 0.9 0.3 <- static / TV-snow look, rarely what artists want

Noise is the secret ingredient behind almost every "magical" or "organic" effect: dissolve patterns, fire flicker, hologram glitches, water surface distortion, cloud shapes. Combined with Time (scrolling the UV fed into the noise node) you get noise that animates — churning fire, drifting fog, a pulsing forcefield.

Common mistake Using a very high-frequency (small-scale) noise for something meant to read as large, slow shapes — like clouds — makes it look like TV static instead. Turn the noise's own UV tiling down (bigger blobs) for big slow shapes, and up for fine grain like skin or rust.

9. Recipe: scrolling water / lava material

The idea

Water and lava both use the same trick: take a tileable texture (waves or cracked magma) and a matching normal map (a texture that stores fake surface bumps so lighting reacts as if the surface were wavy, even though the mesh is flat), then pan both across the surface over time. Panning two normal-map samples at different speeds and directions, then blending them, breaks up the obviously-repeating pattern and reads as constantly shifting liquid.

Node flow

Time --> [x speedA] --> [+UV] --> [Sample Normal A] --+ Time --> [x speedB] --> [+UV] --> [Sample Normal B] --+--> [Blend Normals] --> MASTER.Normal Time --> [x speedA] --> [+UV] --> [Sample Albedo] ----------------------------> MASTER.Base Color

Light HLSL

float2 uvA = uv * tiling + _Time.y * speedA; // _Time.y is seconds in Unity's built-in shaders
float2 uvB = uv * tiling + _Time.y * speedB * float2(0.7, -1.0); // different direction and speed

float3 normalA = UnpackNormal(_NormalTex.Sample(sampler_NormalTex, uvA));
float3 normalB = UnpackNormal(_NormalTex.Sample(sampler_NormalTex, uvB));
float3 blendedNormal = normalize(normalA + normalB); // simple, cheap normal blend

float3 albedo = _AlbedoTex.Sample(sampler_AlbedoTex, uvA).rgb;

What it looks like

A flat plane reads as a gently rolling water surface: light glints shift and swim across it instead of standing still, even though no vertex ever moves. For lava, swap the blue-ish water normal/albedo pair for an orange, cracked-rock pair, and add a strong Emission from a separate, slower-panned "glowing cracks" texture.

Explanation

The mesh is completely static — only the UVs fed into the texture samples change every frame. That is the whole trick: motion made entirely out of "what pixel do I look up," at almost no performance cost, because the GPU was already sampling a texture anyway.

10. Recipe: dissolve effect

The idea

A "dissolve" makes an object crumble away along a noisy, organic edge instead of just fading out or popping — think of a Marvel-style disintegration, or an enemy burning away. The trick: sample a noise texture across the surface, compare it against a Dissolve Amount slider (0 = fully visible, 1 = fully gone) using Step, and cut away (clip) any pixel whose noise value is below that threshold. A thin Smoothstep band right at the cutoff is colored bright and pushed into Emission to fake a burning edge.

Node flow

UV --> [Simplex Noise] --> noiseValue noiseValue, DissolveAmount --> [Step] --> clipValue --> MASTER.Alpha Clip Threshold noiseValue, DissolveAmount --> [Smoothstep band] --> edgeMask --> [x EdgeColor] --> MASTER.Emission

Light HLSL

float noiseValue = SimplexNoise(uv * noiseTiling);

// cut the pixel away entirely once noise falls below the dissolve amount
clip(noiseValue - dissolveAmount); // clip(x): if x < 0, discard this pixel, draw nothing

// thin glowing band exactly at the cutoff line
float edgeWidth = 0.05;
float band = smoothstep(dissolveAmount, dissolveAmount + edgeWidth, noiseValue)
           - smoothstep(dissolveAmount + edgeWidth, dissolveAmount + edgeWidth * 2.0, noiseValue);
float3 emission = edgeColor * band * edgeBrightness;

What it looks like

dissolveAmount = 0.0 dissolveAmount = 0.45 dissolveAmount = 0.9 +------------------+ +------------------+ +------------------+ |##################| |####~~~~~~~~~~~~~~| |~~~~~~~~~~~~~~####| |##################| |###~~~~~~~~~~~~~~~| |~~~~~~~~~~~~~~~###| |##################| |####~~~~~~~~~~~~~~| |~~~~~~~~~~~~~~####| |##################| |#####~~~~~~~~~~~~~| |~~~~~~~~~~~~~#####| +------------------+ +------------------+ +------------------+ fully visible # = still solid, ~ = gone, almost gone, glowing edge sits right tiny solid on the noisy border patch left

Explanation

clip() is an HLSL instruction that throws away a pixel completely (it never gets written to the screen or the depth buffer) if the value passed in is negative. By comparing noise to a slider you drive from code (for example, animating dissolveAmount from 0 to 1 over 2 seconds when an enemy dies), the cutoff line creeps across the noise pattern, and because the noise is organic-looking, the edge looks like burning or crumbling instead of a straight wipe.

Tip Alpha Clip Threshold on Shader Graph's Master node does the clip() call for you — wire noiseValue - dissolveAmount into it (or use the dedicated Clip node) and enable Alpha Clipping in the graph settings.

11. Recipe: fresnel rim glow

The idea

Look at a glass bottle or a soap bubble: the edges, where you are looking almost along the surface instead of straight at it, look brighter and more reflective than the center. That real-world phenomenon is called the Fresnel effect (pronounced "freh-NEL"), and it is the basis of almost every glowing-rim or energy-shield effect in games. It compares the direction the camera is looking at a point against that point's surface normal (the direction the surface faces): dead-on = 0, grazing edge = 1.

Node flow

Normal (world space) --> [Fresnel Effect] --> fresnelValue ViewDirection --------> [Fresnel Effect] fresnelValue --> [Power: rimPower] --> [x RimColor] --> MASTER.Emission

Light HLSL

float3 viewDir = normalize(_WorldSpaceCameraPos - worldPos);
float fresnel = 1.0 - saturate(dot(normalize(worldNormal), viewDir));
fresnel = pow(fresnel, rimPower); // higher power = thinner, tighter rim

float3 rimEmission = rimColor.rgb * fresnel * rimIntensity;

What it looks like

looking straight at a sphere: .::::::. .:: ::. : (dim center) : : : ':. .:' glow -> '::::::::' <- glow (bright rim where the surface curves away from you)

Explanation

dot(normal, viewDir) (the dot product, a way to compare two directions: 1 when they point the same way, 0 when perpendicular) is largest when you are looking straight at a surface, and shrinks toward 0 at the silhouette edge. Flipping it with 1 - x makes the edges bright instead of the center, and pow() lets you squeeze that bright zone into a thin line (high power) or a broad glow (low power, close to 1). Because it only touches Emission, this recipe is cheap and stacks on top of any other material — you almost never build a shader that is only fresnel; you add fresnel to an existing material.

12. Recipe: hologram effect

The idea

A hologram look combines three things you already know: a Fresnel rim (so edges glow more than flat faces, like a projection), horizontal scanlines (thin repeating stripes made from UV plus a sine wave), and a flicker (the whole thing's brightness wobbling over time, using a sine wave or noise), all pushed into Emission on top of a base color, usually with the material set to transparent.

Node flow

Normal, ViewDir --> [Fresnel] --> [Power] --> fresnelGlow UV.y --> [x scanlineFreq] --> [Sine] --> [Remap -1..1 to 0..1] --> [Smoothstep] --> scanlineMask Time --> [x flickerSpeed] --> [Sine] --> [Remap to 0.6..1.0] --> flickerAmount (fresnelGlow + scanlineMask) --> [x flickerAmount] --> [x HologramColor] --> MASTER.Emission

Light HLSL

float fresnel = pow(1.0 - saturate(dot(normalWS, viewDir)), rimPower);

float scanline = sin(uv.y * scanlineFrequency + _Time.y * scrollSpeed);
scanline = scanline * 0.5 + 0.5;           // remap -1..1 to 0..1
scanline = smoothstep(0.4, 0.6, scanline); // sharpen into thin bright lines

float flicker = sin(_Time.y * flickerSpeed) * 0.5 + 0.5;
flicker = lerp(0.6, 1.0, flicker);         // never fully off, just dimmer

float3 emission = hologramColor.rgb * (fresnel + scanline) * flicker;

What it looks like

---------------------- |~~~~~~~~~~~~~~~~~~~~| <- thin bright scanlines |====================| running across the body |~~~~~~~~~~~~~~~~~~~~| |====================| :\ /: <- edges (fresnel) glow brighter than : \ brighter rim / : the flat front-facing surface : \______________/ : the whole image gently pulses brighter and dimmer over time (flicker)

Explanation

None of the three ingredients are new — this recipe is proof that a handful of building blocks (Fresnel, a sine wave driven by UV, a sine wave driven by Time) recombine into a completely different-looking effect just by changing what feeds what. Scanlines are a sine wave of UV.y sharpened with Smoothstep into stripes; flicker is a sine wave of Time remapped so it dims but never goes fully black. This is the normal way technical artists work: not inventing new math, but recombining the same eight or nine nodes in a new order.

Tip Swap the clean sin() flicker for a Noise node sampled with only Time as input (no UV) for a less regular, more "glitchy" flicker — a regular sine wave reads as mechanical, noise reads as unstable.

13. Two more recipes: color tint / mask blend, and wind on grass

Recipe: color tint / mask blend

The idea: games often need the same mesh in different team colors, faction colors, or material variants (a character's cloth is one color, its leather straps another) without authoring separate textures for every combination. The trick is a grayscale mask texture painted once (white = "recolor this," black = "leave this alone"), used as the T of a Lerp between the original albedo and a tint color exposed as a material property.

UV --> [Sample Albedo] --> albedoColor UV --> [Sample Mask.R] --> maskValue [Lerp: A=albedoColor, B=TintColor, T=maskValue] --> MASTER.Base Color
float3 albedo = _AlbedoTex.Sample(sampler_AlbedoTex, uv).rgb;
float  mask   = _MaskTex.Sample(sampler_MaskTex, uv).r; // 0..1, painted by an artist
float3 finalColor = lerp(albedo, tintColor.rgb, mask);

What it looks like: a character's cloth shifts to whatever team color a match assigns, while its skin, metal buckles, and leather (all painted black in the mask) never change. Explanation: this is Lerp from section 5, with the mask texture supplying a different T per pixel instead of a single value — the mask is really just a saved, hand-painted map of blend factors.

Recipe: wind on grass (vertex displacement)

The idea: everything so far only changed pixel colors (the fragment/pixel stage). Shaders can also move geometry, in the vertex stage, before rasterization even happens. For grass blowing in the wind, you do not want the whole blade to slide sideways (it would look like it is floating) — only the tip should sway, while the base stays planted. That per-vertex "how much can this point move" amount is painted into the mesh's vertex color when the grass asset is made (white at the tip, black at the base), and the shader reads it back as a mask.

Time --> [x windSpeed] --> [Sine] --> [x windStrength] --> swayAmount VertexColor.R (0 base, 1 tip) --> [x swayAmount] --> finalSway VertexPosition --> [+ finalSway on X] --> MASTER.Vertex Position
// Vertex shader -- runs once per vertex, not per pixel
float windPhase = _Time.y * windSpeed + worldPos.x * 0.3; // offset by position so blades don't all sway in sync
float sway = sin(windPhase) * windStrength;

float tipAmount = vertexColor.r; // 0 at the base, 1 at the tip -- painted into the mesh
float3 displaced = vertexPosition + float3(sway, 0, 0) * tipAmount;
base pinned, tip swaying: tip \ / \ | / \ / \ | / blade | | --> | | | | | | | | base _|__|_ _|____|_ _|__|_ ====================================== (ground, never moves)

Explanation: vertexColor.r is a value painted directly onto the mesh's vertices (most 3D tools let an artist paint vertex colors like a coarse, per-vertex texture). Multiplying the sway by that mask means the base (r=0) is multiplied by zero and never moves, while the tip (r=1) gets the full sway. Adding worldPos.x into the sine's input means neighboring blades sit at slightly different points in the wave, so a whole field sways like a rolling wave instead of every blade snapping left and right in perfect unison.

Common mistake Doing vertex displacement in the wrong space and forgetting it needs to end up in the same space the Master node's Vertex Position expects. Also: heavy per-vertex displacement needs a mesh with enough vertices to bend smoothly — a 4-vertex grass quad cannot show a curve, only a straight tilt.

14. Glossary

15. Exercises

Exercise 1 — Panning math A water material samples its normal map with Tiling = (2, 2), Offset = (0, 0), and pans it with Speed = (0.1, 0.05). The mesh's raw UV at a point on the surface is (0.3, 0.6). What UV is actually sampled at Time = 4.0 seconds? Use finalUV = uv * tiling + offset + speed * time.
Show answer
tiled      = uv * tiling         = (0.3 * 2, 0.6 * 2)   = (0.6, 1.2)
tiled+off  = tiled + offset      = (0.6, 1.2) + (0, 0)   = (0.6, 1.2)
panAmount  = speed * time        = (0.1*4, 0.05*4)       = (0.4, 0.2)
finalUV    = tiled+off + panAmount                       = (1.0, 1.4)

The sampled UV is (1.0, 1.4). A value above 1 means nothing special by itself — it is the Wrap Mode that decides what happens next. With Repeat wrap mode (the normal choice for a scrolling water or lava tile), the GPU quietly wraps 1.0 back to 0.0 and 1.4 back to 0.4, so it actually samples the same pixel as UV (0.0, 0.4) — the texture has scrolled past a full tile and wrapped seamlessly. With Clamp instead, sampling would incorrectly freeze on the texture's edge pixel, which is why panning textures should always use Repeat.

Exercise 2 — Step vs. Smoothstep math A dissolve shader has dissolveAmount = 0.4 and an edge width of 0.05. At one pixel, the noise texture returns 0.42. (a) Does clip(noiseValue - dissolveAmount) keep or discard this pixel? (b) What is the value of smoothstep(0.4, 0.45, 0.42), rounded to two decimal places, and what does it represent?
Show answer
(a) clip(noiseValue - dissolveAmount) = clip(0.42 - 0.4) = clip(0.02)
    0.02 is NOT less than 0, so the pixel is KEPT (still visible)

(b) smoothstep(edge0=0.4, edge1=0.45, x=0.42)
    t      = (x - edge0) / (edge1 - edge0) = (0.42 - 0.4) / (0.45 - 0.4) = 0.02 / 0.05 = 0.4
    result = t*t*(3 - 2*t) = 0.4*0.4*(3 - 0.8) = 0.16 * 2.2 = 0.352
    rounded: 0.35

(a) The pixel survives — it is just barely past the dissolve threshold, so it has not been eaten away yet. (b) 0.35 is how far this pixel sits inside the thin glowing edge band, on a curved (eased) scale rather than a straight line. It is used to multiply the edge color for the burning-edge Emission, so a pixel right at the very start of the band glows dimly and one further into the band glows closer to full brightness.

Exercise 3 — Design a node graph Design (as an ASCII node-flow diagram, like the ones in this lesson) a "magic forcefield" material: it should have a fresnel rim glow, a slowly scrolling hexagon-pattern texture in Emission, and the whole thing should pulse brighter and dimmer over time. List every node you would use and what feeds what.
Show answer

One reasonable graph (this is a design exercise, so other layouts can be just as correct):

Normal, ViewDir --> [Fresnel Effect] --> [Power] --> rimGlow Time --> [x scrollSpeed] --> [add to UV] --> [Sample Hex Texture] --> hexPattern (rimGlow + hexPattern) --> combinedGlow Time --> [x pulseSpeed] --> [Sine] --> [Remap -1..1 to 0.5..1.0] --> pulseAmount combinedGlow --> [x pulseAmount] --> [x ForcefieldColor] --> MASTER.Emission

Reading it left to right: Fresnel gives the rim brightness. A second branch pans UV over time and samples a hexagon-pattern texture, so the hex pattern slowly drifts across the surface. Those two are added together (rim plus hex pattern) — using Add here instead of Lerp because both should show up at once, not blend between each other. A third branch drives a sine wave from Time, remapped so brightness never drops below half, and multiplies the combined result — this is the same "flicker" idea from the hologram recipe, just applied to the whole effect instead of only Emission's base value. Finally everything is tinted by a single exposed ForcefieldColor property and fed into Emission, so a designer can reuse one shader for a red enemy shield and a blue player shield just by changing that one color.

← Back to all chapters