<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: DeGGeD</title>
    <description>The latest articles on DEV Community by DeGGeD (@degged).</description>
    <link>https://dev.to/degged</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3245515%2Fcee63167-bfaa-44bb-b39c-90dcd07344e6.jpg</url>
      <title>DEV Community: DeGGeD</title>
      <link>https://dev.to/degged</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/degged"/>
    <language>en</language>
    <item>
      <title>Vertex Animation in Games: How to Make It Art-Directable, Performant, and Shippable</title>
      <dc:creator>DeGGeD</dc:creator>
      <pubDate>Fri, 13 Nov 2026 08:51:49 +0000</pubDate>
      <link>https://dev.to/degged/vertex-animation-in-unity-how-to-make-it-art-directable-performant-and-shippable-454k</link>
      <guid>https://dev.to/degged/vertex-animation-in-unity-how-to-make-it-art-directable-performant-and-shippable-454k</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Making vertices move is easy. Making the motion art-directable, cheap at scene scale, and correct across the entire render pipeline is the skill real production actually demands.&lt;/em&gt;&amp;nbsp;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Vertex tricks are among the most popular topics among technical artists and developers and for good reason: they deliver fast, impressive results. Flags flutter, foliage sways, water ripples, list can go on.&lt;/p&gt;

&lt;p&gt;Then the effect leaves the demo scene, and the real work begins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The leaves move, but the shadow stays still.&lt;/li&gt;
&lt;li&gt;The entire forest sways in perfect unison (unnatural).&lt;/li&gt;
&lt;li&gt;The effect pops out of existence near the edge of the screen.&lt;/li&gt;
&lt;li&gt;TAA smears the foliage, even though the geometry looks correct.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the devil in the details and as you'll discover if you've been in the industry long enough, it's always there. Newcomers, don't be afraid: you'll meet it too, as you progress.&lt;/p&gt;

&lt;p&gt;At some point, I decided to build a resource that answers the question I keep getting: "Vertex animation - where do I start? Where can I learn it in the most time-efficient way?" The goal is a future-proof, go-to answer you can hand to anyone who asks.&lt;/p&gt;

&lt;p&gt;This article is my attempt at that: a practical, readable guide to vertex-stage motion in games. What the stage is (and isn't) good at, the building blocks every piece of motion is made from, and the production checks that decide whether your effect survives once it leaves the demo scene. By the end, you'll have a clear mental model of how to approach vertex motion in production.&lt;/p&gt;

&lt;p&gt;The samples and resources mentioned within the article were built and tested in Unity 6.4 with URP 17.4, using Blender 5.1 for the data-authoring and baking workflows, both as the primer and the playground. The concepts are engine-agnostic: spaces, data, time, masks, pivots, passes. Code samples are Unity URP.&lt;/p&gt;

&lt;p&gt;Let's dive in!&lt;/p&gt;




&lt;p&gt;Contents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When Vertex Animation Is the Right Tool
Coordinate Spaces: Who Owns This Motion?&lt;/li&gt;
&lt;li&gt;Data: Know Where Every Input Comes From&lt;/li&gt;
&lt;li&gt;Matrices in a Minute (and why production code often avoids manual calculations)&lt;/li&gt;
&lt;li&gt;Time: Wrap It, Shape It, Offset It&lt;/li&gt;
&lt;li&gt;Masks: Where Does The Motion Happen? &amp;nbsp; &amp;nbsp;&lt;/li&gt;
&lt;li&gt;-- Procedural masks: gradients and noise&lt;/li&gt;
&lt;li&gt;-- Authored masks: vertex colors, UVs, textures&lt;/li&gt;
&lt;li&gt;-- Runtime-driven masks&lt;/li&gt;
&lt;li&gt;Gameplay-Driven Motion: From Loop to Response&lt;/li&gt;
&lt;li&gt;Reusable Motion Patterns&lt;/li&gt;
&lt;li&gt;-- The sine-wave offset&lt;/li&gt;
&lt;li&gt;-- Layered waveforms: Gerstner-style&lt;/li&gt;
&lt;li&gt;-- Shading repair: normals after deformation&lt;/li&gt;
&lt;li&gt;-- When repetition starts to feel fake&lt;/li&gt;
&lt;li&gt;Pivot-Driven Motion: Believability Comes from Anchors&lt;/li&gt;
&lt;li&gt;Now Make It Shippable: The Six Production Checks&lt;/li&gt;
&lt;li&gt;-- Pass consistency&lt;/li&gt;
&lt;li&gt;-- Bounds&lt;/li&gt;
&lt;li&gt;-- Motion vectors and TAA&lt;/li&gt;
&lt;li&gt;-- Normals&lt;/li&gt;
&lt;li&gt;-- LODs&lt;/li&gt;
&lt;li&gt;-- Instancing&lt;/li&gt;
&lt;li&gt;Key Takeaways, Next Steps &amp;amp; Where to Go From Here&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Scope: what this article does not cover: vertex animation textures, Alembic and geometry caches, blend-shape setups, compute-driven deformation, mesh shaders, and ML deformers. Each of those deserves its own dedicated deep dive, and squeezing them in would make this piece wider and shallower. Today we go narrower, but deep enough to build, control, debug, and reason about vertex-stage motion.&lt;/p&gt;

&lt;p&gt;All the scenes, shaders, and scripts referenced in this article are available in the ShaderStory repository, feel free to follow along with the code.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Vertex Animation Is the Right Tool
&lt;/h2&gt;

&lt;p&gt;The rule of thumb that guides everything else:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Broad motion, low cost, high control. Not perfect motion.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Vertex animation is strongest when you need motion that is cheap, procedural, controllable, and scalable across many objects. &lt;/p&gt;

&lt;p&gt;The classic list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Foliage in the wind&lt;/li&gt;
&lt;li&gt;Flags and cloth-like strips&lt;/li&gt;
&lt;li&gt;Water-like surfaces, ripples, waves&lt;/li&gt;
&lt;li&gt;Breathing props, idle sway&lt;/li&gt;
&lt;li&gt;Impact reactions, shockwaves&lt;/li&gt;
&lt;li&gt;Wind response, proximity deformation, gameplay reactions&lt;/li&gt;
&lt;li&gt;Per-instance variation: thousands of assets sharing one shader, each moving slightly differently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key word is approximate. Vertex animation is excellent when the motion needs to feel believable. It is the wrong tool when it has to match a precise simulation, an authored performance, or physical truth. A character animation, a cloth that must collide, a liquid that must preserve volume: those need real systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the vertex stage is so useful
&lt;/h3&gt;

&lt;p&gt;The vertex stage happens early, before rasterization. You move vertices before the GPU even draws triangles, and if the illusion holds at that level, you get a lot of perceived motion for very little cost. Concretely, a well-built vertex stage setup can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;replace bones, baked clips, and CPU-side updates with one shared shader&lt;/li&gt;
&lt;li&gt;drive&amp;nbsp;many&amp;nbsp;assets from a single material&lt;/li&gt;
&lt;li&gt;source variation from masks, vertex colors, time offsets, wind, instance data, or gameplay parameters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is one important condition: the mesh has to be reasonable. Cheap deformation on a sensible mesh is cheap. The same math on a dense mesh, rendered in four passes, and instanced a thousand times, is suddenly not.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv0oegvdwvwooiubcxi6h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv0oegvdwvwooiubcxi6h.png" alt="What vertex stage dose well" width="800" height="318"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Keep this as the filter for every idea: does this need to be believable, or does it need to be true? Believable: the vertex stage. True: a real system. If the silhouette of the motion can sell the effect, prototype it in the vertex stage first.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fesgap0egu9x0jd8o9h3d.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fesgap0egu9x0jd8o9h3d.gif" alt="Vertex animation sample" width="713" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmdeoc0i2i1pnmm4j91n0.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmdeoc0i2i1pnmm4j91n0.gif" alt="Vertex animation sample" width="713" height="400"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Coordinate Spaces: Who Owns This Motion?
&lt;/h2&gt;

&lt;p&gt;When you start moving vertices, the first question is not "what's the formula?" The first question is: what is this motion attached to?&lt;/p&gt;

&lt;p&gt;Get it wrong and the effect looks almost right, which is worse than obviously wrong. It's the root of nearly every space-related vertex bug I've debugged, so it's worth internalizing properly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpup3rnrpumxp77syauow.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpup3rnrpumxp77syauow.png" alt="Coordinate spaces" width="800" height="291"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Object (model) space&lt;/strong&gt;: the motion belongs to the asset's local frame. If the object rotates, the motion rotates with it. This is what you want for local bends, pivots, authored masks, and behavior that should stay welded to the mesh.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;World space&lt;/strong&gt;: the motion belongs to the environment. The right choice for wind direction, waves, shockwaves, proximity fields, and gameplay volumes, because many objects need to agree on the same position or direction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;View (camera) space&lt;/strong&gt;: the motion belongs to the camera. Useful for billboards, impostors, camera-facing cards, view-dependent VFX.&amp;nbsp;Risky for anything that should feel physically grounded, because the motion changes relative to the viewer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clip / NDC space&lt;/strong&gt;: projection-space tricks, outlines, screen-relative effects. A specialist lane, not a starting point for mesh deformation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screen space&lt;/strong&gt;: UI, screen-space decals, outlines.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The practical pattern: you usually start from object-space position; if you need world behavior (wind, gameplay fields), you transform to world, apply the offset, then transform to clip for rendering. If you need camera-relative behavior, you do it in view space.&lt;/p&gt;

&lt;p&gt;Two rules that prevent most space-related bugs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The position and the offset must agree on space.&amp;nbsp;Don't add a world-space wind offset to an object-space position without transforming one into the other.&lt;/li&gt;
&lt;li&gt;Positions, directions, and normals are different things. Positions include translation. Directions don't. Normals need their own correct transform, especially when scale is involved.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fci2jm4tz2gkavc9s3o43.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fci2jm4tz2gkavc9s3o43.gif" alt="Wave deformation in object space. Rotate or move the asset: the wave is welded to the mesh and travels with it" width="713" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1x3hie27236evnm4cup4.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1x3hie27236evnm4cup4.gif" alt="The same wave in world space - the object moves through the field, the wave pattern stays fixed in the environment" width="713" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdhltgq43vaxwmm8qkztb.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdhltgq43vaxwmm8qkztb.gif" alt="The same wave in view space - the deformation is tied to the camera. Orbit the camera: the deformation follows the viewer, not the asset" width="713" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Watch those three together: the math is nearly identical in each, and the behavior changes completely depending on where the position and offset are evaluated. Same wave, three different owners.&lt;/p&gt;




&lt;h2&gt;
  
  
  Data: Know Where Every Input Comes From
&lt;/h2&gt;

&lt;p&gt;Don't treat shader data as magic. Once you know the space, the next question is: what data do I have to drive the motion, and where should custom data live?&lt;/p&gt;

&lt;h3&gt;
  
  
  The data hierarchy
&lt;/h3&gt;

&lt;p&gt;Everything available to a vertex shader falls into tiers:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkqviexgcahroo60xdav0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkqviexgcahroo60xdav0.png" alt="Vertex data stages" width="800" height="327"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Where custom motion data should live
&lt;/h3&gt;

&lt;p&gt;The rule: pick the smallest stable data path that satisfies the control.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Painted or baked per-vertex, rarely changes →&amp;nbsp;vertex colors / UVs / custom streams&amp;nbsp;(masks, stiffness, pivot weights, phase offsets).&lt;/li&gt;
&lt;li&gt;Artist-tuned for the whole material →&amp;nbsp;material properties&amp;nbsp;(amplitude, frequency, direction, noise scale).&lt;/li&gt;
&lt;li&gt;Different for each placed object →&amp;nbsp;per-object / instanced data&amp;nbsp;(random seed, phase offset, bend multiplier).&lt;/li&gt;
&lt;li&gt;Belongs to a shared system →&amp;nbsp;global shader properties&amp;nbsp;(wind, player position, impact points).&lt;/li&gt;
&lt;li&gt;Large, or changing every frame → buffers, with the understanding that this adds setup and an extra memory read.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And here is the production lesson that I cannot emphasize enough:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;A lot of vertex motion bugs are not math bugs. They are undocumented data-ownership bugs.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If UV2.x means pivot weight, vertex-color red means stiffness, and alpha means phase, write that convention&amp;nbsp;down. Document the valid range and how the shader decodes it. The shader can be brilliant and still rot in three months if only one person remembers what the channels mean.&lt;/p&gt;




&lt;h2&gt;
  
  
  Matrices in a Minute (and why production code often avoids manual calculations)
&lt;/h2&gt;

&lt;p&gt;You can't do rotation-based motion without meeting matrices. Here's the shortest version: think of a matrix as a recipe that builds a new position from an old one. Where values land is meaningful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Values on the&amp;nbsp;diagonal&amp;nbsp;→ each axis affects itself →&amp;nbsp;scale&lt;/li&gt;
&lt;li&gt;Values in the&amp;nbsp;translation column&amp;nbsp;→ adds an offset →&amp;nbsp;translation&lt;/li&gt;
&lt;li&gt;Values off-diagonal → axes start mixing → rotation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two things to remember from this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;W matters&lt;/strong&gt;. We use 4×4 matrices even though positions are XYZ because W lets translation live in the same multiply. Positions use w = 1 (translation applies); directions use w = 0 (they rotate/scale but never translate).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Order matters&lt;/strong&gt;. Scale, rotate, and translate are not interchangeable. In Unity, the local matrix is composed T  R  S, but a point experiences Scale → Rotation → Translation because the rightmost operation applies first. The same values produce a different result when the order changes. That sentence will save you at least once per project.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Theory vs. production
&lt;/h3&gt;

&lt;p&gt;For learning, writing out explicit matrices is great: the structure becomes visible. But in production vertex code I usually prefer exposed variables &amp;amp; functions:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;// scale in local space, rotate around the intended pivot,&lt;br&gt;
// translate the final result, no 3 matrices materialization&amp;nbsp;&lt;br&gt;
float3 local = positionOS - pivotOS;&lt;br&gt;
local &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;= RotateAroundAxis(local, axisOS, angle);&lt;br&gt;
positionOS &amp;nbsp; = pivotOS + local;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Why? Explicit matrices can cost more ALU per vertex, increase register pressure, make row-major/column-major mistakes easier, and block compiler optimizations. So the lesson isn't "never use raw matrices". It's: use matrices when they improve clarity or are structurally required; don't build them out of habit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Time: Wrap It, Shape It, Offset It
&lt;/h2&gt;

&lt;p&gt;Time is one of the main ingredients of procedural motion, but raw time is almost never the final value you want.&lt;/p&gt;

&lt;p&gt;Raw _Time.y grows forever. That's fine for sequencing and system-level logic. But for motion, you usually want a local, bounded value.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrap It
&lt;/h3&gt;

&lt;p&gt;For loops, pulses, and repeating motion, convert time into a bounded 0..1 phase:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;// Looping motion: stable repeating 0..1 phase&lt;br&gt;
float phase01 = fracTime.y * Speed);&lt;br&gt;
// Duration-based motion: local time, easier to restart&lt;br&gt;
float localTime = fmodTime.y, Duration);&lt;br&gt;
float local01 = localTime / _Duration;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Why wrap? A bounded local time is easier to reason about, easier to restart, and easier to combine with masks and easing. It also stops you feeding ever-growing values into sin() or noise, which eventually invites precision problems and visual drift.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Use continuous time for systems. Use wrapped time for motion.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl9ouqqheirs6ol7722ch.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl9ouqqheirs6ol7722ch.gif" alt="Wrapped vs. continuous time, the loop restarts cleanly each cycle" width="713" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Shape It
&lt;/h3&gt;

&lt;p&gt;Once you have a normalized 0..1 value, you don't have to use it raw. Two levers shape how it moves.&lt;/p&gt;

&lt;p&gt;Easing changes the curve. Linear time changes at a constant rate, which feels mechanical. Easing reshapes the acceleration profile:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ease-in, starts slow, accelerates. Reads as&amp;nbsp;anticipation, charge-up.&lt;/li&gt;
&lt;li&gt;Ease-out, starts fast, settles. Reads as&amp;nbsp;recovery, landing.&lt;/li&gt;
&lt;li&gt;Ease-in-out, soft, controlled. Often the best default for organic motion.&lt;/li&gt;
&lt;li&gt;Bounce / back / elastic, overshoot and secondary response. Great for stylized impacts and recoil; easy to overuse.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Ping-pong&lt;/strong&gt; removes the hard restart. A wrapped phase runs 0 to 1, then snaps back to 0. Ping-pong runs 0 to 1 to 0 and reverses, which turns a jarring loop into a continuous in-out motion. It's the natural fit for breathing, sway, and anything that should ease back instead of resetting:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;float t01 &amp;nbsp; &amp;nbsp;= fracTime.y * Speed); &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;// wrapped phase, 0 -&amp;gt; 1&lt;br&gt;
float pp &amp;nbsp; &amp;nbsp; = abs(1.0 - t01 * 2.0); &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;// optional ping-pong, 0 -&amp;gt; 1 -&amp;gt; 0&lt;br&gt;
float shaped = EaseInOutSine(pp); &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // or your curve of choice&lt;br&gt;
float3 offsetOS = directionOS  shaped  _Amplitude;&lt;br&gt;
positionOS += offsetOS;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnelsq26jua8jc9n3r4nh.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnelsq26jua8jc9n3r4nh.gif" alt="Easing mode switch. Result is the same amplitude, completely different feel" width="713" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the shift that separates animating values from designing the motion curve: the start and end can be identical; the acceleration profile is what the eye actually reads. If the effect will live in your project long enough for someone else to maintain it, put the easing and ping-pong helpers in a shared include file, not copied into every shader.&lt;/p&gt;

&lt;h3&gt;
  
  
  Offset It
&lt;/h3&gt;

&lt;p&gt;If every object uses exactly the same time value, everything moves in perfect sync, which reads as immediately artificial. A phase offset (a stable random value added to the phase) staggers motion per object, per instance, or per vertex:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;float phase = fracTime.y * Speed + _PhaseOffset);&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One line. One of the highest-value lines in the whole article.&lt;/p&gt;




&lt;h2&gt;
  
  
  Masks: Where Does the Motion Happen?
&lt;/h2&gt;

&lt;p&gt;A mask answers the question that raw time and amplitude never answer: where does the motion happen, and how strongly?&lt;/p&gt;

&lt;h3&gt;
  
  
  Procedural masks: gradient for structure, noise for breakup
&lt;/h3&gt;

&lt;p&gt;The simplest procedural mask is a gradient, for example, object-space height, so the bottom of a plant stays planted while the top moves:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;float gradientMask = smoothstepGradientMin, GradientMax, positionOS.y);&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A gradient alone is structured but uniform. Noise breaks the uniformity so not every vertex moves in lockstep:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;float2 noiseUV = positionWS.xz * NoiseScale + float2(frac(Time.y * _NoiseSpeed), 0.0);&lt;br&gt;
float noiseMask = SimpleNoise(noiseUV);&lt;br&gt;
float mask = gradientMask * noiseMask; &amp;nbsp; // structure × breakup&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The production pairing: the gradient gives structure, the noise adds variation, and smoothstep gives you art direction, because a hard procedural falloff reads as artificial while a smooth one reads as intent.&lt;/p&gt;

&lt;p&gt;And remember the space rule from the coordinate-spaces section: procedural masks are not space-neutral. Noise sampled in object space sticks to the asset; noise sampled in world space stays fixed in the world while the asset moves through the pattern. Same function, different behavior.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ehibbk2r42nvafkm8ac.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ehibbk2r42nvafkm8ac.gif" alt="Procedural gradient × noise mask shaping the deformation" width="713" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Authored masks: direct artistic control
&lt;/h3&gt;

&lt;p&gt;Procedural masks are fast to generate, but they're still procedural guesses. When an artist or a tool needs direct control over where motion happens, you reach for authored data:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fywqp66ic6ls2gf8clepl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fywqp66ic6ls2gf8clepl.png" alt="Masks sources" width="799" height="263"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc6rlzqkk2jjptmoik0fy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc6rlzqkk2jjptmoik0fy.png" alt="Masks sources" width="800" height="714"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The last row is the most powerful and the most fragile: packed channels only work if everyone agrees what each channel means. Write that convention down.&lt;/p&gt;

&lt;p&gt;And the single most repeated rule in this article and production:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Debug the mask before you debug the motion. If the mask is wrong, the deformation will be wrong no matter how good the motion function is. Always build a mask-debug view first, render the mask as the final color and look at it before you apply any displacement.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9y479grpnwrdfex1yclk.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9y479grpnwrdfex1yclk.gif" alt="Authored masks via vertex color and packed channels" width="713" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Runtime-driven masks: motion becomes interaction
&lt;/h3&gt;

&lt;p&gt;Procedural masks come from math. Authored masks come from artists. Runtime-driven masks come from live game state, and this is where vertex motion stops being decoration and starts being behavior.&lt;/p&gt;

&lt;p&gt;The workhorse example is the sphere mask: the game provides a world-space center, radius, and falloff, and every vertex checks its distance to that center and computes its own local response. The same setup drives a player-proximity lean, a wind gust, a shockwave, or a water ripple; only the displacement direction and shaping change.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;float dist = length(positionWS - _MaskCenterWS);&lt;br&gt;
float mask = 1.0 - smoothstepMaskRadius * Softness, _MaskRadius, dist);&lt;br&gt;
positionWS += DisplaceDirWS * Amplitude * mask;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Because it's a distance test in world space, the space rule bites here: if the mask center is world-space, the position used for the test must be world-space as well. Mismatch the spaces and the mask silently fails.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5ry9jv8m2o1x65hmvpyq.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5ry9jv8m2o1x65hmvpyq.gif" alt="A runtime sphere mask driving deformation. The field moves, the geometry follows" width="713" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One or two global masks are simple. If you need many overlapping masks, move to arrays, buffers, or a gameplay-side aggregation system, not a dozen hand-authored globals.&lt;/p&gt;




&lt;h2&gt;
  
  
  Gameplay-Driven Motion: From Loop to Response
&lt;/h2&gt;

&lt;p&gt;This is my favorite part of the whole subject, because it's built by adding one ingredient at a time, and each addition is a distinct, visible level of believability. Follow the four steps with the GIFs; you'll see the same shader go from "generic animation" to "alive."&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Time only
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;float3 positionWS = TransformObjectToWorld(positionOS);&lt;br&gt;
float wave = sinTime.y * Speed) * _Amplitude;&lt;br&gt;
positionWS += float3(0.0, wave, 0.0);&lt;br&gt;
positionCS = TransformWorldToHClip(positionWS);&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The motion works, but every vertex receives the same temporal input, so the whole asset pulses uniformly. This is motion, not yet behavior. It's still a valuable checkpoint: it proves the deformation path is live before you complicate anything.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fossesv9x2b6met2yh3qb.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fossesv9x2b6met2yh3qb.gif" alt="Uniform time-driven motion. It moves, but the whole surface moves together" width="713" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Time + Axis: position gives the motion structure
&lt;/h3&gt;

&lt;p&gt;Project world position onto a direction to spread the phase across the mesh. Now the wave travels instead of pulsing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;float wave = sin(dot(positionWS, normalizeWaveAxisWS)) * Frequency + Time.y * Speed) * _Amplitude;&lt;br&gt;
positionWS += normalize(_MoveAxisWS) * wave;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Note the two jobs: the axis sets where the pattern travels; the move direction sets which way vertices displace. Different vectors, different effects. Time keeps the wave moving; position is what gives it structure.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2ah8x4y50mlwctx4sl2b.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2ah8x4y50mlwctx4sl2b.gif" alt="A traveling wave. Phase now varies across the surface" width="713" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: + Mask: the mask makes it feel attached
&lt;/h3&gt;

&lt;p&gt;Now tell the shader where motion is allowed. Here the vertex color acts as influence: near zero at the attached end, strong at the free end.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;float mask = IN.color.r; &amp;nbsp; // or a UV gradient, the motion function stays reusable&lt;br&gt;
positionWS += normalize(_MoveAxisWS)  wave  mask;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is a strong production pattern: the motion function stays generic and reusable; the asset decides where that motion is allowed. Same shader, different meshes, different character.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgrbv1ae5sot78dkid374.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgrbv1ae5sot78dkid374.gif" alt="Mask on: the base stays planted, the tip moves, same wave function" width="713" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: + Gameplay parameters: reactive, not decorative
&lt;/h3&gt;

&lt;p&gt;The final stage modulates everything with live gameplay data. The controller sends smoothed world velocity, facing direction, and signed turn rate into the shader:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;speed scales the response, running makes it stream, standing still leaves a subtle idle flutter&lt;/li&gt;
&lt;li&gt;turning adds sideways drag&lt;/li&gt;
&lt;li&gt;a small idle flutter remains at zero speed, because a fully static idle reads as dead, while an instant jump from still to full motion reads as fake&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The core gate, reusing the wave and mask from the earlier steps:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;float velocity01 = smoothstep(0.0, 1.0, saturateCharacterSpeed / MaxCharacterSpeed));&lt;br&gt;
float response &amp;nbsp; = _IdleAmplitude + velocity01; &amp;nbsp; // small idle flutter even at rest&lt;br&gt;
positionWS += normalize(_MoveAxisWS)  wave  mask * response;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One production rule that step 4 is really worth memorizing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Filter gameplay data before it drives deformation. Smooth and clamp on the CPU (or in a controlled shader pass); never let raw, jittery inputs reach the vertex math directly. "Fake it till you make it" is a legitimate production strategy, as long as the fake is smooth.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhidc7n0eca69pqypz5ii.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhidc7n0eca69pqypz5ii.gif" alt="The complete system: idle flutter, speed-scaled response, turn drag, shader response" width="713" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That GIF is the thesis of this whole article in eight seconds.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reusable Motion Patterns
&lt;/h2&gt;

&lt;p&gt;There are two patterns that do most of the work, and two follow-ups decide whether they actually sell.&lt;/p&gt;

&lt;h3&gt;
  
  
  The sine-wave offset
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;float3 offsetDirOS  = normalize(_OffsetDirectionOS);&lt;br&gt;
float spatialPhase &amp;nbsp;= positionOS.x * _Frequency;&lt;br&gt;
float temporalPhase = frac(Time.y * Speed) / TWO_PI) * TWO_PI;&lt;br&gt;
float wave = sin(spatialPhase + temporalPhase + PhaseOffset) * Amplitude;&lt;br&gt;
positionOS += offsetDirOS  wave  mask;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The idea is two independent phases: &lt;strong&gt;position&lt;/strong&gt; spreads the wave across the surface, time advances it. &lt;strong&gt;Amplitude&lt;/strong&gt; is how far it moves; &lt;strong&gt;frequency&lt;/strong&gt; is how tightly packed; &lt;strong&gt;direction&lt;/strong&gt; is where it travels; &lt;strong&gt;_PhaseOffset&lt;/strong&gt; is your per-object anti-sync.&amp;nbsp;&lt;/p&gt;

&lt;p&gt;It is the right first pattern for flags, cloth strips, grass, water-like surfaces, idle sway, anything that benefits from rhythmic deformation rather than authored animation.&lt;/p&gt;

&lt;p&gt;And be honest about what it is: &lt;strong&gt;it is not a simulation&lt;/strong&gt;. You are not solving cloth or water. You are building a controllable, repeatable pattern that sells the effect cheaply.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3tp5mmncz39a2mss77x1.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3tp5mmncz39a2mss77x1.gif" alt="Sine motion on a surface" width="713" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Layered waveforms: Gerstner-style
&lt;/h3&gt;

&lt;p&gt;A single sine is readable, and obviously repetitive within a few seconds. The fix is layering: several waves, each with its own direction, amplitude, frequency, speed, and phase:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;float3 posWS = baseWS;&lt;br&gt;
posWS += GerstnerOffset(baseWS, DirA, AmpA, FreqA, SpeedA, PhaseA, SteepA);&lt;br&gt;
posWS += GerstnerOffset(baseWS, DirB, AmpB, FreqB, SpeedB, PhaseB, SteepB);&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The Gerstner step that separates this from plain sines: it displaces horizontally along the wave direction, not just vertically, which produces the sharp, leaning crest that reads as "water" instead of "bumpy plane."&lt;/p&gt;

&lt;p&gt;The cost warning scales with the layer count: every wave adds math per vertex, repeated across every pass, multiplied by mesh density. Layer enough waves to sell the surface; keep the count deliberately controlled.&amp;nbsp;&lt;/p&gt;

&lt;p&gt;Remember that displaced water is not only a vertex problem, normals, reflections, and foam must support the motion (we will take a closer look in the next section).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F42bk6brfs7ga609zyizy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F42bk6brfs7ga609zyizy.gif" alt="One wave → layered Gerstner: repetition breaks up, crests sharpen" width="713" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Shading repair: normals after deformation
&lt;/h3&gt;

&lt;p&gt;Here's a classic trap: the geometry moves perfectly, but the lighting behaves as if the surface never changed. Highlights slide the wrong way, reflections feel detached, and the deformation sells itself as fake in one glance, because the normals still describe the original flat surface.&lt;/p&gt;

&lt;p&gt;The practical fix for wave-style surfaces is finite-difference reconstruction:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F68pb3kfzu41j19nfy708.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F68pb3kfzu41j19nfy708.png" alt="Normals repair methods" width="800" height="257"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For wave-style surfaces, finite-difference reconstruction is the practical default:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;evaluate the displaced surface at the current point,&lt;/li&gt;
&lt;li&gt;evaluate it again at two small nearby offsets,&lt;/li&gt;
&lt;li&gt;build local tangent directions from those displaced neighbors,&lt;/li&gt;
&lt;li&gt;cross product → the reconstructed geometric normal.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;float3 D(float3 p) { return deform(p); } // the same deformation the pass uses&lt;br&gt;
float3 p = D(baseWS);&lt;br&gt;
float3 t = D(baseWS + e1) - p; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;// e1 / e2: small, non-parallel neighbor offsets&lt;br&gt;
float3 b = D(baseWS + e2) - p;&lt;br&gt;
float3 n = normalize(cross(t, b)); // tangent x tangent = new normal&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This follows the actual displaced shape, which matters more for Gerstner motion, which moves both vertically and horizontally. For mostly-vertical motion, a plain height-field gradient is cheaper and fine.&lt;/p&gt;

&lt;p&gt;And keep one artistic knob: a repair-strength blend from original normal to fully reconstructed. In production, partial repair plus a normal map for micro-detail is usually the better option than chasing mathematical perfection: large motion from geometry, small detail from shading.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F95z6c9g4pdts03c66e81.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F95z6c9g4pdts03c66e81.gif" alt="Original vs. reconstructed normals, lighting finally follows the waves" width="713" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  When repetition starts to feel fake
&lt;/h3&gt;

&lt;p&gt;Procedural motion usually fails not because the math is wrong, but because the pattern becomes visible. One wave, same speed, same amplitude, same phase on every object → the whole field moves like a machine. And the first instinct, more amplitude, usually makes the fakeness louder.&lt;/p&gt;

&lt;p&gt;The real toolkit is controlled irregularity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;phase offsets across vertices, objects, or instances&lt;/li&gt;
&lt;li&gt;slightly different amplitudes, speeds, frequencies&lt;/li&gt;
&lt;li&gt;layered broad + small secondary motion (a hierarchy, not one uniform wave)&lt;/li&gt;
&lt;li&gt;masks and noise to break uniform patterns&lt;/li&gt;
&lt;li&gt;simplified or reduced motion by distance, importance, or LOD&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Enough variation to hide the repetition; not so much that the motion loses its art direction.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4fvbjwqf6makok0q2l5z.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4fvbjwqf6makok0q2l5z.gif" alt="Repetition made visible" width="713" height="400"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Pivot-Driven Motion: Believability Comes from Anchors
&lt;/h2&gt;

&lt;p&gt;Simple offset motion is cheap and readable, but it often feels like the mesh is translating through space. Real objects don't do that. A leaf rotates from its stem. A branch rotates from its base. A door rotates around its hinge. The motion is believable because it is anchored to a meaningful point.&lt;/p&gt;

&lt;p&gt;It's worth naming what goes wrong, because the failure is subtle. A plain offset floats the whole mesh in place. A rotation around the object origin reads as the mesh spinning on itself. Only a rotation around a local pivot reads as the part actually hinging where it should. Same angle, three completely different results.&lt;/p&gt;

&lt;p&gt;That anchor is the pivot, and it's the single biggest leap in perceived quality for foliage and prop motion.&lt;/p&gt;

&lt;p&gt;The core pattern:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;float3 local = positionOS - pivotOS; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;// 1. move into pivot-local space&lt;br&gt;
float3 axis &amp;nbsp;= normalize(axisOS); &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // 2. rotation axis, same space&lt;br&gt;
local &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;= RotateAroundAxis(local, axis, angle);&lt;br&gt;
positionOS &amp;nbsp; = pivotOS + local; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // 3. move back to object space&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Subtract the pivot, rotate, add it back. One production detail: position, pivot, and axis must agree on space. Object-space position with a world-space pivot is a silent, confusing failure.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs3w8jo0duvi75jz6bh0r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs3w8jo0duvi75jz6bh0r.png" alt="Pivot calculation" width="800" height="313"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Where does pivot data come from?
&lt;/h3&gt;

&lt;p&gt;One object: define the pivot by hand (a material property is fine). Many leaves, many parts: you need a data strategy, and this is a great Blender workflow to know:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Model each leaf as its own object, with the&amp;nbsp;object origin placed at the stem.&lt;/li&gt;
&lt;li&gt;Run a small bake script: write each object's origin into its vertices'&amp;nbsp;vertex color, RGB = encoded pivot position (0..1 within bounds), A = random phase.&lt;/li&gt;
&lt;li&gt;Merge everything into one mesh. Every vertex still&amp;nbsp;knows&amp;nbsp;which pivot it belongs to.&lt;/li&gt;
&lt;li&gt;Decode in the shader:&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;float3 pivotOS = lerpPivotBoundsMin, PivotBoundsMax, IN.color.rgb);&lt;br&gt;
float &amp;nbsp;phase &amp;nbsp; = IN.color.a;&lt;br&gt;
float angle = sinTime.y * Speed + phase  TWO_PI)  _AngleAmplitude;&lt;br&gt;
float3 local = positionOS - pivotOS;&lt;br&gt;
local &amp;nbsp;= RotateAroundAxis(local, axisOS, angle);&lt;br&gt;
positionOS = pivotOS + local;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The result: one merged mesh, every leaf rotating from its own stem, with the alpha phase de-synchronizing the field. In the viewport the vertex color looks like a random gradient, but it's data. What matters isn't this exact channel layout; what matters is that the source, the packing, and the decode path are all explicitly defined and documented, as the data section says.&lt;/p&gt;

&lt;p&gt;And, exactly as with the masks: debug the pivot before you trust the rotation. Render the pivot, or the distance to it, as color and look at it. If the pivot data is wrong, the motion will be wrong no matter how good the rotation function is.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqc6ovnus49atp7f0o4re.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqc6ovnus49atp7f0o4re.gif" alt="Baked pivot data: every leaf rotates from its own stem, desynced by the packed phase channel" width="713" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Baking and unpacking pivot data
&lt;/h3&gt;

&lt;p&gt;The classic foliage workflow bakes the pivot into the mesh, then unpacks it in the shader:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Model each leaf as its own object, with the&amp;nbsp;object origin placed at the stem.&lt;/li&gt;
&lt;li&gt;Bake each object's origin into its vertices'&amp;nbsp;vertex color.&lt;/li&gt;
&lt;li&gt;Merge everything into one mesh; every vertex still&amp;nbsp;knows&amp;nbsp;which pivot it belongs to.&lt;/li&gt;
&lt;li&gt;Unpack in the shader:&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;// A common per-channel packing convention&lt;br&gt;
float3 pivotOS &amp;nbsp; = lerpPivotBoundsMin, PivotBoundsMax, IN.color.rgb); // pivot, 0..1 in bounds&lt;br&gt;
float &amp;nbsp;influence = IN.color.a; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;// stiffness / influence&lt;br&gt;
float3 axisOS &amp;nbsp; &amp;nbsp;= DecodeAxisOS(IN.uv2.xy); &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;// rotation axis&lt;br&gt;
float &amp;nbsp;phase &amp;nbsp; &amp;nbsp; = IN.uv2.z * TWO_PI; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;// per-leaf phase&lt;br&gt;
float &amp;nbsp;variation = IN.uv2.w; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;// per-leaf variationfloat angle = sinTime.y * Speed + phase)  _AngleAmplitude  influence;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feokcprecmg1lxx9micpu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feokcprecmg1lxx9micpu.png" alt="Baked pivots preview" width="736" height="652"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The axis is the one flexible channel: it can be a constant, derived from the normal, or stored in another stream. But pivot, axis, and influence all have to live somewhere, and the source, the packing, and the decode path must all be explicitly defined and documented, as the data section says. In the viewport the packed vertex color looks like a random gradient, but it's data.&lt;/p&gt;

&lt;p&gt;And, exactly as with the masks: debug the pivot before you trust the rotation. Render the pivot, or the distance to it, as color and look at it. If the pivot data is wrong, the motion will be wrong no matter how good the rotation function is.&lt;/p&gt;




&lt;h2&gt;
  
  
  Now Make It Shippable: The Six Production Checks
&lt;/h2&gt;

&lt;p&gt;Everything above makes a demo. This section is what separates a demo from a shipped system: the things that tend to break once the effect leaves the hero scene.&lt;/p&gt;

&lt;p&gt;The unifying idea:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Don't ship a pretty vertex function. Ship the motion contract.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;"Looks fine" in the main pass is not enough; it's only one part of the contract.&lt;/p&gt;

&lt;p&gt;The six checks, at a glance:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8tahhi591q4hug791cck.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8tahhi591q4hug791cck.png" alt="Six production checks" width="799" height="233"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Pass consistency: same deformation, same inputs, same space, same time
&lt;/h3&gt;

&lt;p&gt;The most common failure: the forward pass bends the vertices, but the shadow, depth, and motion-vector passes don't reproduce that deformation. The asset looks correct, but the shadow doesn't move, while depth-dependent systems (fog, decals, SSAO, soft particles) read the wrong shape, and the whole thing is subtly inconsistent, which is exactly what the eye catches before the brain can name.&lt;/p&gt;

&lt;p&gt;The correct fix is not to re-implement the effect per pass. Centralize the deformation in one shared function / include, call it from every pass, and make sure each pass uses the same inputs: same time source, same wind parameters, same masks, same pivots, same per-instance variation, same space conventions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;// One function, many passes:&lt;br&gt;
float3 ApplyWind(float3 posOS, float time, float instanceSeed);&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Copy-paste per pass will drift. Shared logic with shared inputs doesn't. The contract, pass by pass:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq7l1w11x5haqhtxnusba.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq7l1w11x5haqhtxnusba.png" alt="Passes" width="799" height="388"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg4fys28y4scemw3jy6s9.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg4fys28y4scemw3jy6s9.gif" alt="Main pass deformed, shadow pass is not" width="713" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Bounds: design for worst-case displacement
&lt;/h3&gt;

&lt;p&gt;The renderer decides visibility from bounds computed at rest, on the CPU, but your deformation happens later, on the GPU. If displaced vertices escape those bounds, the object pops, clips, or disappears at the edge of the screen. The same mismatch can hurt shadows too.&lt;/p&gt;

&lt;p&gt;The fix is not to make bounds arbitrarily huge; oversized bounds wreck culling efficiency. Design for the maximum possible displacement (you should have defined this on day one), and expand bounds just enough to cover that worst case.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;float3 safeExtents = originalExtents + float3(maxRadius, maxRadius, maxRadius);&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcm3yk2xxfv9z11jvc52u.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcm3yk2xxfv9z11jvc52u.gif" alt="Displaced vertices escaping the original bounds" width="713" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Motion vectors and TAA: temporal systems need correct motion
&lt;/h3&gt;

&lt;p&gt;TAA, motion blur, and friends don't only care about the current frame; they need to know where the surface was in the previous frame so history can be reprojected. If the motion-vector pass doesn't evaluate the same deformation, the renderer behaves as if the surface were static, and you get ghosting, smearing, and shimmering. Foliage makes this especially visible: thin, alpha-tested, high-frequency, constantly moving.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;float3 currentOS &amp;nbsp;= ApplyWind(restOS, currentTime, &amp;nbsp; instanceSeed);&lt;br&gt;
float3 previousOS = ApplyWind(restOS, previousTime, &amp;nbsp;instanceSeed);&lt;br&gt;
// motion vectors derive from the difference of the two clip-space results&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two details that matter: current and previous must come from the same function, and the previous-frame result must be deterministic; if it's driven by unstable randomness or frame-dependent noise, the vectors stay broken even when the code looks correct.&lt;/p&gt;

&lt;p&gt;And a legitimate production escape valve: for dense distant forests, deliberately simplify or fade motion layers, and accept some temporal artifacts as a conscious performance tradeoff, never as an accidental bug.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwvq6nxwgyk40a50lzaao.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwvq6nxwgyk40a50lzaao.gif" alt="TAA ghosting on &amp;quot;correct&amp;quot; geometry, the shape is right, the history is wrong" width="713" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Normals: match the visual goal, not the math by default
&lt;/h3&gt;

&lt;p&gt;Subtle leaf flutter can be perfectly fine on imported, art-directed normals, often bent, averaged, or biased for a broader canopy response. Physically "correct" reconstruction can make a stylized asset look worse. But strong deformation (broad bends, large pivots, waves) detaches highlights and reflections from the shape, and that you must repair.&lt;/p&gt;

&lt;p&gt;So this is a per-asset decision, keep, bend, bias, blend, or reconstruct, driven by the lighting intent of the asset, not by default. The reconstruction methods themselves live in the patterns section; here the question is a shipping decision, not a math exercise.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjlc81y9h8cwvaij2gocp.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjlc81y9h8cwvaij2gocp.gif" alt="Normal strategies compared on the same deformed mesh" width="713" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. LODs: LOD the motion, not only the mesh
&lt;/h3&gt;

&lt;p&gt;LODs are often different meshes, not cheaper copies of the same mesh: different topology, vertex counts, vertex colors, UV payloads, pivots, bounds. If your motion depends on vertex colors, packed UVs, stored pivots, or phase offsets, and that data disappears or shifts in a lower LOD, the motion can pop, rotate around the wrong point, or change amplitude at the transition.&lt;/p&gt;

&lt;p&gt;And consistency is not identical motion. A coarse distant LOD should run simpler, weaker motion, spending performance where the player can actually read it. The production decision is intentional per LOD: keep, simplify, or disable layers, with stable cross-fades. If two LODs visibly disagree during dithered overlap, the asset shimmers and double-images.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fclpbq391pxrhwwojgu84.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fclpbq391pxrhwwojgu84.gif" alt="LOD transition with inconsistent motion data" width="713" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Instancing: variation per instance; logic stays shared
&lt;/h3&gt;

&lt;p&gt;One plant in the wind is believable. A hundred identical plants pulsing in unison is a machine; the problem only becomes visible at population scale, which is the trap: the single-asset test always passes.&lt;/p&gt;

&lt;p&gt;The fix is per-instance variation, phase offset, bend multiplier, stiffness, gust response, tint, sourced from a supported per-instance channel: instanced properties, per-object data, indirect buffers, or a stable object hash. The cardinal sin to avoid: solving synchronization by creating a unique material per plant. That destroys batching, multiplies draw calls and material state, and trades one artificial-looking problem for an expensive one.&lt;/p&gt;

&lt;p&gt;And variation must be consistent across passes: if the main pass varies per instance but the shadow pass ignores the seed, you're back to the first shipping check (pass consistency).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpg8op62gt97zxof2ryob.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpg8op62gt97zxof2ryob.gif" alt="Per-instance phase offsets: the field moves as a field, not as a machine" width="713" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways, Next Steps &amp;amp; Where to Go From Here
&lt;/h2&gt;

&lt;p&gt;If you remember nothing else, make it these:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Vertex motion is for believable approximation, not precision.&amp;nbsp;Broad motion, low cost, high control.&lt;/li&gt;
&lt;li&gt;Always define the space first.&amp;nbsp;Who owns this motion, the asset, the world, or the camera? Position and offset must agree on space.&lt;/li&gt;
&lt;li&gt;Every input has an owner.&amp;nbsp;Pick the smallest stable data path, mesh, material, instance, global, or buffer, and&amp;nbsp;document the convention.&lt;/li&gt;
&lt;li&gt;Wrap time for motion, shape it with easing, offset it per instance.&amp;nbsp;You're designing a curve, not moving a value.&lt;/li&gt;
&lt;li&gt;Debug the mask before you debug the motion.&lt;/li&gt;
&lt;li&gt;Cost = vertices × passes × complexity.&amp;nbsp;Measure at scene scale, with real density, real instances, real passes.&lt;/li&gt;
&lt;li&gt;Ship the motion contract: same deformation, same inputs, same space, same time, in every pass. Bounds, shadows, depth, motion vectors, normals, LODs, and instancing are all part of the same system.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  From Idea to Shipped Asset: the Practical Order
&lt;/h3&gt;

&lt;p&gt;When you start a new vertex-motion effect, in this order:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwkennl2biymwj8fuchxr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwkennl2biymwj8fuchxr.png" alt="From idea to shipped asset" width="800" height="132"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start with the simplest motion that proves the idea, time + one direction.&lt;/li&gt;
&lt;li&gt;Define the space&amp;nbsp;(object / world / pivot / custom) and the&amp;nbsp;maximum displacement, early, because it drives bounds, shadows, LODs, and performance.&lt;/li&gt;
&lt;li&gt;Decide where the driving data lives, properties, vertex colors, UVs, textures, buffers, or gameplay, and write the convention down.&lt;/li&gt;
&lt;li&gt;Build the deformation as shared logic, one function, every pass.&lt;/li&gt;
&lt;li&gt;Add debug views before adding complexity, mask view, normal view, motion-vector view, batch-cost view.&lt;/li&gt;
&lt;li&gt;Validate in three passes: one asset → all LODs → scene scale.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Where to Go Deeper
&lt;/h3&gt;

&lt;p&gt;The mental checklist that outlasts every specific technique: who consumes this moved position, and what data do they need? If you keep that question alive, the domain opens up naturally from here. Four directions worth going deeper in:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbn5mo67o3zhv8qe6m4ds.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbn5mo67o3zhv8qe6m4ds.png" alt="What's next" width="799" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In practice, that means xCode for shader cost, Unity Frame Debugger and RenderDoc for the render pipeline, motion-vector debug views, and batch / draw-call inspection at field scale.&lt;/p&gt;




&lt;p&gt;There's a line I keep coming back to as a technical artist, one that captures the whole discipline, the point where craft effort and artistic intent meet:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"When love and skill work together, expect a masterpiece." - John Ruskin&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Don't try to catch the rainbow. Try to be better than you were yesterday.&lt;/p&gt;

&lt;p&gt;If this saved you an afternoon of wondering why your shadow wasn't moving, share it with a colleague who's about to find out the hard way.&lt;/p&gt;

&lt;p&gt;Thank you for the attention and till next time!&lt;/p&gt;




&lt;h2&gt;
  
  
  Code, Resources &amp;amp; Support
&lt;/h2&gt;

&lt;p&gt;If this was useful, here's where to find more:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code: All scenes, shaders and scripts from this article are in the &lt;a href="https://github.com/DeGGeD/ShaderStory" rel="noopener noreferrer"&gt;ShaderStory repository on GitHub&lt;/a&gt;. Stars and issues are welcome.&lt;/li&gt;
&lt;li&gt;More articles: &lt;a href="https://www.artstation.com/degged/blog" rel="noopener noreferrer"&gt;ArtStation blog&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Support the work: &lt;a href="https://www.patreon.com/cw/decompiled_art" rel="noopener noreferrer"&gt;Patreon&lt;/a&gt; for additional material and to help keep this content coming.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Questions or your own production war stories with vertex animation? Leave a comment below.&lt;/p&gt;

&lt;p&gt;—-&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.artstation.com/blogs/degged/PQbOA/vertex-animation-in-games-how-to-make-it-art-directable-performant-and-shippable" rel="noopener noreferrer"&gt;ArtStation&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>unity3d</category>
      <category>shaders</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
