CGWiki DLC
Various Houdini tips and tricks I use a bunch. Hope someone finds this helpful!
Rabbit Hole
These articles grew too long to fit here. They\’re the most interesting in my opinion, so be sure to check them out!
- Vertex Block Descent in Houdini
- 3D Signed Distance Functions
- Lerp and Fit
- Waveforms
- Easings
- Normalized Device Coordinates
- 4D Geometry
- Time Smoothing (WIP, interactive!)
- Vexember 2023 (WIP)
- Sound Effects (WIP)
HDA: Fast Straight Skeleton 3D
They added a new Laplacian node to Houdini 20.5. You can do cool frequency-based tricks with it.
One example is calculating a straight skeleton. You can do this with a Laplacian node followed by a Linear Solver set to \”SymEigsShiftSolver\”. This gives you a bunch of Laplacian eigenvectors, which are like frequencies making up a mesh.
The second lowest frequency (or eigenvector) is called the Fiedler vector. It follows the general flow of the geometry, which is great for straight skeletons. Also it\’s orders of magnitude faster than Labs Straight Skeleton 3D!
Thanks to White Dog for letting me share this and suggesting improvements! It\’s based on his Curve Skeleton example.
| Download the HDA! | Download the HIP file! |
|---|
HDA: Rigid Piece Preroll
Most preroll nodes use position difference only. This is fine for translation but not rotation.
Extract Transform can be used to estimate the rotation difference too. This gives preroll for both rotation and translation.
This HDA works on single and multiple pieces, either packed or unpacked. For packed pieces, it uses their intrinsic transforms.
| Download the HDA! | Download the HIP file! |
|---|
Simple spring solver
Need to overshoot an animation or smooth it over time to reduce bumps? Introducing the simple spring solver!
Recursive Version
I stole this from an article on 2D wave simulation by Michael Hoffman. The idea is to set a target position and set the acceleration towards the target. This causes a natural overshoot when the object flies past the target, since the velocity takes time to flip. Next you apply damping to stop it going too crazy.
First add a target position to your geometry:
v@targetP = v@P;
Next add a solver. Inside the solver, add a point wrangle with this VEX:
float freq = 100.0; float damping = 5.0; // Dampen velocity to prevent infinite overshoot (done first to avoid distorting the acceleration) v@v /= 1.0 + damping * f@TimeInc; // Find direction towards target vector dir = v@targetP - v@P; // Accelerate towards it (@TimeInc to handle substeps) v@accel = dir * freq; v@v += v@accel * f@TimeInc; v@P += v@v * f@TimeInc;
To adjust motion over time, plug the current geometry into the second input and use it instead of v@targetP:
// Find direction towards target vector dir = v@opinput1_P - v@P;
UPDATE: The spring solver in MOPs uses Hooke\’s law.
This is more physically accurate, but I don\’t know how to make it substep independent.
float mass = 1.0; float k = 0.4; float damping = 0.9; // Find direction towards target vector dir = v@targetP - v@P; // Accelerate towards it vector force = k * dir; v@v += force / mass; // Dampen velocity to prevent infinite overshoot v@v *= damping; v@P += v@v * f@TimeInc;
Non-Recursive Version
An easy approximation is an oscillator with an exponential falloff. This is basically damped harmonic motion.
Note how it travels from 1 to 0. This makes it perfect to use as a mix factor, for example with lerp.
float spring(float time; float frequency; float damping) { return cos(frequency * time) * exp(-damping * time); } // Example usage, spring to targetP v@P = lerp(v@targetP, v@P, spring(f@Time, 10.0, 5.0));
Overshoot occurs since cos ranges between -1 and 1. To fix this, remap cos between 0 and 1 instead.
float spring_less(float time; float frequency; float damping) { return (cos(frequency * time) * 0.5 + 0.5) * exp(-damping * time); // Or fit11(..., 0, 1) } // Example usage, spring to targetP v@P = lerp(v@targetP, v@P, spring_less(f@Time, 10.0, 5.0));
Make an aimbot (find velocity to hit a target)
Want to prepare for the next war but can\’t solve projectile motion? Never fear, the Ballistic Path node is all you need.
| Video Tutorial |
|---|
Hit a static target
- Connect your projectile to a Ballistic Path node.
- Set the Launch Method to \”Targeted\” and disable drag.
- Add a
@targetPattribute to your projectile. Set it to the centroid of the target object.
v@targetP = getbbox_center(1);
- You should see an arc. Transfer the velocity of the first point of the arc to your projectile.
v@v = point(1, \"v\", 0);
-
Connect everything to a RBD Solver.
-
Use \”Life\” to set the height of the path, and lower the \”FPS\” to reduce unneeded points.
Hit a moving target
Use the same method as before, but sample the target\’s position forwards in time.
- On the Ballistic Path node, set the Targeting Method to \”Life\”.
- Copy the \”Life\” attribute. It\’s the number of seconds until we hit the target. We need to find where the target is at that time.
- Add a Time Shift node to the target (before the centroid is calculated). Set it to the current time plus the \”Life\” attribute.
| Download the HIP file! |
|---|
Hit multiple targets
Extract multiple centroids and transfer v from each arc. Enable \”Path Point Index\” on Ballistic Path, blast non-zero indices, then Attribute Copy v.
If your \”Life\” changes per target, set a life attribute on each point.
Copernicus: Radial Blur
Simple radial blur shader I made for Balthazar on the CGWiki Discord.
#bind layer src? val=0 #bind layer !&dst #bind parm quality int val=10 #bind parm center float2 val=0 #bind parm scale float val=0.2 #bind parm rotation float val=0 @KERNEL { float2 offset = @P - @center; float4 result = 0.; float scale = 1; for (int i = 0; i <= @quality; ++i) { result += @src.imageSample(offset * scale + @center) / (@quality + 1); offset = rotate2D(offset, @rotation / @quality); scale -= @scale / @quality; } @dst.set(result); }
| Download the HIP file! |
|---|
Copernicus to Heightfield
Copernicus stores images in 2D volumes. Guess what else is stored in 2D volumes? Heightfields!
| Video Tutorial |
|---|
Complex Growth in 2 nodes
You can get cool and organic looking shapes using opposing forces, like Relax and Attribute Blur.
| Download the HIP file! | Video Tutorial |
|---|
Smooth steps
Smoothstep\’s evil uncle, smooth steps. This helps for staggering animations, like points moving along lines.
Start with regular steps. This is the integer component:
Use modulo to form a line per step, then clamp it below 1. This is the fractional component:
Add them together to achieve smooth steps:
float x = f@Time; // Replace with whatever you want to step float width = 2; // Size of each step float steepness = 1; // Gradient of each step int int_step = floor(x / width); // Integer component, steps float frac_step = min(1, x % width * steepness); // Fractional component, lines float smooth_steps = int_step + frac_step; // Both combined, smooth steps
Remove points by time after simulation
Sometimes POP sims take ages to run, especially FLIP sims. This makes it annoying to get notes about timing changes.
I found a decent approach to avoid resimulation:
- Simulate tons of points, way more than you need.
- After simulating, get the birth time of each point using
f@Time - f@age. - Cull points based on the birth time. There\’s 2 main ways to do it.
Keyframes over time
chf() lets you fetch values over time using chf(\"channel\", time). Use the birth time and you\’re good to go!
float birth_time = f@Time - f@age; if (chf(\"keep_percent\", birth_time) < rand(i@id)) { removepoint(0, i@ptnum, 0); }
Ramp over time
I used to remap time using a ramp instead. It\’s not as controllable as keyframes, but helps in some cases.
float birth_time = f@Time - f@age; float time_factor = invlerp(birth_time, $TSTART, $TEND); if (chramp(\"keep_percent\", time_factor) < rand(i@id)) { removepoint(0, i@ptnum, 0); }
| Original Sim | Post-Sim Removal |
|---|---|
I used this ramp for the demo above:
| Download the HIP file! |
|---|
Generating circles
Sometimes you need to generate circles without relying on built-in nodes, like to know the phase.
Luckily it\’s easy, just use sin() on one axis and cos() on the other:
float theta = chf(\"theta\"); float radius = chf(\"radius\"); v@P = set(cos(theta), 0, sin(theta)) * radius;
See Waveforms for more about sine and cosine.
To draw a circle, add points while moving between 0 and 2*PI:
int num_points = chi(\"point_count\"); float radius = chf(\"radius\"); for (int i = 0; i < num_points; ++i) { // Sin/cos range from 0 to 2*PI, so remap from 0-1 to 0-2*PI float theta = float(i) / num_points * 2 * PI; // Use sin and cos on either axis to form a circle vector pos = set(cos(theta), 0, sin(theta)) * radius; addpoint(0, pos); }
To connect the points, you can use addprim():
int num_points = chi(\"point_count\"); float radius = chf(\"radius\"); int points[]; for (int i = 0; i < num_points; ++i) { // Sin/cos range from 0 to 2*PI, so remap from 0-1 to 0-2*PI float theta = float(i) / num_points * 2 * PI; // Use sin and cos on either axis to form a circle vector pos = set(cos(theta), 0, sin(theta)) * radius; // Add the point to the array for polyline int id = addpoint(0, pos); append(points, id); } // Connect all the points with a polygon addprim(0, \"poly\", points);
| Download the HIP file! |
|---|
Extract Transform in VEX
Ever wondered how Extract Transform works? Turns out it uses a popular matrix solving technique called singular value decomposition.
Align Translation
Aligning the translation is easy. The best translation happens when you align the center of mass (average) of each point cloud.
You don\’t even need VEX for this, just use Extract Centroid set to \”Center of Mass\”, then offset the position by that amount.
// Detail Wrangle: Solves translation only // Input 1: Source // Input 2: Target // 1. Calculate centroids float n = npoints(1); vector source_centroid = 0, target_centroid = 0; for (int i = 0; i < n; ++i) { source_centroid += point(1, \"P\", i); target_centroid += point(2, \"P\", i); } // 2. Turn translation into 4x4 matrix matrix transform = ident(); translate(transform, (target_centroid - source_centroid) / n); // 3. Add point for Transform Pieces to use setpointattrib(0, \"transform\", addpoint(0, {0, 0, 0}), transform);
Align Translation + Rotation
Aligning the rotation is harder. You need to build a covariance matrix, then solve it with SVD.
Luckily we don\’t need to leave VEX! Houdini has a SVD solver called svddecomp().
// Detail Wrangle: Solves translation and rotation only // Input 1: Source // Input 2: Target // 1. Calculate centroids float n = npoints(1); vector source_centroid = 0, target_centroid = 0; for (int i = 0; i < n; ++i) { source_centroid += point(1, \"P\", i); target_centroid += point(2, \"P\", i); } target_centroid /= n; source_centroid /= n; // 2. Build covariance matrix matrix3 covariance = ident(); for (int i = 0; i < n; ++i) { vector source_diff = point(1, \"P\", i) - source_centroid; vector target_diff = point(2, \"P\", i) - target_centroid; covariance += outerproduct(target_diff, source_diff); } // 3. Solve rotation with SVD matrix3 U; vector S; matrix3 V; svddecomp(covariance, U, S, V); matrix3 R = V * transpose(U); // 4. Flip if determinant is negative (this causes negative scales to screw up) if (determinant(R) < 0) { R = V * diag({1, 1, -1}) * transpose(U); } // 5. Combine translation and rotation into 4x4 matrix matrix transform = set(R); translate(transform, target_centroid - source_centroid); // 6. Add point for Transform Pieces to use setpointattrib(0, \"transform\", addpoint(0, {0, 0, 0}), transform);
Align Translation + Rotation + Scale
Aligning the scale is even harder. One popular way is the Umeyama algorithm.
Sadly it only solves uniform scale, and breaks on negative scales. This happens when you flip the sign of the 2nd column of the rotation matrix.
Extract Transform set to \”Uniform Scale\” also breaks with negative scales, so it probably uses this method!
// Detail Wrangle: Solves translation, rotation, uniform scale (like Eigen::umeyama) // Input 1: Source // Input 2: Target // 1. Calculate centroids float n = npoints(1); vector source_centroid = 0, target_centroid = 0; for (int i = 0; i < n; ++i) { source_centroid += point(1, \"P\", i); target_centroid += point(2, \"P\", i); } target_centroid /= n; source_centroid /= n; // 2. Build covariance matrix float deviation = 0; matrix3 covariance = ident(); for (int i = 0; i < n; ++i) { vector source_diff = point(1, \"P\", i) - source_centroid; vector target_diff = point(2, \"P\", i) - target_centroid; deviation += length2(source_diff); covariance += outerproduct(target_diff, source_diff); } // 3. Solve rotation with SVD matrix3 U; vector S; matrix3 V; svddecomp(covariance, U, S, V); matrix3 R = V * transpose(U); // 4. Flip if determinant is negative (this causes negative scales to screw up) float det = determinant(R); vector e = set(1, 1, det < 0 ? -1 : 1); if (det < 0) { R = V * diag(e) * transpose(U); } // 5. Solve scale using standard deviation R *= dot(S, e) / deviation; // 6. Combine translation rotation and scale into 4x4 matrix matrix transform = set(R); translate(transform, target_centroid - (R * source_centroid)); // 7. Add point for Transform Pieces to use setpointattrib(0, \"transform\", addpoint(0, {0, 0, 0}), transform);
| Download the HIP file! |
|---|
Attribute Interpolate / Primuv in VEX
Ever wondered how primuv works? It doesn\’t exist in OpenCL, so I had to remake it:
vector primuv_diy(int geo; string attr; int prim; vector</span
