Various Houdini tips and tricks I use a bunch. Hope someone finds this helpful!
These articles grew too long to fit here. They’re the most interesting in my opinion, so be sure to check them out!
Need to overshoot an animation or smooth it over time to reduce bumps? Introducing the simple spring solver!
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;
// Find direction towards target
vector dir = v@targetP - v@P;
// Accelerate towards it (@TimeInc to handle substeps)
vector accel = dir * freq * f@TimeInc * f@TimeInc;
v@v += accel;
// Dampen velocity to prevent infinite overshoot
v@v /= 1.0 + damping * f@TimeInc;
v@P += v@v;
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 has better damping:
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;
vector accel = force / mass;
v@v += accel;
// Dampen velocity to prevent infinite overshoot
v@v *= damping;
v@P += v@v;
Want to prepare for the next war but can’t solve projectile motion? Never fear, the Ballistic Path node is all you need.
@targetP
attribute to your projectile. Set it to the centroid of the target object.v@targetP = getbbox_center(1);
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.
Use the same method as before, but sample the target’s position forwards in time.
If your “Life” is the same for all projectiles, extract multiple centroids and transfer velocities from the first point of each arc based on connectivity. Try enabling “Path Point Index” on Ballistic Path and blasting all non-zero indices.
If your “Life” changes per target, use a for loop instead.
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
Sometimes POP sims take ages to run, especially FLIP sims. This makes it painful to get notes about precise timing changes.
I found a decent approach to avoid resimulation:
f@Time - f@age
.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:
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);
Ever wanted to remake Sweep in VEX? Me neither, but let’s do it anyway!
First we need to know the orientation of each point along the curve.
The easiest way is with the Orientation Along Curve node. Enable the X and Y vectors @out
and @up
.
The @out
and @up
vectors define a 3D plane we can slap circles on.
For example, multiply sin()
by one vector and cos()
by the other, then add them together. This gets a point around a circle flattened to the plane.
v@P += sin(phase) * v@up - sin(phase) * v@out;
Using that in a for loop, we can replace each point with a correctly oriented circle.
int cols = chi("columns");
float radius = chf("radius");
float angle = radians(chf("angle"));
for (int i = 0; i < cols; ++i) {
// Column factor (0-1 exclusive)
float col_factor = float(i) / cols;
// Row factor (0-1 inclusive)
float row_factor = float(i@ptnum) / (i@numpt - 1);
// Scale and rotation ramps
float scale_ramp = chramp("scale_ramp", row_factor);
float roll_ramp = chramp("roll_ramp", row_factor);
// Generate circles on the plane defined by v@out and v@up
float phase = 2 * PI * (col_factor + roll_ramp) + angle;
vector offset = cos(phase) * v@out - sin(phase) * v@up;
addpoint(0, v@P + offset * radius * scale_ramp);
}
// Remove original points
removepoint(0, i@ptnum);
Now we just need to connect the circles with quads. To make a quad, we need the index of each corner.
The tricky bit is looping back to the start after we reach the end of each ring, which takes a bit of modulo.
int cols = chi("columns");
// Skip last connections when path is open
int closed = chi("close_path");
if (!closed && i@ptnum >= i@numpt - cols) return;
// Row and column indexes per point
i@ptrow = i@ptnum / cols;
i@ptcol = i@ptnum % cols;
// Point indexes of the 4 corners of each quad
int corner1 = i@ptnum;
int corner2 = i@ptrow * cols + (i@ptnum + 1) % cols;
int corner3 = (corner2 + cols) % i@numpt;
int corner4 = (corner1 + cols) % i@numpt;
// Add quads
int prim_id = addprim(0, "poly", corner1, corner2, corner3, corner4);
// Row and column indexes per prim
setprimattrib(0, "primrow", prim_id, i@ptrow);
setprimattrib(0, "primcol", prim_id, i@ptcol);
The same code works for custom cross sections, though it’s easier to use Copy to Points to orient each cross section.
Just make sure the points are sorted and cols
matches the point count of the cross section!
If cols
doesn’t match the point count, never fear. You’ll get cool trippy looking shapes!
I showed this to Lara Belaeva, who pushes Sweep to its limits on LinkedIn. She tried it already, but had an interesting point:
If I decided to build my own Sweep I would try to do it similar to what we have in Plasticity. In Plasticity you can take several different cross-sections, put them in different regions of the curve, and the Sweep creates the blends between them across the curve. I tried to make such a tool but it’s still so-so. Houdini’s Sweep also can use different cross sections, but doesn’t create blends between them
It would be cool if Sweep supported different cross sections. But what if it does already?
Try using normals from Orientation Along Curve, then copy different cross sections onto each point with Copy to Points.
Plug that into Sweep’s second input and you’ll see we can use different cross sections after all!
Although it works, if you look closely it resampled each cross section to the same number of points. Lara tried this too:
I just resampled these cross-sections with a constant number of points and then they were sort of placed along the curve based on curveu attribute, and then the algorithm connected 1-1-1, 2-2-2, 3-3-3 points of these cross-sections with polylines. And then these polylines were turned into a mesh with Loft.
So what if we want the exact same cross sections? PolyBridge comes to our rescue!
Plug the cross sections into a PolyBridge node instead. Set the source group to all prims from 0 to the second last:
0-`nprims(0)-2`
Set the destination group to all prims from 1 to the last:
1-`nprims(0)-1`
Now the cross sections connect perfectly without any resampling!
Sometimes you need to overlap UV islands and fit them to a full tile, like when slicing a sphere.
This is hard to do with Houdini’s built-in nodes, so here’s a manual approach.
v@uv = invlerp(v@uv, v@uvmin, v@uvmax); // Or v@uv = fit(v@uv, v@uvmin, v@uvmax, 0, 1);
Before | After |
---|---|
Most programming languages have ways to share and reuse code. C has #include
, JavaScript has import
, but what about VEX?
VEX has #include
as well, but sadly it only works if you put the file in a specific Houdini directory.
Luckily there’s a secret way to reuse code without #include
! I found it in a couple of LABS nodes.
First add any node with a string property. It can be a wrangle, a null or anything else. In the string property, type the functions you want to share.
vector addToPos(vector p) {
return p + {1, 2, 3};
}
Now you can import and run those functions in any other wrangle with chs("../path/to/string_property")
enclosed in backticks!
// Append the code string and run it (like #include)
`chs("../path/to/string_property")`
v@P = addToPos(v@P);
UPDATE: Van and WaffleboyTom said this is evil since it causes the code to recompile. Use if you dare!
A similar hack is using chs("var_name")
to set an attribute name at compile time.
For example, making a dynamically named integer attribute set to 123
:
i@`chs("var_name")` = 123;
Certain characters like spaces aren’t allowed in variable names, so try not to include them!
Surprisingly it’s tricky to display text based on an attribute or VEX snippet. Here’s a few ways to do it!
Igor Elovikov told us about a top secret Houdini feature, multiline expressions!
You can do it in expression but it’s a rather an esoteric part of Houdini parameters.
Multiline expressions must be enclosed in {} and have a return statement, otherwise they evaluate to zero.
`{
string result = '';
for (i = 0; i < 10; i++) {
result = strcat(result, 'any');
}
return result;
}`
Igor used strcat()
to join the strings. I found adding works too, it doesn’t need typecasting unlike VEX!
If you want to use VEX, never fear! Make a detail attribute, add it as a spare input, then use details()
to display it as text.
`details(-1, "your_string_attrib")`
nearpoint()
finds the closest point to @P
, but what if you need the closest point to something else?
The shortest way is abusing pcfind()
, which takes any input as the position channel:
string attrib = "density";
float target = 16.0;
int nearest_id = pcfind(0, attrib, target, 99999.9, 1)[0];
Another option is using Attribute Swap to move the attribute to @P
. Keep in mind this only works for certain types of attributes.
To find an exact match, use findattribval()
instead.
Swalsch told us about a top secret alternative to the above, known as unwrap.
It changes the context of any VEX function, allowing you to override functions to work with any attribute.
For example, nearpoint()
uses @P
. Normally you have to round trip to use another attribute like @uv
:
// Get the geometry position closest to a UV coordinate
vector world_pos = uvsample(0, "P", "uv", chv("uv_coordinate"));
// Find the nearest point to that position
i@near_id = nearpoint(0, world_pos);
The direct way is using unwrap to replace the context:
// All in one, thanks @swalsch!
i@near_id = nearpoint("unwrap:uv opinput:0", chv("uv_coordinate"));
A cool trick from John Kunz is sampling a HDRI using VEX. It’s a cheap way to get environment mapping without leaving the viewport.
// Insert your camera position here
vector cam_pos = fromNDC("/obj/cam1", {0, 0, 0});
// John Kunz magic
vector r = normalize(reflect(normalize(v@P - cam_pos), v@N));
vector uv = set(atan2(-r.z, -r.x) / PI + 0.5, r.y * 0.5 + 0.5, 0);
v@Cd = texture("$HFS/houdini/pic/hdri/HDRIHaven_skylit_garage_2k.rat", uv.x, uv.y);
Levin on the CGWiki Discord wanted to blur volumes in VEX. You can do it by sample neighbors in a box and averaging them together. This is slower than the built-in volume nodes, but might be useful one day:
float density_sum = 0;
int num_samples = 0;
int voxel_radius = chi("voxel_radius");
for (int x = -voxel_radius; x <= voxel_radius; ++x) {
for (int y = -voxel_radius; y <= voxel_radius; ++y) {
for (int z = -voxel_radius; z <= voxel_radius; ++z) {
// Sample voxel at offset index
vector voxel = set(i@ix + x, i@iy + y, i@iz + z);
float density = volumeindex(0, 0, voxel);
// Add to sum and sample count
density_sum += density;
++num_samples;
}
}
}
f@density = density_sum / num_samples;
The Group node has a useful option to select by normals. Carlll on the CGWiki Discord was looking for a VEX equivalent.
A dot product tells how similar two vectors are, negative when opposite, positive when similar.
To know if a vector points in a direction, you’d check if the dot product passes a threshold.
@group_upward = dot(v@N, {0, 1, 0}) > chf("threshold");
Here’s a more complete version:
float similarity = dot(v@N, normalize(chv("direction")));
if (chi("include_opposite_direction")) {
similarity = abs(similarity);
}
i@group_`chs("group_name")` = similarity >= cos(radians(chf("spread_angle")));
Spot the difference. On the left is the Group node, on the right is VEX.
Sometimes you need to select the inside or outside of double-sided geometry, for example to make single-sided geometry if Fuse doesn’t work.
The normals are great whenever you need to select anything by direction. Usually you can use a Group node set to “Keep By Normals”, then use “Backface from” to pick the interior.
If it screws up, here’s another approach. Assuming the interior points inwards and the exterior points outwards, the normals of the interior should point roughly towards the center. That means to detect the interior, you can compare the direction of the normal with the direction towards the center.
vector center = point(1, "P", 0);
vector dir = normalize(center - v@P);
float correlation = dot(dir, v@N);
@group_inside = correlation > ch("threshold");
It’s always hard to get a decent sim when your collision geometry is on life support. Here’s a few ways to clean it up!
Good for point clouds! VDB from Particles works too, but not as smoothly.
Good for flat surfaces! For more control, use point normals to set the extrusion direction.
Copy to Points automatically applies quaternions like @orient
, but what if you need the same effect without Copy to Points?
Normally you’d set the transform
intrinsic, but this replaces everything. To just replace the @orient
, set pointinstancetransform
to 1.
setprimintrinsic(0, "pointinstancetransform", i@elemnum, 1);
Thanks to WaffleboyTom for this tip!
Often it’s nice to organise geometry by snapping it to the floor. Here’s a few ways to do it!
The easiest way is using a Match Size node with “Justify Y” set to “Min”. It snaps the position only, and won’t affect the rotation.
To affect the rotation too, swalsch suggested using dihedral()
. You can use it to rotate the normal towards a down vector.
First the object needs prim normals, which you can add with a Normal node set to “Primitives”.
Next pick a prim to snap to the floor, get its normal and use dihedral()
to build the rotation matrix.
int primIndex = 2670;
// Snap object to prim center
vector centroid = prim(0, "P", primIndex);
vector baseN = prim(0, "N", primIndex);
@P -= centroid;
// Rotate normal vector towards down vector
matrix3 rotMat = dihedral(baseN, {0, -1, 0});
@P *= rotMat;
To use multiple prims, make a prim group and average out the normals within that group.
// Get prims in a prim group called "bottom"
int prims[] = expandprimgroup(0, "bottom");
int primCount = len(prims);
// Find average position and normal
vector posSum = 0, normalSum = 0;
foreach (int primIndex; prims) {
posSum += prim(0, "P", primIndex);
normalSum += prim(0, "N", primIndex);
}
// Snap object to average position (same as getbbox_center(0, "bottom"))
v@P -= posSum / primCount;
// Rotate average normal towards down vector
matrix3 rotMat = dihedral(normalSum / primCount, {0, -1, 0});
v@P *= rotMat;
The hackiest way is abusing Extract Transform. You flatten the prims to the floor, then approximate the transform for it.
This affects position and rotation, but isn’t as good as dihedral()
since it won’t flip the object past 180 degrees.
Unlike the RBD Solver, the FEM Solver doesn’t list real world physical units. How do you use measurements with it?
I emailed SideFX, and they responded with some useful information:
I am told both Shape Stiffness and Volume Stiffness have units of Pascal when the stiffness multiplier is set to 1.
With the default Stiffness Multiplier of 1000, both these parameters would be in KPa.These coefficients have the following meaning:
shape_stiffness = 2x Lamé's second parameter volume_stiffness = Lamé's first parameter
Conversion from other physical parameters, which are more commonly available:
If you have the Young’s modulus and the Poisson ratio of your material, you can compute the corresponding shape stiffness and volume stiffness parameters by:
shape_stiffness = youngs_modulus / ( 1 + poisson_ratio ) volume_stiffness = shape_stiffness * ( poisson_ratio / ( 1 - ( 2 * poisson_ratio ) ) )
The damping ratio parameters are unitless and should be chosen in between 0 and 1.
The attributes solidshapestiffness, solidvolumestiffness, etc. work as multipliers for the parameters that you specify on the object.
You could keep these between 0 and 1 if you like to dial in the relative stiffness for parts of the material, where the overall stiffness is determined by the shape stiffness and volume stiffness parameters.
Vellum is usually wobbly like jelly, making hard objects tricky to achieve without an RBD Solver.
If you absolutely need Vellum, a great technique comes from Matt Estela.
Keep the topology as basic as possible and try increasing the substeps to make Shape Match even more stiff.
Otherwise known as swapping reference frames, rest to animated, world to local, frozen to unfrozen…
Perhaps the best trick in Houdini is moving geometry to a rest pose, doing something and moving it back to an animated pose.
It fixes tons of issues like broken collisions, VDBs jumping around, plus aliasing and quantization artifacts.
Extract Transform and Transform Pieces are your best friends.
As well as Transform Pieces, you can set Extract Transform to output a matrix and transform manually in VEX:
// Extract Transform matrix from input 1
matrix mat = point(1, "transform", 0);
// Forward transform
v@P *= mat;
// Inverse transform
v@P *= invert(mat);
For simple cases, Point Deform is your best friend.
If you need better interpolation, try primuv()
or Attribute Interpolate. This works great for proxy geometry, for example remeshed cloth.
xyzdist()
to map the positions of the good geometry onto the proxy geometry, both in rest position.xyzdist(1, v@P, i@near_prim, v@near_uv);
primuv()
to match the good geometry’s position to the animated proxy.v@P = primuv(1, "P", i@near_prim, v@near_uv);
@orient
, @N
and @up
attributes.A common misconception is primuv()
uses the actual UV map of the geometry. This would cause problems if the UVs overlapped.
Instead it uses intrinsic UVs. Intrinsic UVs are indexed by prim and range from 0 to 1 per prim. Since each prim is separated by index, it’ll never overlap.
Regular UVs | Intrinsic UVS |
---|---|
If you want to use the actual UVs, use uvsample()
instead.
Usually liquids resting on a surface have a small gap due to the collision geometry, easier to see once rendered.
A tip from Raphael Gadot is to transfer normals from the surface onto the liquid with some falloff. This greatly improves the blending.
Is your scene slow? Don’t blame Houdini, it’s likely you haven’t optimized properly.
Use Houdini’s performance monitor to track down what’s slowest.
Velocity is easy to overlook and hard to get right. I’ve rendered full shots before realising I forgot to put velocity on deforming geo, transfer it to packed geo, or it doesn’t line up.
A great tip from Lewis Taylor is to double check velocities from POP sims. It sometimes ignores POP forces and calculates an incorrect result.
For checking velocities, a tip from Ben Anderson is Time Shift a frame backward, template it and display velocity. You should see a line between the past and present position.
Combining multiple pairs of VDBs is often unpredictable, for example combining two sims by density may skip velocity.
Make sure to combine each VDB pair separately, then feed all pairs into a merge node.
I used to do this to generate random velocities between -0.5 and 0.5. See if you can spot the problem.
v@v = rand(i@ptnum) - 0.5;
The issue is VEX uses the float version of rand()
, making the result 1D:
float x = rand(i@ptnum);
v@v = x - 0.5;
To get a 3D result, there are two options. Either explicitly declare 0.5 as a vector:
v@v = rand(i@ptnum) - vector(0.5);
Or explicity declare rand()
as a vector:
vector x = rand(i@ptnum);
v@v = x - 0.5;
This happens a lot, so always explicitly declare types to be safe!
Even with fixed typecasting, there’s still a problem. See if you can spot it:
v@v = rand(i@ptnum) - vector(0.5);
What shape would you expect to see? Surely a sphere, since it’s centered at 0 and random in all directions?
Unfortunately it’s a cube, since the range is -0.5 to 0.5 on all axes separately.
To get a sphere and random vector lengths, use sample_sphere_uniform()
:
v@v = sample_sphere_uniform(rand(i@ptnum));
Roughly equivalent to the following:
v@v = normalize(rand(i@ptnum) - vector(0.5)) * rand(i@ptnum + 1);
To get a sphere and normalized vector lengths, use sample_direction_uniform()
:
v@v = sample_direction_uniform(rand(i@ptnum));
Roughly equivalent to the following:
v@v = normalize(rand(i@ptnum) - vector(0.5));
Sometimes you need to change part of a vector but not the other, like to randomize velocity but inherit the magnitude. It’s easy with rotation, but here’s a more general approach:
// Deconstruction
vector dir = normalize(v@v);
float mag = length(v@v);
// Modify magnitude or direction here
dir = sample_direction_uniform(rand(@ptnum));
// Reconstruction, make sure direction is normalized
v@v = dir * mag;
A key characteristic of fluid is how it sticks together, forming clumps and strands. POP Fluid tries to emulate this, but it doesn’t look as good as FLIP.
To get nicer clumps, a tip from Raphael Gadot is to use Attribute Blur set to “Proximity”. Though it won’t affect the motion, it looks incredible on still frames.
Fluids often screw up whenever colliders move, like water in a moving cup or smoke in an elevator. Either the collider deletes the volume as it moves, or velocity doesn’t transfer properly from the collider.
A great fix comes from Raphael Gadot: Stabilise the collider, freeze it in place. Simulate in local space, apply forces in relative space, then invert back to world space. This works best for enclosed containers or pinned geometry, since it’s hard to mix local and world sims.
@up
vector in world space (before Transform Pieces).v@up = {0, 1, 0};
@up
vector as your gravity force.Force X = -9.81 * point(-1, 0, "up", 0)
Force Y = -9.81 * point(-1, 0, "up", 1)
Force Z = -9.81 * point(-1, 0, "up", 2)
Make sure the force is “Set Always”!
Add a Trail node set to “Calculate Velocity”, then enable “Calculate Acceleration”. It’s faster to do this after packing so it only trails one point.
Add another Gravity Force node, using negative @accel
as your force vector.
Force X = -point(-1, 0, "accel", 0)
Force Y = -point(-1, 0, "accel", 1)
Force Z = -point(-1, 0, "accel", 2)
Make sure the force is “Set Always”!
If you want to deal with open containers, the easiest way is to do a separate sim when the fluid exits the container. This is done by killing points outside the container, then feeding the killed points into the other sim. Make sure to nuke all point attributes to keep it clean for the next sim.
Another tip is use “Central Difference” when calculating the velocity. This gives the fluid more time to move away from the collider.
Cloth sims work best with preroll starting in a neutral rest pose. For example, the character starts in an A-pose or T-pose before transitioning into the animation. If anim screwed you over, never fear! Preroll can be added in Houdini.
One option is using a Blend Shapes node, but you’ll find the limbs usually clip through the body as it swaps from the T-pose to the animated pose. My sketchy method of improving this is using Extract Transform and Transform Pieces.
In other words, the T-posed character flies over to the animated position and rotates to match it. That gives you an easier time using Blend Shapes, since it only has to move the arms and legs a short distance to match the animated pose.
To avoid ruffling the clothes, skip the flying step. Just move the clothes directly to the new T-pose location using Transform Pieces.
Another option could be Labs Straight Skeleton 3D. It generates a skeleton from any mesh which could help with blending, but I haven’t tried it myself.
Cloth sims screw up from clipping, especially when clipped from the start. One option is growing the character into the cloth.
One little known feature of Vellum Cloth (at least to me) is layering. It can improve the physics of overlapping garments, like jackets on top of t-shirts.
Motion blur in Karma can be pretty unpredictable, especially with packed instances.
A great fix comes from Matt Estela: just add a Cache node set to “Rolling Window”. Usually I use 1 frame before and 1 frame after.
This is faster than the new Motion Blur node, which caches the entire timeline at once. It also fixes issues with animated materials.
Use Attribute Promote set to “Detail” with the appropriate mode.
The tricky part about modelling brick walls is the alternating pattern. Every second row is slid across by half a brick’s width. How would you create this pattern? Manual interpolation? Primuv?
An easy way is working subtractively. Take the base curve and resample it. This gives you the first row. For the second row, subdivide the first. Use a “Group by Range” node to select every second point, then delete them with a “Dissolve” node.
If you need geometry in a context that doesn’t provide it (like the forces of a Vellum Solver), just drop down a SOP Solver. You can use Object Merge inside a SOP Solver to grab geometry from anywhere else too. Great for feedback loops!
Many techniques work depending on the situation. Sometimes more randomisation is needed, other times the velocity needs reducing.
A common technique is cranking up the disturbance. Controlling it by speed helps add it where mushrooms are likely to form.
Seems obvious but worth noting: Unlike some software, Houdini supports negative frame ranges.
For preroll you can always start simulating on a negative frame without needing to time shift anything.
Don’t take this section seriously. These are just techniques which seem to work for me.
Density loss often happens when Surface Tension is enabled. Droplets tend to disappear when bunched too close together, so try disabling it before anything else.
Grid Scale and Particle Radius Scale also affect the density. According to SideFX, if the Particle Radius Scale divided by the Grid Scale is at least sqrt(3)/2, it will never be underresolved.
No idea if this affects density, but just in case here’s the minimum:
Particle Radius Scale = Grid Scale × (sqrt(3)/2)
Grid Scale = Particle Radius Scale / (sqrt(3)/2)