// OCTASPHERE :: https://prideout.net/blog/octasphere // Tiny malloc-free library that generates triangle meshes for spheres, rounded boxes, and capsules. // // This library proffers the following functions: // // - par_octasphere_get_counts // - par_octasphere_populate // // Usage example: // // /* Specify a 100x100x20 rounded box. */ // const par_octasphere_config cfg = { // .corner_radius = 5, // .width = 100, // .height = 100, // .depth = 20, // .num_subdivisions = 3, // }; // // /* Allocate memory for the mesh and opt-out of normals. */ // uint32_t num_indices; // uint32_t num_vertices; // par_octasphere_get_counts(&cfg, &num_indices, &num_vertices); // par_octasphere_mesh mesh = { // .positions = malloc(sizeof(float) * 3 * num_vertices), // .normals = NULL, // .texcoords = malloc(sizeof(float) * 2 * num_vertices), // .indices = malloc(sizeof(uint16_t) * num_indices), // }; // // /* Generate vertex coordinates, UV's, and triangle indices. */ // par_octasphere_populate(&cfg, &mesh); // // To generate a sphere: set width, height, and depth to 0 in your configuration. // To generate a capsule shape: set only two of these dimensions to 0. // // Distributed under the MIT License, see bottom of file. #ifndef PAR_OCTASPHERE_H #define PAR_OCTASPHERE_H #include #include #ifdef __cplusplus extern "C" { #endif #define PAR_OCTASPHERE_MAX_SUBDIVISIONS 5 typedef enum { PAR_OCTASPHERE_UV_LATLONG, } par_octasphere_uv_mode; typedef enum { PAR_OCTASPHERE_NORMALS_SMOOTH, } par_octasphere_normals_mode; typedef struct { float corner_radius; float width; float height; float depth; int num_subdivisions; par_octasphere_uv_mode uv_mode; par_octasphere_normals_mode normals_mode; } par_octasphere_config; typedef struct { float* positions; float* normals; float* texcoords; uint16_t* indices; uint32_t num_indices; uint32_t num_vertices; } par_octasphere_mesh; // Computes the maximum possible number of indices and vertices for the given octasphere config. void par_octasphere_get_counts(const par_octasphere_config* config, uint32_t* num_indices, uint32_t* num_vertices); // Populates a pre-allocated mesh structure with indices and vertices. void par_octasphere_populate(const par_octasphere_config* config, par_octasphere_mesh* mesh); #ifdef __cplusplus } #endif // ----------------------------------------------------------------------------- // END PUBLIC API // ----------------------------------------------------------------------------- #ifdef PAR_OCTASPHERE_IMPLEMENTATION #include #include #include // for memcpy #define PARO_PI (3.14159265359) #define PARO_MIN(a, b) (a > b ? b : a) #define PARO_MAX(a, b) (a > b ? a : b) #define PARO_CLAMP(v, lo, hi) PARO_MAX(lo, PARO_MIN(hi, v)) #define PARO_MAX_BOUNDARY_LENGTH ((1 << PAR_OCTASPHERE_MAX_SUBDIVISIONS) + 1) #ifndef PARO_CONSTANT_TOPOLOGY #define PARO_CONSTANT_TOPOLOGY 1 #endif static uint16_t* paro_write_quad(uint16_t* dst, uint16_t a, uint16_t b, uint16_t c, uint16_t d) { *dst++ = a; *dst++ = b; *dst++ = c; *dst++ = c; *dst++ = d; *dst++ = a; return dst; } static void paro_write_ui3(uint16_t* dst, int index, uint16_t a, uint16_t b, uint16_t c) { dst[index * 3 + 0] = a; dst[index * 3 + 1] = b; dst[index * 3 + 2] = c; } static float* paro_write_f3(float* dst, const float src[3]) { dst[0] = src[0]; dst[1] = src[1]; dst[2] = src[2]; return dst + 3; } static void paro_copy(float dst[3], const float src[3]) { dst[0] = src[0]; dst[1] = src[1]; dst[2] = src[2]; } static float paro_dot(const float a[3], const float b[3]) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; } static void paro_add(float result[3], float const a[3], float const b[3]) { result[0] = a[0] + b[0]; result[1] = a[1] + b[1]; result[2] = a[2] + b[2]; } static void paro_normalize(float v[3]) { float lsqr = sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); if (lsqr > 0) { v[0] /= lsqr; v[1] /= lsqr; v[2] /= lsqr; } } static void paro_cross(float result[3], float const a[3], float const b[3]) { float x = (a[1] * b[2]) - (a[2] * b[1]); float y = (a[2] * b[0]) - (a[0] * b[2]); float z = (a[0] * b[1]) - (a[1] * b[0]); result[0] = x; result[1] = y; result[2] = z; } static void paro_scale(float dst[3], float v) { dst[0] *= v; dst[1] *= v; dst[2] *= v; } static void paro_scaled(float dst[3], const float src[3], float v) { dst[0] = src[0] * v; dst[1] = src[1] * v; dst[2] = src[2] * v; } static void paro_quat_from_rotation(float quat[4], const float axis[3], float radians) { paro_copy(quat, axis); paro_normalize(quat); paro_scale(quat, sin(0.5 * radians)); quat[3] = cos(0.5 * radians); } static void paro_quat_from_eulers(float quat[4], const float eulers[3]) { const float roll = eulers[0]; const float pitch = eulers[1]; const float yaw = eulers[2]; const float halfRoll = roll * 0.5; const float sR = sin(halfRoll); const float cR = cos(halfRoll); const float halfPitch = pitch * 0.5; const float sP = sin(halfPitch); const float cP = cos(halfPitch); const float halfYaw = yaw * 0.5; const float sY = sin(halfYaw); const float cY = cos(halfYaw); quat[0] = (sR * cP * cY) + (cR * sP * sY); quat[1] = (cR * sP * cY) - (sR * cP * sY); quat[2] = (cR * cP * sY) + (sR * sP * cY); quat[3] = (cR * cP * cY) - (sR * sP * sY); } static void paro_quat_rotate_vector(float dst[3], const float quat[4], const float src[3]) { float t[3]; paro_cross(t, quat, src); paro_scale(t, 2.0); float p[3]; paro_cross(p, quat, t); paro_scaled(dst, t, quat[3]); paro_add(dst, dst, src); paro_add(dst, dst, p); } static float* paro_write_geodesic(float* dst, const float point_a[3], const float point_b[3], int num_segments) { dst = paro_write_f3(dst, point_a); if (num_segments == 0) { return dst; } const float angle_between_endpoints = acos(paro_dot(point_a, point_b)); const float dtheta = angle_between_endpoints / num_segments; float rotation_axis[3], quat[4]; paro_cross(rotation_axis, point_a, point_b); paro_normalize(rotation_axis); for (int point_index = 1; point_index < num_segments; point_index++, dst += 3) { paro_quat_from_rotation(quat, rotation_axis, dtheta * point_index); paro_quat_rotate_vector(dst, quat, point_a); } return paro_write_f3(dst, point_b); } void paro_add_quads(const par_octasphere_config* config, par_octasphere_mesh* mesh) { const int ndivisions = PARO_CLAMP(config->num_subdivisions, 0, PAR_OCTASPHERE_MAX_SUBDIVISIONS); const int n = (1 << ndivisions) + 1; const int verts_per_patch = n * (n + 1) / 2; const float r2 = config->corner_radius * 2; const float w = PARO_MAX(config->width, r2); const float h = PARO_MAX(config->height, r2); const float d = PARO_MAX(config->depth, r2); const float tx = (w - r2) / 2, ty = (h - r2) / 2, tz = (d - r2) / 2; // Find the vertex indices along each of the patch's 3 edges. uint16_t boundaries[3][PARO_MAX_BOUNDARY_LENGTH]; int a = 0, b = 0, c = 0, row; uint16_t j0 = 0; for (int col_index = 0; col_index < n - 1; col_index++) { int col_height = n - 1 - col_index; uint16_t j1 = j0 + 1; boundaries[0][a++] = j0; for (row = 0; row < col_height - 1; row++) { if (col_height == n - 1) { boundaries[2][c++] = j0 + row; } } if (col_height == n - 1) { boundaries[2][c++] = j0 + row; boundaries[2][c++] = j1 + row; } boundaries[1][b++] = j1 + row; j0 += col_height + 1; } boundaries[0][a] = boundaries[1][b] = j0 + row; // If there is no rounding (i.e. this is a plain box), then clobber the existing indices. if (!PARO_CONSTANT_TOPOLOGY && config->corner_radius == 0) { mesh->num_indices = 0; } uint16_t* write_ptr = mesh->indices + mesh->num_indices; const uint16_t* begin_ptr = write_ptr; if (PARO_CONSTANT_TOPOLOGY || config->corner_radius > 0) { // Go around the top half. for (int patch = 0; patch < 4; patch++) { if (!PARO_CONSTANT_TOPOLOGY && (patch % 2) == 0 && tz == 0) continue; if (!PARO_CONSTANT_TOPOLOGY && (patch % 2) == 1 && tx == 0) continue; const int next_patch = (patch + 1) % 4; const uint16_t* boundary_a = boundaries[1]; const uint16_t* boundary_b = boundaries[0]; const uint16_t offset_a = verts_per_patch * patch; const uint16_t offset_b = verts_per_patch * next_patch; for (int i = 0; i < n - 1; i++) { const uint16_t a = boundary_a[i] + offset_a; const uint16_t b = boundary_b[i] + offset_b; const uint16_t c = boundary_a[i + 1] + offset_a; const uint16_t d = boundary_b[i + 1] + offset_b; write_ptr = paro_write_quad(write_ptr, a, b, d, c); } } // Go around the bottom half. for (int patch = 4; patch < 8; patch++) { if (!PARO_CONSTANT_TOPOLOGY && (patch % 2) == 0 && tx == 0) continue; if (!PARO_CONSTANT_TOPOLOGY && (patch % 2) == 1 && tz == 0) continue; const int next_patch = 4 + (patch + 1) % 4; const uint16_t* boundary_a = boundaries[0]; const uint16_t* boundary_b = boundaries[2]; const uint16_t offset_a = verts_per_patch * patch; const uint16_t offset_b = verts_per_patch * next_patch; for (int i = 0; i < n - 1; i++) { const uint16_t a = boundary_a[i] + offset_a; const uint16_t b = boundary_b[i] + offset_b; const uint16_t c = boundary_a[i + 1] + offset_a; const uint16_t d = boundary_b[i + 1] + offset_b; write_ptr = paro_write_quad(write_ptr, d, b, a, c); } } // Connect the top and bottom halves. if (PARO_CONSTANT_TOPOLOGY || ty > 0) { for (int patch = 0; patch < 4; patch++) { const int next_patch = 4 + (4 - patch) % 4; const uint16_t* boundary_a = boundaries[2]; const uint16_t* boundary_b = boundaries[1]; const uint16_t offset_a = verts_per_patch * patch; const uint16_t offset_b = verts_per_patch * next_patch; for (int i = 0; i < n - 1; i++) { const uint16_t a = boundary_a[i] + offset_a; const uint16_t b = boundary_b[n - 1 - i] + offset_b; const uint16_t c = boundary_a[i + 1] + offset_a; const uint16_t d = boundary_b[n - 1 - i - 1] + offset_b; write_ptr = paro_write_quad(write_ptr, a, b, d, c); } } } } // Fill in the top and bottom holes. if (PARO_CONSTANT_TOPOLOGY || tx > 0 || ty > 0) { uint16_t a, b, c, d; a = boundaries[0][n - 1]; b = a + verts_per_patch; c = b + verts_per_patch; d = c + verts_per_patch; write_ptr = paro_write_quad(write_ptr, a, b, c, d); a = boundaries[2][0] + verts_per_patch * 4; b = a + verts_per_patch; c = b + verts_per_patch; d = c + verts_per_patch; write_ptr = paro_write_quad(write_ptr, a, b, c, d); } // Fill in the side holes. if (PARO_CONSTANT_TOPOLOGY || ty > 0) { const int sides[4][2] = {{7, 0}, {1, 2}, {3, 4}, {5, 6}}; for (int side = 0; side < 4; side++) { int patch_index, patch, next_patch; uint16_t *boundary_a, *boundary_b; uint16_t offset_a, offset_b; uint16_t a, b; patch_index = sides[side][0]; patch = patch_index / 2; next_patch = 4 + (4 - patch) % 4; offset_a = verts_per_patch * patch; offset_b = verts_per_patch * next_patch; boundary_a = boundaries[2]; boundary_b = boundaries[1]; if (patch_index % 2 == 0) { a = boundary_a[0] + offset_a; b = boundary_b[n - 1] + offset_b; } else { a = boundary_a[n - 1] + offset_a; b = boundary_b[0] + offset_b; } uint16_t c, d; patch_index = sides[side][1]; patch = patch_index / 2; next_patch = 4 + (4 - patch) % 4; offset_a = verts_per_patch * patch; offset_b = verts_per_patch * next_patch; boundary_a = boundaries[2]; boundary_b = boundaries[1]; if (patch_index % 2 == 0) { c = boundary_a[0] + offset_a; d = boundary_b[n - 1] + offset_b; } else { c = boundary_a[n - 1] + offset_a; d = boundary_b[0] + offset_b; } write_ptr = paro_write_quad(write_ptr, a, b, d, c); } } mesh->num_indices += write_ptr - begin_ptr; #ifndef NDEBUG uint32_t expected_indices; uint32_t expected_vertices; par_octasphere_get_counts(config, &expected_indices, &expected_vertices); assert(mesh->num_indices <= expected_indices); #endif } void par_octasphere_get_counts(const par_octasphere_config* config, uint32_t* num_indices, uint32_t* num_vertices) { const int ndivisions = PARO_CLAMP(config->num_subdivisions, 0, PAR_OCTASPHERE_MAX_SUBDIVISIONS); const int n = (1 << ndivisions) + 1; const int verts_per_patch = n * (n + 1) / 2; const float r2 = config->corner_radius * 2; const float w = PARO_MAX(config->width, r2); const float h = PARO_MAX(config->height, r2); const float d = PARO_MAX(config->depth, r2); const float tx = (w - r2) / 2, ty = (h - r2) / 2, tz = (d - r2) / 2; const int triangles_per_patch = (n - 2) * (n - 1) + n - 1; // If this is a sphere, return early. if (tx == 0 && ty == 0 && tz == 0) { *num_indices = triangles_per_patch * 8 * 3; *num_vertices = verts_per_patch * 8; return; } // This is a cuboid, so account for the maximum number of possible quads. // - 4*(n-1) quads between the 4 top patches. // - 4*(n-1) quads between the 4 bottom patches. // - 4*(n-1) quads between the top and bottom patches. // - 6 quads to fill "holes" in each cuboid face. const int num_connection_quads = (4 + 4 + 4) * (n - 1) + 6; *num_indices = (triangles_per_patch * 8 + num_connection_quads * 2) * 3; *num_vertices = verts_per_patch * 8; } void par_octasphere_populate(const par_octasphere_config* config, par_octasphere_mesh* mesh) { const int ndivisions = PARO_CLAMP(config->num_subdivisions, 0, PAR_OCTASPHERE_MAX_SUBDIVISIONS); const int n = (1 << ndivisions) + 1; const int verts_per_patch = n * (n + 1) / 2; const float r2 = config->corner_radius * 2; const float w = PARO_MAX(config->width, r2); const float h = PARO_MAX(config->height, r2); const float d = PARO_MAX(config->depth, r2); const float tx = (w - r2) / 2, ty = (h - r2) / 2, tz = (d - r2) / 2; const int triangles_per_patch = (n - 2) * (n - 1) + n - 1; const int total_vertices = verts_per_patch * 8; // START TESSELLATION OF SINGLE PATCH (one-eighth of the octasphere) float* write_ptr = mesh->positions; for (int i = 0; i < n; i++) { const float theta = PARO_PI * 0.5 * i / (n - 1); const float point_a[] = {0, sinf(theta), cosf(theta)}; const float point_b[] = {cosf(theta), sinf(theta), 0}; const int num_segments = n - 1 - i; write_ptr = paro_write_geodesic(write_ptr, point_a, point_b, num_segments); } int f = 0, j0 = 0; uint16_t* faces = mesh->indices; for (int col_index = 0; col_index < n - 1; col_index++) { const int col_height = n - 1 - col_index; const int j1 = j0 + 1; const int j2 = j0 + col_height + 1; const int j3 = j0 + col_height + 2; for (int row = 0; row < col_height - 1; row++) { paro_write_ui3(faces, f++, j0 + row, j1 + row, j2 + row); paro_write_ui3(faces, f++, j2 + row, j1 + row, j3 + row); } const int row = col_height - 1; paro_write_ui3(faces, f++, j0 + row, j1 + row, j2 + row); j0 = j2; } // END TESSELLATION OF SINGLE PATCH // START 8-WAY CLONE OF PATCH // clang-format off float euler_angles[8][3] = { {0, 0, 0}, {0, 1, 0}, {0, 2, 0}, {0, 3, 0}, {1, 0, 0}, {1, 0, 1}, {1, 0, 2}, {1, 0, 3}, }; // clang-format on for (int octant = 1; octant < 8; octant++) { paro_scale(euler_angles[octant], PARO_PI * 0.5); float quat[4]; paro_quat_from_eulers(quat, euler_angles[octant]); float* dst = mesh->positions + octant * verts_per_patch * 3; const float* src = mesh->positions; for (int vindex = 0; vindex < verts_per_patch; vindex++, dst += 3, src += 3) { paro_quat_rotate_vector(dst, quat, src); } } for (int octant = 1; octant < 8; octant++) { const int indices_per_patch = triangles_per_patch * 3; uint16_t* dst = mesh->indices + octant * indices_per_patch; const uint16_t* src = mesh->indices; const uint16_t offset = verts_per_patch * octant; for (int iindex = 0; iindex < indices_per_patch; ++iindex) { dst[iindex] = src[iindex] + offset; } } // END 8-WAY CLONE OF PATCH if (mesh->texcoords && config->uv_mode == PAR_OCTASPHERE_UV_LATLONG) { for (int i = 0; i < total_vertices; i++) { const int octant = i / verts_per_patch; const int relative_index = i % verts_per_patch; float* uv = mesh->texcoords + i * 2; const float* xyz = mesh->positions + i * 3; const float x = xyz[0], y = xyz[1], z = xyz[2]; const float phi = -atan2(z, x); const float theta = acos(y); uv[0] = 0.5 * (phi / PARO_PI + 1.0); uv[1] = theta / PARO_PI; // Special case for the north pole. if (octant < 4 && relative_index == verts_per_patch - 1) { uv[0] = fmod(0.375 + 0.25 * octant, 1.0); uv[1] = 0; } // Special case for the south pole. if (octant >= 4 && relative_index == 0) { uv[0] = 0.375 - 0.25 * (octant - 4); uv[0] = uv[0] + uv[0] < 0 ? 1.0 : 0.0; uv[1] = 1.0; } // Adjust the prime meridian for proper wrapping. if ((octant == 2 || octant == 6) && uv[0] < 0.5) { uv[0] += 1.0; } } } if (mesh->normals && config->normals_mode == PAR_OCTASPHERE_NORMALS_SMOOTH) { memcpy(mesh->normals, mesh->positions, sizeof(float) * 3 * total_vertices); } if (config->corner_radius != 1.0) { for (int i = 0; i < total_vertices; i++) { float* xyz = mesh->positions + i * 3; xyz[0] *= config->corner_radius; xyz[1] *= config->corner_radius; xyz[2] *= config->corner_radius; } } mesh->num_indices = triangles_per_patch * 8 * 3; mesh->num_vertices = total_vertices; if (tx == 0 && ty == 0 && tz == 0) { return; } for (int i = 0; i < total_vertices; i++) { float* xyz = mesh->positions + i * 3; const int octant = i / verts_per_patch; const float sx = (octant < 2 || octant == 4 || octant == 7) ? +1 : -1; const float sy = octant < 4 ? +1 : -1; const float sz = (octant == 0 || octant == 3 || octant == 4 || octant == 5) ? +1 : -1; xyz[0] += tx * sx; xyz[1] += ty * sy; xyz[2] += tz * sz; } paro_add_quads(config, mesh); } #endif // PAR_OCTASPHERE_IMPLEMENTATION #endif // PAR_OCTASPHERE_H // par_octasphere is distributed under the MIT license: // // Copyright (c) 2019 Philip Rideout // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE.