Coding for Fun: Measuring Models and Making Advanced Collision Responses
-
This is going to be a long one, which adds greater likelihood of transcription errors, but it is totally worth the trouble.
With objectIntersect() alone, you are limited to pretty much making things explode on impact or stop moving entirely. However, you can do much much more if you figure out where the boundary faces are, when the collision occurs, and where it should move.
It is a multi-step process that uses the objectIntersect function like crazy, and with so much involved, I apologize if there are any errors from where I retyped it.
Here are the general steps:
- Make a useful 3D object constructor using createSprite() so you can store your object properties in an object-oriented way.
- Measure the your models to get the upper-front-left and lower-bottom-right corners.
- Ram your objects into each other.
- Adjust the objects' positions to find the point of impact.
- Find the faces of the bounding box that collided.
- Adjust the movement based on the remaining velocity projected onto the face.
- Make sure the movement didn't mess up other collisions
vector homogeneous_scale = {1,1,1} function createObject(model_name, pos, vel, sca, rot, rvel) // model_name is the Media name of the model. // pos is the initial position (vector). // vel is the initial velocity (vector). // sca is the scale vector of the object. // rot is the rotation vector of the object. // rvel is the rotational velocity (vector). // sprites allow for arbitrary "object-oriented" attributes. // we won't be using any actual sprite features. var obj = createSprite() var handle = loadModel(model_name) // store the data obj.model_name = model_name obj.model = handle obj.pos = pos obj.vel = vel obj.rvel = rvel obj.sca = sca // initially place the object at the origin for measurement obj.obj = placeObject(handle, {}, obj.sca) // store the dimensions of the model here var dimensions = measurement(obj.obj) obj.dim = dimensions // set the real position now that measurement is done setObjectPos(obj.obj, obj.pos) return obj // define the local directions vector forward = {0,0,1} vector backward = {0,0,-1} vector leftward = {1,0,0} vector rightward = {-1,0,0} vector upward = {0,1,0} vector downward = {0,-1,0} // the order of the vectors in directions is _very_ important // for measurement() and collision_faces()! var directions = [leftward, rightward, upward, downward, forward, backward] int directions_length = len(directions) function measurement(object) // our upper and lower dimensions, respectively vector dimensionsA = {} vector dimensionsB = {} // the flattened cube we use to measure things var ruler = placeObject(cube, {}, {1,1,.00001}) for i=0 to directions_length loop // the final position of the ruler has nonzero value on only one axis vector pos = measure_direction(object, ruler, directions[i]) // so add that position to the respective dimension if i%2 then dimensionsB += pos else dimensionsA += pos endif repeat removeObject(ruler) // you cannot skip out on making it a variable to return: // it will crash if you return the array via "return [dimensionsA, dimensionsB]". var dims = [dimensionsA, dimensionsB] return dims function measure_direction(object, ruler, direction) var ruler_pos = direction/2 // start at the origin, where there is definitely a collision setObjectPos(ruler, {}) // set the ruler's flat side to be parallel to the face it will measure if direction.y then // I think the terminology is something like: avoid gimbal lock objectPointAt(ruler, forward) rotateObject(ruler, {1,0,0}, 90) else objectPointAt(ruler, direction) endif // move the ruler out in large steps until it doesn't touch the ruler while objectIntersect(object, ruler) loop ruler_pos *= 2 setObjectPos(ruler, ruler_pos) repeat //refine the ruler position // start halfway between the point of no touching and definitely touching var t = 0.5 // change t by dt/2, but go up or down based on whether there was a collision var dt = 0.5 // the refined times of no intersection and intersection var no_intersect_t = 0 var intersect_t = 1 vector cpos = {} vector origin = {} // "binary search" for a very near the edge touching point while dt > 0.005 loop cpos = lerp(ruler_pos, origin, t) setObjectPos(ruler, cpos) // it is kind of cool to watch it measure stuff. // uncomment to watch /*drawObjects() printAt(0,0, direction, cpos) update() sleep(0.01)*/ if objectIntersect(ruler, object) then intersect_t = t t -= dt/2 else no_intersect_t = t t += dt/2 endif repeat // get the last touching intersection t = intersect_t cpos = lerp(ruler_pos, origin, t) return cpos function sign(value) int sign = 0 if value > 0 then sign = 1 else if value < 0 then sign = -1 endif endif return sign function updateObj(obj, dt) // here, dt is a variable defined in the update loop // dt = deltaTime() setOld(obj) var rot = obj.rot+obj.rvel*dt for r=0 to 3 loop // avoid integer overflow (which would take a LOT of rotation) if abs(rot[r]) > 360 then rot[r] -= 360*sign(rot[r]) endif repeat obj.rot = rot // convert the velocity to world coordinates vector world_vel = rotate(obj.vel, obj.rot) obj.pos = obj.pos + world_vel*dt setObjectPos(obj.obj, obj.pos) // make the object face forward in the world vector world_dir = rotate(forward, obj.rot) // probably wouldn't like looking straight up // because the direction is normalized (and rotated normalized vectors are normalized), // we only need to assess if y is 1 if abs(world_dir.y) == 1 then objectPointAt(obj.obj, obj.pos+forward) rotateObject(obj.obj, {1,0,0}, sign(world_dir.y)*90) else objectPointAt(obj.obj, obj.pos+world_dir) endif return void function setOld(obj1) // set things up for collision detection obj1.opos = obj1.pos obj1.ovel = obj1.vel obj1.osca = obj1.sca obj1.orot = obj1.rot obj1.orvel = obj1.rvel return void function setOrbitCam(degrees, dist, height, lookat) var cam = lookat+{cos(degrees)*dist, height, sin(degrees*dist)} setCamera(cam, lookat) return void function collision_faces(objs) // given that a collision happened between // objs[0] and objs[1], AND you narrowed down the point of impact, // find which faces are involved var all_faces = [] for i=0 to 2 loop // from pos, move the object over by each dimension separately var inst = objs[i] var obj = inst.obj vector pos = inst.pos vector sca = inst.sca var dim = inst.dim vector rot = inst.rot // there's a face to check in every direction for d=0 to directions_length loop // upper or lower corner (0 or 1) int idx2 = int(d%2) // what is the index for the positive direction? // i.e., right, up, or forward // obj.dim has negative values built-in to lower corner, // so don't negate the negative by using a negative direction. int idx3 = d-idx2 // x,y,z are 0,1,2 int idx = idx3/2 setObjectPos(obj, pos+rotate(directions[idx3]*dim[idx2], rot)) vector sca2 = sca // flatten it somewhat sca2[idx] = 0.02 setObjectScale(obj, sca2) // see if the "face" (a flattened, shifted model) collides faces[d] = objectIntersect(objs[0].obj, objs[1].obj) repeat // reset the features setObjectPos(obj, pos) setObjectScale(obj, sca) // store this object's face collision data all_faces[i] = faces repeat return all_faces function interpolateObjectRotation(obj, t) obj1.crot = lerp(obj1.orot, obj1.rot, t) var world_dir = rotate(forward, obj1.crot) if abs(world_dir.y) == 1 then objectPointAt(obj.obj, obj.cpos+forward) rotateObject(obj.obj, {1,0,0}, sign(world_dir.y)*90) else objectPointAt(obj.obj, obj.cpos+world_dir) endif return void function project(vec, plane_normal) l = length(plane_normal) return cross(plane_normal, cross(vec, plane_normal/l)/l) function place_objects(objs, t, final) // helper function for point of impact for i=0 to 2 loop objs[i].cpos = lerp(objs[i].opos, objs[i].pos, t) setObjectPos(objs[i].obj, objs[i].cpos) interpolateObjectRotation(objs[i],t) var sca = lerp(objs[i].osca, objs[i].sca, t) setObjectScale(objs[i].obj, sca) if final then objs[i].pos = objs[i].cpos objs[i].rot = objs[i].crot objs[i].rvel = (objs[i].crot-objs[i].orot)*t objs[i].sca = objs[i].sca endif repeat return void function separate(objs) // midpoint vector m = (objs[0].pos+objs[1].pos)/2 while objectIntersect(objs[0].obj, objs[1].obj) loop // even split if length(objs[0].ovel) and length(objs[1].obj) then objs[0].pos = m+ (objs[0].pos-m)*1.1 objs[1].pos = m+ (objs[1].pos-m)*1.1 else if length(objs[0].ovel) and !length(objs[1].obj) then objs[0].pos = m+ (objs[0].pos-m)*1.1 else if !length(objs[0].ovel) and length(objs[1].obj) then objs[1].pos = m+ (objs[1].pos-m)*1.1 else // neither moved if !length(objs[0].ovel) and !length(objs[1].obj) then objs[0].pos = m+ (objs[0].pos-m)*1.1 objs[1].pos = m+ (objs[1].pos-m)*1.1 endif endif endif endif setObjectPos(objs[0].obj, objs[0].pos) setObjectPos(objs[1].obj, objs[1].pos) repeat return void function pointOfImpact(obj1, obj2, loops, wdt) // this function assumes either that they started apart // or that one/both of them is/are movable float t = 0.0 // were either of them moving? if length(obj1.ovel) or length(obj2.ovel) then t = 0.5 var objs = [obj1, obj2] float dt = 0.5 float intersect_t = 1 float no_intersect_t = 0 while dt > 0.05 loop for i=0 to 2 loop objs[i].cpos = lerp(objs[i].opos, objs[i].pos, t) setObjectPos(objs[i].obj, objs[i].cpos) setObjectScale(objs[i].obj, lerp(objs[i].osca, objs[i].sca, t)) interpolateObjectRotation(objs[i], t) repeat // finding the point of impact uses the // same general formula as measuring the objects if objectIntersect(obj1.obj, obj2.obj) then intersect_t = t t -= dt else no_intersect_t = t t += dt endif dt/=2 repeat t=intersect_t place_objects(objs, t, false) var faces = collision_faces(objs) t = no_intersect_t place_objects(objs, t, false) // if they are still together, then separate them // likely caused by objects starting together if objectIntersect(objs[0].obj, objs[1].obj) then separate(objs) objs[0].cpos = objs[0].pos objs[1].cpos = objs[1].pos endif // try to rotate anyway var tmps = [] var bools = [0,0] for i=0 to 2 loop interpolateObjectRotation(objs[i], 1) tmps[i] = objs[i].crot if !objectIntersect(objs[0].obj, objs[1].obj) then bools[i] = true endif repeat place_objects(objs, t, true) for i=0 to 2 loop if bools[i] then objs[i].rot = tmps[i] endif repeat if loops == 1 then for i=0 to 2 loop if length(objs[i].vel)!=0 then // get the remaining velocity in world coordinates vector vel = rotate(objs[i].vel*(wdt-wdt*t), objs[i].rot) vector projected_move = {} var other = !i var other_faces = faces[other] var count = 0 for f=0 to directions_length loop // if they hit the face if other_faces[f] then // project onto normal from world coordinates direction vector wd = rotate(directions[f], objs[other].rot) vector p = project(vel, wd) // see if it causes a collision (measurements and floats may be imprecise) // plus, edge cases may occur for the face detection setObjectPos(objs[i].obj, objs[i].pos+(p+projected_move)/(count+1)) if !objectIntersect(objs[0].obj, objs[1].obj) then projected_move += p count += 1 endif endif repeat if count > 0 then // move by average projected velocity objs[i].pos = objs[i].pos + projected_move/count endif setObjectPos(objs[i].obj, objs[i].pos) endif // the velocity is the amount it moved in the frame given the // amount of time that passed, but transformed to local coordinates objs[i].vel = unrotate((objs[i].pos)/wdt, objs[i].rot) repeat endif // if they are still together, then separate them if objectIntersect(objs[0].obj, objs[1].obj) then separate(objs) endif endif return t function matrix_transform(vec, mat) // matrices are defined here as an array of row vectors // i.e., [{1,0,0}, {0,1,0}, {0,0,1}] for a 3x3 identity vector v = {} for i=0 to len(mat) loop v[i] = dot(mat[i], vec) repeat return v function rotation_matX(rx) // make a rotation matrix for the x axis var xc = cos(rx) var xs = sin(rx) var rmx = [{1,0,0}, {0,xc,-xs}, {0, xs, xc}] return rmx function rotation_matY(ry) // make a rotation matrix for the y axis var yc = cos(ry) var ys = cos(ry) var rmy = [{yc,0,ys}, {0,1,0}, {-ys,0,yc}] return rmy function rotation_matZ(rz) // make a rotation matrix for the z axis var zc = cos(rz) var zs = sin(rz) var rmz = [{zc, -zs, 0}, {zs, zc, 0}, {0,0,1}] return rmz function rotateX(direction, degrees) vector new_dir = matrix_transform(direction, rotation_matX(degrees)) return new_dir function rotateZ(direction, degrees) vector new_dir = matrix_transform(direction, rotation_matZ(degrees)) return new_dir function rotateY(direction, degrees) // best one! rotate around Up vector new_dir = matrix_transform(direction, rotation_matY(degrees)) return new_dir function rotate(direction, rot) // direction is a normal vector that specifies // facing (i.e., to use with objectPointAt) // rot is a vector of x, y, and z values var new_dir = direction // order of operations is important, I just like this order new_dir = rotateY(new_dir, rot.y) new_dir = rotateZ(new_dir, rot.z) new_dir = rotateX(new_dir, rot.x) return new_dir function unrotate(direction, rot) // go back to local coordinates var new_dir = direction // order of operations is important, and it is reversed for unrotate new_dir = rotateX(new_dir, -1*rot.x) new_dir = rotateZ(new_dir, -1*rot.z) new_dir = rotateY(new_dir, -1*rot.y) return new_dir // now let's get an example: car = createObject("Quaternius/NormalCar1", {0, 5.5, 0}, {}, homogeneous_scale, {}, {}) car2 = createObject("Quaternius/NormalCar2", {-1, 1.5, 0}, {}, homogeneous_scale, {0,45,0}, {}) street = createObject("Quaternius/Street_Empty", {}, {}, {10,1,10}, {}, {}) height = 10 angle = 0 showObjectBounds(true, white, 1) setOrbitCam(angle, 10, height, {}) light = pointShadowLight({0,4,2}, white, 100, 100) x = 0 float t = 0 var cars = [car, car2] var collides = [car, car2, street] loop dt = deltaTime() clear() c = controls(0) setLightPos(light, {x,4,2}) if c.left then x = x-0.2 endif if c.right then x = x+0.2 endif if c.a then vector v = car.vel // increase speed in local coordinates! forward is z v.z += 0.1 car.vel = v else if c.b then vector v = car.vel v.z -= 0.1 car.vel = v endif endif // rotate the car in the expected direction // with the left stick (if they are moving) vector r = car.rvel r.y = -c.lx*50*sign(car.vel.z) car.rvel = r // slow down and gravity for i = 0 to len(cars) loop var carA = cars[i] vector v = carA.vel if length(carA.vel) > 0 then // gradual slowing v -= normalize(v)*dt carA.vel = v endif // gravity test, again, in local coordinates v.y -= .1 carA.vel = v repeat for i=0 to len(collides) loop updateObj(collides[i]) repeat angle += c.rx height += c.ry setOrbitCam(angle, 10, height, {}) int collision = true int loops = 0 // adjusting object positions due to collisions can cause more // collisions with other objects, // so keep going until there are no collisions. while collision loop collision = false loops += 1 for i=0 to len(collides) loop var obj1 = collides[i] for j=i+1 to len(collides) loop var obj2 = collides[j] if objectIntersect(obj1.obj, obj2.obj) then collision = true pointOfImpact(obj1, obj2, loops, dt) showObjectBounds(true, red, 1) endif repeat repeat repeat drawObjects() update() repeat
The project itself doesn't has the same comments, and has slight differences based on what I plan to implement (or things I thought could be slightly better as I was retyping): 5E1R7MNDN5
Thanks for reading, and make something great.
Jason -
Wow, this is quite a lot to take in! I’ve had a similar thing on my to do list for quite some time, and it looks like you’ve done all I was hoping to do and more! I won’t be able to drag-and-drop this but I’ll be studying it for sure.
Is there any advantage to using sprites as the base rather than a struct?
-
I had memory leaks the first time I implemented something else with structs (it was when I did sprite splicing to add to the possible animations). I don't know if this was the problem exactly, but I think that structs were copied into each function instead of being passed by reference (without being deleted). FUZE4 silently crashed without an error (which isn't something it does if I load too many models), so it is a bit of conjecture on my part.
Regardless, structs are limited to their preset attributes. I may try to extend it to allow optional spherical boundaries, which will add attributes that aren't always present (i.e., radius).
Let me know if you have any questions, and thanks for your interest.
Jason -
Do you know about the ref keyword?
-
I definitely tried to use it for the sprite-splicing related project, but since that was a while ago (and I reimplemented it with sprites too), I can't just post it as an example case. I'll try to make a short program that makes it crash that way with structs later.
-
I figured you’d already know about it. I just thought I’d mention it just in case.
I’m excited to have a look at all of this here, especially curious about the rotational velocity stuff, which I had decided was ‘maybe sometime down the road, but not now’.
I will definitely write back here if I have any questions. In the meantime, thanks again! :)
-
Huh, I had all of the struct code for the sprites conveniently commented out because of the problem I'd had, but when I reintroduced them (and waited for a crash) nothing bad happened. Maybe I didn't wait long enough, or maybe there was a patch?
At any rate, I rescind my comment about the struct crash, for now xD.
Sprite splicing:
With structs N1WGDMNDN5
Without structs NSYMENNDN5 -
@Chronos You have shared these programs but they are currently only visible to your Switch Friends. If you want them to be downloadable by code they have to be submitted for approval.Take a look here: https://fuzearena.com/forum/topic/725/new-community-sharing-on-fuze4.
-
Done! Thanks for the tip.
-
Hiya! Just got around to downloading this, and I can tell straight away that it’s going to make my kids happy. They’re always shocked and offended when in my game, the player character can just walk straight through buildings as if they’re not there. Looking forward to having a good dive into this, thanks again :)
-
thanks soo much for this i use it to know objects dimensions for my projects well i use it to find the values i need, i don't use the code in my project