0 like 0 dislike

hey folks.

I am writing a crowd system using popcornFX,  which is working well..  but I have a fundamental problem.

for each particle,  I need to find a mesh terrains position.Z given the particles XY location.  (Z-up).

the idea is to get the particle position.XY,  remap to float2 UV space,  then use those UV's to sample the mesh terrain,  and return the world space position at that point.

the .samplePosition  function wants int3 pCoords not UV's.

perhaps I am missing something obvious,   but looking at the samplers in the scripting reference docs I cannot see an obvious way to do this.

does anybody have an idea of how to achieve this?   

any help here is appreciated.



1 Answer

0 like 0 dislike

Hi Gray,

sounds pretty cool ! we'd love to see what you manage to create :)

regarding the problem at hand:
you can't get a position or a parametric coordinate from an UV. The issue is that depending on the way your mesh is unwrapped, there can be multiple locations sharing the same UV (ex: mirrored mesh, or more generally triangles overlapping in UV space). And there can be different UVs sharing the same position (think a mesh that's folded onto itself with overlapping 3D triangles).
The problem would be the same if you asked "give me the point on the mesh where the normal is upwards, equal to float3(0,0,1)", if the mesh is a flat square, an infinite number of points (all points on the mesh) match the request. Even if the mesh has a single perfectly horizontal triangle, there's an infinite number of points. Same issue with finding a position from an UV.

Therefore there isn't a clean generic solution for you to feed an UV and get a 3D position on the mesh.

That's why we have parametric coordinates, these are unique, once you have the parametric coordinate of a point on the mesh, you can access its position, UV, color, normal, whatever.

However, it's not unsolvable.
There are two main solutions to get your end result:

If I understand you correctly, you want, given an initial particle position not on the terrain, move it on the terrain surface beneath it ?

Solution #1:

use the projection evolver. This will reproject the particle position on the terrain mesh each frame.

You can also do the same thing inside a script using "float4 shape.project(Position)" or "int3 shape.projectParametricCoords(Position)" :

float4 projVec = Terrain.project(Position);
float3 posOnTerrain = select(Position, Position + projVec.xyz * projVec.w, projVec.w != infinity);


int3 projPCoords = Terrain.projectParametricCoords(Position);
float3 posOnTerrain = Terrain.samplePosition(projPCoords);

I didn't benchmark the two, but they should give the same results, and projectParametricCoords should be faster at runtime.

IMPORTANT: The projection evolver uses approximations, it's not an exact "closest-point-on-mesh" projection.
This means that the reprojected positions will tend to drift from frame to frame, and could lead to non-moving particles appearing to drift on the terrain surface. Also, the further away the position to reproject is from the actual surface, the more costly the reprojection, and the less precise it is.

Solution #2:

Use a raycast.
take the particle position, and raycast from way above this position to way below the terrain's lowest point:

float4 isec = Terrain.intersect(Position + scene.axisUp() * 10000, -scene.axisUp(), 20000);
float3 posOnTerrain = select(Position, Position - scene.axisUp() * isec.w, isec.w != infinity);

when the raycast doesn't hit anything, isec.w contains infinity.
when it does intersect, isec.w contains the intersection distance along the ray (here, -scene.axisUp(), which is (0,0,-1) in your Z-Up coordinate system), and isec.xyz contains the intersection normal.

Downside is there is no "shape.intersectParametricCoords()" function which returns the parametric coords of the intersection point, so if you do other stuff than getting the terrain position and normal under the particle, you're pretty much stuck with the projection method of solution #1.


If you're dynamically changing the mesh in-engine through an attribute sampler, as attribute samplers do not work at evolve time, only at spawn, you'll be stuck.
You'll have to bake your terrain mesh and reference it as a regular sampler in your effect to be able to project or intersect it at evolve.

Another option, would be to use the intersection method, but using scene.intersect() instead.
This will raycast the entire ingame scene, you can then use collision filters to filter which geometry you'd like to intersect or not, and raycast only the terrain and buildings, for example.

This option can be pretty costly depending on the engine you're using. (Are you on UE4?)

Cool thing with scene intersect is that you have the "scene.intersectExt" function, which allows you to get surface information, and you can make your particles react differently depending on that surface. It requires a bit of setup both on the PK-Editor and your engine's side though.

by Julien (32.6k points)
(about scene.intersectExt() you can get a similar functionality if you use the projectionEvolver or the projectParametricCoords() function, once you've got the pcoords of the reprojected point, you can sample the UV of the terrain at that location, then use that UV to sample a texture sampler where you've encoded color information representing special locations, heatmaps, roads, whatever, and have your crowd agents react differently based on what they "see" of the terrain texture undeneath the location they're at.)