From 09dc9470ae5b85564fb099219b2a9a7b2e0bf54a Mon Sep 17 00:00:00 2001 From: kichikuou Date: Sat, 21 Sep 2024 10:31:30 +0900 Subject: [PATCH] TapirEngine: Proper collision detection Collision detection is based on a mesh named "collision" in the map's 3D model. The "collision" mesh specifies the area in which the character can move in the xz plane, and the y coordinate represents the height of the map at that point. Now cglm 0.9.2 is required, for 2D AABB operations. --- include/reign.h | 1 + meson.build | 2 +- src/3d/3d_internal.h | 18 +++- src/3d/collision.c | 201 ++++++++++++++++++++++++++++++++++++------ src/3d/model.c | 11 ++- src/3d/reign.c | 22 +++-- src/hll/ReignEngine.c | 6 +- subprojects/cglm.wrap | 2 +- 8 files changed, 217 insertions(+), 46 deletions(-) diff --git a/include/reign.h b/include/reign.h index 13cbaa9..4c0e154 100644 --- a/include/reign.h +++ b/include/reign.h @@ -212,6 +212,7 @@ bool RE_instance_set_vertex_pos(struct RE_instance *instance, int index, float x int RE_instance_get_bone_index(struct RE_instance *instance, const char *name); bool RE_instance_trans_local_pos_to_world_pos_by_bone(struct RE_instance *instance, int bone, vec3 offset, vec3 out); float RE_instance_calc_height(struct RE_instance *instance, float x, float z); +float RE_instance_calc_2d_detection_height(struct RE_instance *instance, float x, float z); bool RE_instance_calc_2d_detection(struct RE_instance *instance, float x0, float y0, float z0, float x1, float y1, float z1, float *x2, float *y2, float *z2, float radius); bool RE_instance_set_debug_draw_shadow_volume(struct RE_instance *instance, bool draw); diff --git a/meson.build b/meson.build index a59f42e..70c13fa 100644 --- a/meson.build +++ b/meson.build @@ -21,7 +21,7 @@ ffi = dependency('libffi', static : static_libs) tj = dependency('libturbojpeg', static : static_libs) webp = dependency('libwebp', static : static_libs) png = dependency('libpng', static : static_libs) -cglm = dependency('cglm', version : '>=0.8.3', fallback : ['cglm', 'cglm_dep']) +cglm = dependency('cglm', version : '>=0.9.2', fallback : ['cglm', 'cglm_dep']) sndfile = dependency('sndfile', static : static_libs) avcodec = dependency('libavcodec', version : '>=59.37.100', required: false, static : static_libs) diff --git a/src/3d/3d_internal.h b/src/3d/3d_internal.h index 7a90dda..f0d8192 100644 --- a/src/3d/3d_internal.h +++ b/src/3d/3d_internal.h @@ -515,15 +515,25 @@ struct amt_material *amt_find_material(struct amt *amt, const char *name); struct collider { struct collider_triangle *triangles; uint32_t nr_triangles; + struct collider_edge *edges; // boundary edges + uint32_t nr_edges; }; struct collider_triangle { - vec3 vertices[3]; - vec3 aabb[2]; + vec2 vertices[3]; // xz coordinates + vec2 aabb[2]; + vec2 slope; + float intercept; }; -struct collider *collider_create(struct pol *pol); +struct collider_edge { + vec2 vertices[2]; // xz coordinates + vec2 aabb[2]; +}; + +struct collider *collider_create(struct pol_mesh *mesh); void collider_free(struct collider *collider); -bool check_collision(struct collider *collider, vec3 p0, vec3 p1, float radius, vec3 out); +bool collider_height(struct collider *collider, vec2 xz, float *h_out); +bool check_collision(struct collider *collider, vec2 p0, vec2 p1, float radius, vec2 out); #endif /* SYSTEM4_3D_3D_INTERNAL_H */ diff --git a/src/3d/collision.c b/src/3d/collision.c index 5443ef2..91ecfd0 100644 --- a/src/3d/collision.c +++ b/src/3d/collision.c @@ -14,6 +14,7 @@ * along with this program; if not, see . */ +#include #include #include @@ -21,52 +22,194 @@ #include "3d_internal.h" -struct collider *collider_create(struct pol *pol) +void init_triangles(struct collider *collider, struct pol_mesh *mesh) { - struct collider *collider = xcalloc(1, sizeof(struct collider)); - - int nr_triangles = 0; - for (int i = 0; i < pol->nr_meshes; i++) { - nr_triangles += pol->meshes[i]->nr_triangles; - } - - collider->nr_triangles = nr_triangles; - collider->triangles = xcalloc(nr_triangles, sizeof(struct collider_triangle)); + collider->nr_triangles = mesh->nr_triangles; + collider->triangles = xcalloc(mesh->nr_triangles, sizeof(struct collider_triangle)); struct collider_triangle *t = collider->triangles; - for (int mesh_i = 0; mesh_i < pol->nr_meshes; mesh_i++) { - struct pol_mesh *mesh = pol->meshes[mesh_i]; - for (int tri_i = 0; tri_i < mesh->nr_triangles; tri_i++) { - glm_aabb_invalidate(t->aabb); - for (int i = 0; i < 3; i++) { - glm_vec3_copy(mesh->vertices[mesh->triangles[tri_i].vert_index[i]].pos, t->vertices[i]); - glm_vec3_minv(t->vertices[i], t->aabb[0], t->aabb[0]); - glm_vec3_maxv(t->vertices[i], t->aabb[1], t->aabb[1]); + for (int tri_i = 0; tri_i < mesh->nr_triangles; tri_i++, t++) { + vec3 vertices[3]; + glm_aabb2d_invalidate(t->aabb); + for (int i = 0; i < 3; i++) { + glm_vec3_copy(mesh->vertices[mesh->triangles[tri_i].vert_index[i]].pos, vertices[i]); + t->vertices[i][0] = vertices[i][0]; + t->vertices[i][1] = vertices[i][2]; + glm_vec2_minv(t->vertices[i], t->aabb[0], t->aabb[0]); + glm_vec2_maxv(t->vertices[i], t->aabb[1], t->aabb[1]); + } + // Do some precomputation so that we can calculate height(x, z) quickly. + vec3 v1, v2, normal; + glm_vec3_sub(vertices[1], vertices[0], v1); + glm_vec3_sub(vertices[2], vertices[0], v2); + glm_vec3_cross(v1, v2, normal); + t->slope[0] = -normal[0] / normal[1]; + t->slope[1] = -normal[2] / normal[1]; + t->intercept = glm_vec3_dot(normal, vertices[0]) / normal[1]; + } +} + +void init_edges(struct collider *collider, struct pol_mesh *mesh) +{ + // Find boundary edges, i.e., edges that belong to only one triangle. + const int nv = mesh->nr_vertices; + const int bv_size = (nv * nv + 31) / 32; + uint32_t *bv = xcalloc(bv_size, 4); + int nr_edges = 0; + for (int i = 0; i < mesh->nr_triangles; i++) { + for (int j = 0; j < 3; j++) { + int v1 = mesh->triangles[i].vert_index[j]; + int v2 = mesh->triangles[i].vert_index[(j + 1) % 3]; + int b = v2 * nv + v1; + if (bv[b >> 5] & 1U << (b & 31)) { + bv[b >> 5] &= ~(1U << (b & 31)); + nr_edges--; + } else { + b = v1 * nv + v2; + bv[b >> 5] |= 1U << (b & 31); + nr_edges++; } - t++; } } + collider->nr_edges = nr_edges; + collider->edges = xcalloc(nr_edges, sizeof(struct collider_edge)); + struct collider_edge* edge = collider->edges; + for (int i = 0; i < bv_size; i++) { + if (!bv[i]) continue; + for (int j = 0; j < 32; j++) { + if (!(bv[i] & 1U << j)) continue; + int b = i << 5 | j; + int v1 = b / nv; + int v2 = b % nv; + edge->vertices[0][0] = mesh->vertices[v1].pos[0]; + edge->vertices[0][1] = mesh->vertices[v1].pos[2]; + edge->vertices[1][0] = mesh->vertices[v2].pos[0]; + edge->vertices[1][1] = mesh->vertices[v2].pos[2]; + glm_vec2_minv(edge->vertices[0], edge->vertices[1], edge->aabb[0]); + glm_vec2_maxv(edge->vertices[0], edge->vertices[1], edge->aabb[1]); + edge++; + } + } + assert(edge == collider->edges + nr_edges); + free(bv); +} +struct collider *collider_create(struct pol_mesh *mesh) +{ + struct collider *collider = xcalloc(1, sizeof(struct collider)); + init_triangles(collider, mesh); + init_edges(collider, mesh); + NOTICE("collider: %d triangles %d edges", collider->nr_triangles, collider->nr_edges); return collider; } void collider_free(struct collider *collider) { free(collider->triangles); + free(collider->edges); free(collider); } -// When a sphere of radius `radius` moves from `p0` to `p1`, return the point -// where it collides with `collider` in `out`. If no collision is detected, -// return `p1` in `out`. -bool check_collision(struct collider *collider, vec3 p0, vec3 p1, float radius, vec3 out) +static bool in_triangle(struct collider_triangle* t, vec2 xz) { - // XXX: Very rough collision detection; just check if p1 is inside any triangle's AABB. - for (int i = 0; i < collider->nr_triangles; i++) { - if (glm_aabb_point(collider->triangles[i].aabb, p1)) { - glm_vec3_copy(p0, out); - return true; + for (int i = 0; i < 3; i++) { + vec2 a, b; + glm_vec2_sub(t->vertices[(i + 1) % 3], t->vertices[i], a); + glm_vec2_sub(xz, t->vertices[i], b); + if (glm_vec2_cross(a, b) > 0.f) + return false; + } + return true; +} + +static struct collider_triangle* find_triangle(struct collider *collider, vec2 xz) +{ + struct collider_triangle* t = collider->triangles; + for (; t < collider->triangles + collider->nr_triangles; t++) { + if (!glm_aabb2d_point(t->aabb, xz)) + continue; + if (in_triangle(t, xz)) + return t; + } + return NULL; +} + +bool collider_height(struct collider *collider, vec2 xz, float *h_out) +{ + struct collider_triangle* t = find_triangle(collider, xz); + if (!t) + return false; + *h_out = glm_vec2_dot(t->slope, xz) + t->intercept; + return true; +} + +static float distance_point_to_edge(vec2 p, struct collider_edge *e, vec2 closest_point_out) +{ + vec2 v, w; + glm_vec2_sub(e->vertices[1], e->vertices[0], v); + glm_vec2_sub(p, e->vertices[0], w); + + float c1 = glm_vec2_dot(w, v); + if (c1 < 0.f) { + glm_vec2_copy(e->vertices[0], closest_point_out); + return glm_vec2_distance(p, e->vertices[0]); + } + float c2 = glm_vec2_norm2(v); + if (c2 <= c1) { + glm_vec2_copy(e->vertices[1], closest_point_out); + return glm_vec2_distance(p, e->vertices[1]); + } + float t = c1 / c2; + glm_vec2_copy(e->vertices[0], closest_point_out); + glm_vec2_muladds(v, t, closest_point_out); + return glm_vec2_distance(p, closest_point_out); +} + +bool check_collision(struct collider *collider, vec2 p0, vec2 p1, float radius, vec2 out) +{ + if (find_triangle(collider, p1)) { + glm_vec2_copy(p1, out); + } else { + if (!find_triangle(collider, p0)) + return false; + // Find a point on the "collision" mesh, using binary search. + vec2 good, bad; + glm_vec2_copy(p0, good); + glm_vec2_copy(p1, bad); + float w = glm_vec2_distance(good, bad); + while (w > radius) { + vec2 mid; + glm_vec2_center(good, bad, mid); + if (find_triangle(collider, mid)) { + glm_vec2_copy(mid, good); + } else { + glm_vec2_copy(mid, bad); + } + w /= 2.f; + } + glm_vec2_copy(good, out); + } + + // Ensure that `out` is at least `radius` away from any boundary edge. + vec2 aabb[2]; + glm_vec2_subs(out, radius, aabb[0]); + glm_vec2_adds(out, radius, aabb[1]); + for (int i = 0; i < collider->nr_edges; i++) { + struct collider_edge *e = &collider->edges[i]; + if (!glm_aabb2d_aabb(e->aabb, aabb)) + continue; + vec2 q; + float distance = distance_point_to_edge(out, e, q); + if (distance < radius) { + // out = q + normalize(out - q) * radius + vec2 v; + glm_vec2_sub(out, q, v); + glm_vec2_normalize(v); + glm_vec2_copy(q, out); + glm_vec2_muladds(v, radius, out); + // update AABB + glm_vec2_subs(out, radius, aabb[0]); + glm_vec2_adds(out, radius, aabb[1]); } } - glm_vec3_copy(p1, out); return true; } diff --git a/src/3d/model.c b/src/3d/model.c index 46c4c7a..48d90ef 100644 --- a/src/3d/model.c +++ b/src/3d/model.c @@ -463,6 +463,13 @@ struct model *model_load(struct archive *aar, const char *path) for (uint32_t i = 0; i < pol->nr_meshes; i++) { if (!pol->meshes[i]) continue; + if (!strcmp(pol->meshes[i]->name, "collision")) { + if (model->collider) + WARNING("multiple collision meshes"); + else + model->collider = collider_create(pol->meshes[i]); + continue; + } struct pol_material_group *mg = &pol->materials[pol->meshes[i]->material]; int m_off = material_offsets[pol->meshes[i]->material]; if (mg->nr_children == 0) { @@ -474,10 +481,6 @@ struct model *model_load(struct archive *aar, const char *path) } } - // Collision detection is only required for maps. - if (strstr(path, "Map\\")) - model->collider = collider_create(pol); - pol_compute_aabb(pol, model->aabb); free(material_offsets); diff --git a/src/3d/reign.c b/src/3d/reign.c index 7a953d3..94fe5f8 100644 --- a/src/3d/reign.c +++ b/src/3d/reign.c @@ -549,19 +549,29 @@ float RE_instance_calc_height(struct RE_instance *instance, float x, float z) return cnt ? total / cnt : 0.0f; } +float RE_instance_calc_2d_detection_height(struct RE_instance *instance, float x, float z) +{ + if (!instance || !instance->model->collider) + return false; + vec2 xz = { x, -z }; + float h; + if (collider_height(instance->model->collider, xz, &h)) + return h; + return 0.f; +} + bool RE_instance_calc_2d_detection(struct RE_instance *instance, float x0, float y0, float z0, float x1, float y1, float z1, float *x2, float *y2, float *z2, float radius) { if (!instance || !instance->model->collider) return false; - vec3 p0 = { x0, y0, -z0 }; - vec3 p1 = { x1, y1, -z1 }; - vec3 p2; + vec2 p0 = { x0, -z0 }; + vec2 p1 = { x1, -z1 }; + vec2 p2; if (!check_collision(instance->model->collider, p0, p1, radius, p2)) return false; *x2 = p2[0]; - *y2 = p2[1]; - *z2 = -p2[2]; - return true; + *z2 = -p2[1]; + return collider_height(instance->model->collider, p2, y2); } bool RE_instance_set_debug_draw_shadow_volume(struct RE_instance *inst, bool draw) diff --git a/src/hll/ReignEngine.c b/src/hll/ReignEngine.c index 4f93a35..b765ff4 100644 --- a/src/hll/ReignEngine.c +++ b/src/hll/ReignEngine.c @@ -1825,7 +1825,11 @@ static int TapirEngine_CreatePlugin(void) HLL_WARN_UNIMPLEMENTED(false, bool, TapirEngine, SetInstanceDrawParam, int plugin_number, int instance_number, int draw_param, int value); //bool TapirEngine_GetInstanceDrawParam(int PluginNumber, int InstanceNumber, int DrawParam, int *Value); -HLL_WARN_UNIMPLEMENTED(0.0f, float, TapirEngine, CalcInstance2DDetectionHeight, int plugin_number, int instance_number, float x, float z); + +static float TapirEngine_CalcInstance2DDetectionHeight(int plugin, int instance, float x, float z) +{ + return RE_instance_calc_2d_detection_height(get_instance(plugin, instance), x, z); +} static bool TapirEngine_CalcInstance2DDetection(int plugin, int instance, float x0, float y0, float z0, float x1, float y1, float z1, float *x2, float *y2, float *z2, float radius) { diff --git a/subprojects/cglm.wrap b/subprojects/cglm.wrap index 964ff79..77212e5 100644 --- a/subprojects/cglm.wrap +++ b/subprojects/cglm.wrap @@ -1,3 +1,3 @@ [wrap-git] url = https://github.com/recp/cglm.git -revision = v0.8.5 +revision = v0.9.2