Creating a Non-Deterministic Interaction System (or putting souls into the creatures)

The biggest challenge of this thesis has clearly been to create the displacement set of rules that was pitched as the component named “Phasma”. Different from the actual creature leg locomotion for semi-believable behavior, this component deals with the displacement of the creatures in space to establish seeking and avoidance behaviors between the two types of creatures, hunters and prey.

Originally the goal was to set up a locator system that would represent the position of each creature using a particle system using Houdini’s built-in POPs, but it quickly organically transitioned towards a customized VEX-based system within a SOP Solver. 

This custom approach starts by using the environment component of this system (Arena) as a base to distribute points on it. These points are scattered on it in a way that ensures a uniformly distant distribution to guarantee no territorial or spatial overlaps (prey points don’t scatter in the same space as hunters and vice-versa).

Creature Distribution on “Arena”

Each point is flagged as either prey or hunter to, later on, set their custom behavior. User parameters are made available to control the amount of each type as well as speeds. 

The system works by pre-establishing a VEX array of the creatures in detail mode to, later on, be accessed by the hunters to identify their target.

				
					//make array of prey pts
int @preypts[] ;

for (int i = 0; i < @numpt; i++) {
    if(point(0, "prey_pt", i) == 1){
        int index = point(0,"index", i);
        append(@preypts, index);
    }
}
				
			

A Simple Point Wrangle is Used to Make The Prey Array

Next, in a separate Point Wrangle, a set of rules is established to determine initial targets. This works by locating the nearest point flagged as a prey point and setting it up as the current point’s target. If no near prey is found an arbitrary prey is set as a target.

				
					//find near points for hunters
float maxdist = chf("max_dist");
int maxpts = chi("max_points");

if (i@hunter_pt == 1){
    int nearpreypts[] = nearpoints(®, "@prey_pt==1", @P, maxdist, maxpts);

    //remove self from array
    removeindex(nearpreypts, 0);

    //set target for hunter
    if(len(nearpreypts) >= 1){
        i@target_pt = nearpreypts([0];
    }
    else {
        i@target_pt = -1;
    }
}

//assing random prey pt from prey pts array as target
if(i@target_pt == -1){
    int preypts[] = detail(0, "preypts", 0);
    int selector = int(rand(sqrt(@ptnum*159.0254)) * len(preypts));
    i@target_pt = preypts[selector];
}
				
			

Wrangle Setting Up The Initial Rules for Hunters

Everything else happens within the SOP solver where the system is reevaluated each frame. In a separate Point Wrangle, Initial directions and speeds are set for Hunters by creating vectors between the two points. These vectors update themselves each time the solver is reevaluated. Since no real contact interaction has been implemented, a separation rule is set so preys and hunters don’t overlap.

Prey Speeds are set to be random, but with a user control multiplier.

				
					
vector targetpos = point(0, "P", i@target_pt);
float seed = chf("seed");
float dist = distance(@P, targetpos) ;

//HUNTER speeds

if(@hunter_pt == 1 && i@target_pt != -1){
    //vector pos = point(0, "P", i@target_pt);
    vector dir = normalize(targetpos - @P);
    v@dir =  dir*10;
    v@v = dir*chf("“hunter_velocity_mult") ;

    //ensure HUNTER separation from PREY
    if(dist < chf("threshold")){
        float vel_prox_mult = fit(dist, 3, 5, 0.01, 1);
        v@v *= vel_prox_mult;
    }
}

//PREY speeds

else if(@prey_pt == 1 && @Frame == 1) {
    v@v = set(fit0l(rand(@index) ,-1,1), 0, fitO1(rand(@index*seed) ,-1,1))*chf("prey_velocity_mult");
}

				
			
Naturally, prey creatures need to avoid hunters, so additional rules had to be set for this behavior as well. This is simply developed by looking for near points tagged as hunters and calculating vectors away from them. Keep in mind this all happens within the solve.
				
					float maxdist = chf("max_dist");
int maxpts = chi("max_pts");

if(@prey_pt ==1){
    i[]@hunters_near_prey = nearpoints(0, "Ghunter_pt==1", @P, maxdist, maxpts) ;

    int nearptslen = len(@hunters_near_prey);
    vector avgpos = 0;

    for(int i = 0; i < nearptslen; i++) {
        vector pos = point ( 0, "P", @hunters_near_prey[i]);
        avgpos += pos;
    }
    avgpos /= nearptslen;
    float blend = fit(distance(@P, avgpos), 0, 5, 1, 0);
    v@v = lerp(@v, normalize(@P - avgpos)*chf("prey_velocity_mult"), blend);
}
				
			

It is also necessary for hunters to avoid themselves to a certain degree to ensure that no overlaps happen. This rule is applied in a similar fashion, with hunters looking for near points that are flagged as hunters and calculating and applying vectors away from those.

				
					//hunter self avoidance

float maxdist = chf("max_dist");
int maxpts = chi("max_pts");

if(@hunter_pt ==1){
    i[J@hunters_near_hunters = nearpoints(®, "@hunter_pt==1", @P, maxdist, maxpts);
}

int removed = removeindex(@hunters_near_hunters, 0);

vector pos = point(0, "P", @hunters_near_hunters[0]);
vector dir = normalize(@P-pos) ;

if(@hunter_pt == 1){
    va@v += dir*0.1;
}
				
			
And naturally, the same behavior has to be set for prey to avoid themselves.
				
					// prey avoidance

float maxdist = chf("max_dist");
int maxpts = chi("max_pts");

if(@prey_pt ==1){
    i[]@prey_near_prey = nearpoints(®, "@prey_pt==1", @P, maxdist, maxpts);
}
int removed = removeindex(@prey_near_prey, ©);
vector pos = point(0, "P", @prey_near_prey[0]);
vector dir = normalize(@P-pos) ;

if(@hunter_pt ==1){
    va@v += dir*d.1;
}
				
			

The behavior has to be limited to the bounds of “Arena”. To make sure the creatures stay within bounds, “Arena” is used as a reference to create a boundary zone at its limits where the velocity gets rotated. The closer it gets to the edge, the more it rotates. This ensures that the creatures don’t wander out of the environment. 

				
					//get bbox of ref terrain

vector bboxmin = getbbox_min(1);
vector bboxmax = getbbox_max(1);

//turn domain amplitude using threshold
float threshold = chf("threshold");

// make turn domain
float xpositive_turn_gradient = fit(@P.x, (bboxmax.x - threshold), bboxmax.x, 0, 1};
float xnegative_turn_gradient = fit(@P.x, (bboxmin.x + threshold), bboxmin.x, 0, 1);
float zpositive_turn_gradient = fit(@P.z, (bboxmax.z - threshold), bboxmax.z, 0, 1);
float znegative_turn_gradient = fit(@P.z, (bboxmin.z + threshold), bboxmin.z, 0, 1);
float xturn_grad = max(xpositive_turn_gradient, xnegative_turn_gradient) ;
float zturn_grad = max(zpositive_turn_gradient, znegative_turn_gradient) ;
float turn_grad = max(xturn_grad, zturn_grad);

//velocity rotation

//the left/right direction each creature picks depends on a random number fed to angle
float turn_mag = chf("turn_magnitude");
float angle = fit(rand(@index) ,0.49, 0.51, -turn_mag, turn_mag)* turn_grad;

//make quaternion to rotate vector using qrotate further down
vector4 rot = quaternion(radians(angle), set(@,1,0));

//make ref vector to stop rotation once it "aligns" towards the desired direction
v@ref_edge_v = normalize(set(@P.x, 0, @P.z))*-1;

//dot is used to compare the direction of the velocity vs. the ref edge vector
@dot = dot(normalize(ve@v), @ref_edge_v);
if(@dot < 0.75 && turn_grad > 0){
    v@v = qrotate(rot, v@v);
}
else{
     @v = @v;
}
				
			

Turn Enforcement Setup

The last step is to update the position of points and make sure they stay on top of the surface of “Arena”.

				
					vector vsample = volumesample(1, "height", @P);
if(@P.y != vsample.y){
    @P.y = vsample.y;
}
				
			
With all the rules set, the system allows for non-deterministic motion. User input is provided to change the creature scattering independently, the speeds of the creatures, and the turn magnitude once they enter the boundary zones. Additionally, visualizers are provided so the user can see the paths creatures will take once this component is connected to the individual creatures’ HDAs.

An Example Of Creature Displament Using the Embedded Visualizers Provided

Once the behavior is tweaked, the 3 elements that conform the Bioma system are connected together. Creatures’ HDAs are connected last and as many as representing points. This allows for customization per creature since each individual HDA provides user controllers to change creature morphology and leg locomotion.

An example of how the 3-part Bioma System is Stacked Together in the Network Editor

The creatures’ HDAs have an internal system to recognize which point to use in order to place themselves in the right place in the environment and if it’s a prey or hunter flagged point. Once all the nodes are properly stacked, the creatures locomote as expected.

It’s worth mentioning that the creatures’ HDAs have a parallel embedded system to use them without the “Phasma” component. This allows for semi-deterministic motion, where the user sets a direction, locomotion speed, and path length. Since one of the goals of this tool is to have flexibility, as well as the ability to art direct, it was important to have the option to include this alternative as well. 

This concludes this rather intricate post about the component that gives life to the creatures. Thank you for reading and until the next post!

Leave a Reply

Your email address will not be published. Required fields are marked *