diff --git a/src/library/gl.c b/src/library/gl.c index 09b8b0d..bb04ae9 100644 --- a/src/library/gl.c +++ b/src/library/gl.c @@ -86,6 +86,8 @@ static size_t _bolt_gl_plugin_drawelements_vertex3d_atlas_meta(size_t index, voi static void _bolt_gl_plugin_drawelements_vertex3d_meta_xywh(size_t meta, void* userdata, int32_t* out); static void _bolt_gl_plugin_drawelements_vertex3d_uv(size_t index, void* userdata, double* out); static void _bolt_gl_plugin_drawelements_vertex3d_colour(size_t index, void* userdata, double* out); +static uint8_t _bolt_gl_plugin_drawelements_vertex3d_boneid(size_t index, void* userdata); +static void _bolt_gl_plugin_bone_transform(uint8_t bone_id, void* userdata, double* out); static void _bolt_gl_plugin_matrix3d_toworldspace(int x, int y, int z, void* userdata, double* out); static void _bolt_gl_plugin_matrix3d_toscreenspace(int x, int y, int z, void* userdata, double* out); static void _bolt_gl_plugin_matrix3d_worldpos(void* userdata, double* out); @@ -606,6 +608,7 @@ static GLuint _bolt_glCreateProgram() { program->loc_uTextureAtlasSettings = -1; program->loc_uAtlasMeta = -1; program->loc_uModelMatrix = -1; + program->loc_uBoneTransforms = -1; program->loc_uGridSize = -1; program->loc_uVertexScale = -1; program->loc_sSceneHDRTex = -1; @@ -666,6 +669,7 @@ static void _bolt_glLinkProgram(GLuint program) { const GLint loc_uVertexScale = gl.GetUniformLocation(program, "uVertexScale"); p->loc_sSceneHDRTex = gl.GetUniformLocation(program, "sSceneHDRTex"); p->loc_sSourceTex = gl.GetUniformLocation(program, "sSourceTex"); + p->loc_uBoneTransforms = gl.GetUniformLocation(program, "uBoneTransforms"); const GLchar* view_var_names[] = {"uCameraPosition", "uViewProjMatrix"}; const GLuint block_index_ViewTransforms = gl.GetUniformBlockIndex(program, "ViewTransforms"); @@ -1375,12 +1379,15 @@ void _bolt_gl_onDrawElements(GLenum mode, GLsizei count, GLenum type, const void struct Render3D render; render.vertex_count = count; + render.is_animated = c->bound_program->loc_uBoneTransforms != -1; render.vertex_functions.userdata = &vertex_userdata; render.vertex_functions.xyz = _bolt_gl_plugin_drawelements_vertex3d_xyz; render.vertex_functions.atlas_meta = _bolt_gl_plugin_drawelements_vertex3d_atlas_meta; render.vertex_functions.atlas_xywh = _bolt_gl_plugin_drawelements_vertex3d_meta_xywh; render.vertex_functions.uv = _bolt_gl_plugin_drawelements_vertex3d_uv; render.vertex_functions.colour = _bolt_gl_plugin_drawelements_vertex3d_colour; + render.vertex_functions.bone_id = _bolt_gl_plugin_drawelements_vertex3d_boneid; + render.vertex_functions.bone_transform = _bolt_gl_plugin_bone_transform; render.texture_functions.userdata = &tex_userdata; render.texture_functions.id = _bolt_gl_plugin_texture_id; render.texture_functions.size = _bolt_gl_plugin_texture_size; @@ -1588,6 +1595,42 @@ static void _bolt_gl_plugin_drawelements_vertex3d_colour(size_t index, void* use out[3] = (double)colour[0]; } +static uint8_t _bolt_gl_plugin_drawelements_vertex3d_boneid(size_t index, void* userdata) { + struct GLPluginDrawElementsVertex3DUserData* data = userdata; + uint32_t ret[4]; + if (!_bolt_get_attr_binding_int(data->c, data->xyz_bone, data->indices[index], 4, (int32_t*)&ret)) { + float retf[4]; + _bolt_get_attr_binding(data->c, data->xyz_bone, data->indices[index], 4, retf); + return (uint8_t)(uint32_t)(int32_t)roundf(retf[3]); + } + return (uint8_t)(ret[3]); +} + +static void _bolt_gl_plugin_bone_transform(uint8_t bone_id, void* userdata, double* out) { + struct GLContext* c = _bolt_context(); + const GLint uniform_loc = c->bound_program->loc_uBoneTransforms + (bone_id * 3); + GLfloat f[4]; + gl.GetUniformfv(c->bound_program->id, uniform_loc, f); + out[0] = (double)f[0]; + out[1] = (double)f[1]; + out[2] = (double)f[2]; + out[3] = 0.0; + out[4] = (double)f[3]; + gl.GetUniformfv(c->bound_program->id, uniform_loc + 1, f); + out[5] = (double)f[0]; + out[6] = (double)f[1]; + out[7] = 0.0; + out[8] = (double)f[2]; + out[9] = (double)f[3]; + gl.GetUniformfv(c->bound_program->id, uniform_loc + 2, f); + out[10] = (double)f[0]; + out[11] = 0.0; + out[12] = (double)f[1]; + out[13] = (double)f[2]; + out[14] = (double)f[3]; + out[15] = 1.0; +} + static void _bolt_gl_plugin_matrix3d_toworldspace(int x, int y, int z, void* userdata, double* out) { const struct GLPlugin3DMatrixUserData* data = userdata; const double dx = (double)x; diff --git a/src/library/gl.h b/src/library/gl.h index 66c2e10..ef54cf1 100644 --- a/src/library/gl.h +++ b/src/library/gl.h @@ -193,6 +193,7 @@ struct GLProgram { GLint loc_uTextureAtlasSettings; GLint loc_uAtlasMeta; GLint loc_uModelMatrix; + GLint loc_uBoneTransforms; GLint loc_uGridSize; GLint loc_uVertexScale; GLint loc_sSceneHDRTex; diff --git a/src/library/plugin/plugin.c b/src/library/plugin/plugin.c index 8ecae00..7862881 100644 --- a/src/library/plugin/plugin.c +++ b/src/library/plugin/plugin.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -628,7 +629,7 @@ uint8_t _bolt_plugin_add(const char* path, struct Plugin* plugin) { PUSHSTRING(plugin->state, RENDER3D_META_REGISTRYNAME); lua_newtable(plugin->state); PUSHSTRING(plugin->state, "__index"); - lua_createtable(plugin->state, 0, 14); + lua_createtable(plugin->state, 0, 17); API_ADD_SUB(plugin->state, vertexcount, render3d) API_ADD_SUB(plugin->state, vertexxyz, render3d) API_ADD_SUB(plugin->state, vertexmeta, render3d) @@ -642,6 +643,9 @@ uint8_t _bolt_plugin_add(const char* path, struct Plugin* plugin) { API_ADD_SUB(plugin->state, toworldspace, render3d) API_ADD_SUB(plugin->state, toscreenspace, render3d) API_ADD_SUB(plugin->state, worldposition, render3d) + API_ADD_SUB(plugin->state, vertexbone, render3d) + API_ADD_SUB(plugin->state, bonetransforms, render3d) + API_ADD_SUB(plugin->state, animated, render3d) API_ADD_SUB_ALIAS(plugin->state, vertexcolour, vertexcolor, render3d) lua_settable(plugin->state, -3); lua_settable(plugin->state, LUA_REGISTRYINDEX); @@ -1554,6 +1558,69 @@ static int api_render3d_worldposition(lua_State* state) { return 3; } +static int api_render3d_vertexbone(lua_State* state) { + _bolt_check_argc(state, 2, "render3d_vertexbone"); + struct Render3D* render = lua_touserdata(state, 1); + const int index = lua_tointeger(state, 2); + uint8_t ret = render->vertex_functions.bone_id(index, render->vertex_functions.userdata); + lua_pushinteger(state, ret); + return 1; +} + +static int api_render3d_bonetransforms(lua_State* state) { + _bolt_check_argc(state, 2, "render3d_bonetransforms"); + struct Render3D* render = lua_touserdata(state, 1); + const int bone_id = lua_tointeger(state, 2); + + if (!render->is_animated) { + PUSHSTRING(state, "render3d_bonetransforms: cannot get bone transforms for non-animated model"); + lua_error(state); + } + + // not a mistake - game's shader supports values from 0 to 128, inclusive + if (bone_id < 0 || bone_id > 128) { + PUSHSTRING(state, "render3d_bonetransforms: invalid bone ID"); + lua_error(state); + } + +#define LENGTH(N1, N2, N3) sqrt((N1 * N1) + (N2 * N2) + (N3 * N3)) + double transform[16]; + render->vertex_functions.bone_transform((uint8_t)bone_id, render->vertex_functions.userdata, transform); + lua_pushnumber(state, transform[12]); + lua_pushnumber(state, transform[13]); + lua_pushnumber(state, transform[14]); + const double scale_x = LENGTH(transform[0], transform[1], transform[2]); + const double scale_y = LENGTH(transform[4], transform[5], transform[6]); + const double scale_z = LENGTH(transform[8], transform[9], transform[10]); + lua_pushnumber(state, scale_x); + lua_pushnumber(state, scale_y); + lua_pushnumber(state, scale_z); + if (scale_x == 0.0 || scale_y == 0.0 || scale_z == 0.0) { + // scale=0 means we can't calculate angles because there would be a division by zero, + // and angle is meaningless for a zero-scale model anyway, so just put 0 for all the angles + lua_pushnumber(state, 0.0); + lua_pushnumber(state, 0.0); + lua_pushnumber(state, 0.0); + } else { + // see https://stackoverflow.com/questions/39128589/decomposing-rotation-matrix-x-y-z-cartesian-angles + const double yaw = atan2(-(transform[8] / scale_z), sqrt((transform[0] * transform[0] / (scale_x * scale_x)) + (transform[4] * transform[4] / (scale_y * scale_y)))); + const double pitch = atan2((transform[9] / scale_z) / cos(yaw), (transform[10] / scale_z) / cos(yaw)); + const double roll = atan2((transform[4] / scale_y) / cos(yaw), (transform[0] / scale_x) / cos(yaw)); + lua_pushnumber(state, yaw); + lua_pushnumber(state, pitch); + lua_pushnumber(state, roll); + } + return 9; +#undef LENGTH +} + +static int api_render3d_animated(lua_State* state) { + _bolt_check_argc(state, 1, "render3d_animated"); + struct Render3D* render = lua_touserdata(state, 1); + lua_pushboolean(state, render->is_animated); + return 1; +} + static int api_resizeevent_size(lua_State* state) { _bolt_check_argc(state, 1, "resizeevent_size"); struct ResizeEvent* event = lua_touserdata(state, 1); diff --git a/src/library/plugin/plugin.h b/src/library/plugin/plugin.h index 5d5026b..52750b2 100644 --- a/src/library/plugin/plugin.h +++ b/src/library/plugin/plugin.h @@ -58,6 +58,12 @@ struct Vertex3DFunctions { /// Returns the RGBA colour of this vertex, each one normalised from 0.0 to 1.0. void (*colour)(size_t index, void* userdata, double* out); + + /// Returns the ID of the bone this vertex belongs to. + uint8_t (*bone_id)(size_t index, void* userdata); + + /// Returns the transform matrix for the given bone, as 16 doubles. + void (*bone_transform)(uint8_t bone_id, void* userdata, double* out); }; /// Struct containing "vtable" callback information for textures. @@ -199,6 +205,7 @@ struct RenderBatch2D { struct Render3D { uint32_t vertex_count; + uint8_t is_animated; struct Vertex3DFunctions vertex_functions; struct TextureFunctions texture_functions; struct Render3DMatrixFunctions matrix_functions; diff --git a/src/library/plugin/plugin_api.h b/src/library/plugin/plugin_api.h index 511da01..17213ac 100644 --- a/src/library/plugin/plugin_api.h +++ b/src/library/plugin/plugin_api.h @@ -484,7 +484,9 @@ static int api_window_onscroll(lua_State*); static int api_render3d_vertexcount(lua_State*); /// [-2, +3, -] -/// Given an index of a vertex in a model, returns its X Y and Z in model coordinates. +/// Given an index of a vertex in a model, returns its X Y and Z in model coordinates. More +/// specifically, this is the default position of this vertex in the model - it is not affected by +/// any kind of scaling, rotation, movement, or animation that may be happening to the model. static int api_render3d_vertexxyz(lua_State*); /// [-2, +1, -] @@ -571,6 +573,39 @@ static int api_render3d_toscreenspace(lua_State*); /// Equivalent to `render:toworldspace(0, 0, 0)` static int api_render3d_worldposition(lua_State*); +/// [-1, +1, -] +/// Returns the bone ID of this vertex. Animated models have multiple bones which can move +/// independently of each other, and this function can be used to find out which bone a vertex +/// belongs to. The returned value may be any integer from 0 to 255, although the game engine +/// actually seems to be unable to handle indices higher than 128. (128 itself is valid.) +/// +/// All vertices have bone IDs, even in non-animated models, so plugins may call this function +/// regardless of whether the model is animated or not. For a non-animated model the bone ID seems +/// to be meaningless and is usually 0. To check if the model is animated, use `animated()`. +static int api_render3d_vertexbone(lua_State*); + +/// [-1, +9, -] +/// Given a bone ID, returns the following nine floating-point values in this order: translation X, +/// Y and Z, in model coordinates; scale factor X, Y and Z; yaw, pitch, and roll, in radians. These +/// values represent the animation state of this bone during this render. +/// +/// It is a fatal error to call this function on a render event for a non-animated model, since +/// non-animated models have no bone transforms that could be queried. To check if the model is +/// animated, use `animated()`. +/// +/// The results of this function are imperfect for two reasons. Firstly, the engine doesn't use +/// static animation frames; instead it interpolates between multiple frames, so the exact state of +/// animation will depend on the user's FPS. Secondly, these values get pre-multiplied into one big +/// matrix by the time Bolt can access them. Bolt decomposes the matrix back to its original values +/// but there will be some loss of precision. +static int api_render3d_bonetransforms(lua_State*); + +/// [-1, +1, -] +/// Returns a boolean value indicating whether this model is animated. Animated models can have +/// multiple bones which can move independently of each other. For more information on bones, see +/// `vertexbone()` and `bonetransforms()`. +static int api_render3d_animated(lua_State*); + /// [-1, +2, -] /// Returns the new width and height that the window was resized to. static int api_resizeevent_size(lua_State*);