hermespy-rt

Minimalistic signal processing ray-tracer in C
git clone https://git.ea.contact/hermespy-rt
Log | Files | Refs

commit 912f80b5e42aa7f420e0c37f235b05fabe3fe8cf
Author: Egor Achkasov <eaachkasov@edu.hse.ru>
Date:   Thu, 21 Nov 2024 12:46:35 +0100

Implement ray tracing without field computation

Diffstat:
AMakefile | 2++
Acommon.h | 7+++++++
Acompute_paths.c | 347+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acompute_paths.h | 42++++++++++++++++++++++++++++++++++++++++++
Apy_compute_paths.c | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asetup.py | 17+++++++++++++++++
Atest.c | 29+++++++++++++++++++++++++++++
7 files changed, 538 insertions(+), 0 deletions(-)

diff --git a/Makefile b/Makefile @@ -0,0 +1,2 @@ +all: + gcc -g -O0 compute_paths.c test.c -lxml2 -lm -o test diff --git a/common.h b/common.h @@ -0,0 +1,7 @@ +#ifndef COMMON_H +#define COMMON_H + +#define IN +#define OUT + +#endif /* COMMON_H */ diff --git a/compute_paths.c b/compute_paths.c @@ -0,0 +1,347 @@ +#include "compute_paths.h" + +#include <libxml/parser.h> +#include <libxml/tree.h> + +#include <stddef.h> /* for size_t */ +#include <stdlib.h> /* for exit, malloc, free */ +#include <libgen.h> /* for dirname */ +#include <string.h> /* for strlen, sprintf */ +#include <stdio.h> /* for fopen, FILE, fclose */ +#include <math.h> /* for sin, cos, sqrt */ + +#define PI 3.14159265358979323846 +#define EPS 1e-6 + +typedef struct { + float x, y, z; +} Vec3; + +typedef struct { + Vec3 o, d; +} Ray; + +typedef struct { + Vec3 *vertices; + size_t num_vertices; + int32_t *indices; + size_t num_indices; + Vec3 *normals; +} Mesh; + +typedef struct { + Mesh *meshes[256]; /* TODO: dynamic allocation, fix possible overflow */ + size_t num_meshes; +} Scene; + +/* ==== VECTOR OPERATIONS ==== */ +/* TODO fastcalls */ + +Vec3 vec3_sub(Vec3 a, Vec3 b) +{ + return (Vec3){a.x - b.x, a.y - b.y, a.z - b.z}; +} +Vec3 vec3_add(Vec3 a, Vec3 b) +{ + return (Vec3){a.x + b.x, a.y + b.y, a.z + b.z}; +} +Vec3 vec3_cross(Vec3 a, Vec3 b) +{ + return (Vec3){ + a.y * b.z - a.z * b.y, + a.z * b.x - a.x * b.z, + a.x * b.y - a.y * b.x + }; +} +float vec3_dot(Vec3 a, Vec3 b) +{ + return a.x * b.x + a.y * b.y + a.z * b.z; +} +Vec3 vec3_scale(Vec3 a, float s) +{ + return (Vec3){a.x * s, a.y * s, a.z * s}; +} + +/* ==== MESH LOADING ==== */ + +/** Load a mesh from a PLY file. + * + * Supports only format binary_little_endian 1.0. + * + * All faces must be triangles, meaning + * indices set as "property list uchar int" + * with each uchar being 3. + * + * Vertices must contain float x, y, z, u and v coordinates. + * + * \param mesh_filepath path to the PLY file + * \return the loaded mesh + */ +Mesh* load_mesh_ply(const char *mesh_filepath) +{ + FILE *f = fopen(mesh_filepath, "rb"); + if (!f) { + fprintf(stderr, "Could not open file %s\n", mesh_filepath); + exit(8); + } + + char buff[256]; + + /* HEADER */ + /* ply */ + fread(buff, 1, 4, f); + if (strncmp(buff, "ply\n", 4)) exit(8); + /* format binary_little_endian 1.0 */ + fread(buff, 1, 32, f); + if (strncmp(buff, "format binary_little_endian 1.0\n", 32)) exit(8); + + Mesh *mesh = (Mesh*)malloc(sizeof(Mesh)); + while (fgets(buff, sizeof(buff), f)) { + if (strncmp(buff, "element vertex", 14) == 0) + sscanf(buff, "element vertex %zu", &mesh->num_vertices); + else if (strncmp(buff, "element face", 12) == 0) + sscanf(buff, "element face %zu", &mesh->num_indices); + else if (strncmp(buff, "end_header", 10) == 0) + break; + } + mesh->num_indices *= 3; + mesh->vertices = (Vec3*)malloc(mesh->num_vertices * sizeof(Vec3)); + mesh->indices = (int32_t*)malloc(mesh->num_indices * sizeof(int32_t)); + mesh->normals = (Vec3*)malloc(mesh->num_indices / 3 * sizeof(Vec3)); + + /* VERTICES */ + for (size_t i = 0; i < mesh->num_vertices; ++i) { + fread(&mesh->vertices[i], sizeof(Vec3), 1, f); + fseek(f, 8, SEEK_CUR); /* skip u and v */ + } + + /* INDICES */ + /* also calculate normals */ + uint8_t n; + Vec3 v1, v2, v3, u, v; + for (size_t i = 0; i < mesh->num_indices; i += 3) { + fread(&n, 1, 1, f); + if (n != 3) { + fprintf(stderr, "Only trianglar faces are supported\n"); + exit(8); + } + fread(&mesh->indices[i], sizeof(int32_t), 3, f); + /* calculate normal */ + v1 = mesh->vertices[mesh->indices[i]]; + v2 = mesh->vertices[mesh->indices[i + 1]]; + v3 = mesh->vertices[mesh->indices[i + 2]]; + u = vec3_sub(v2, v1); + v = vec3_sub(v3, v1); + mesh->normals[i / 3] = vec3_cross(u, v); + } + + fclose(f); + return mesh; +} + +/** Load a scene from a Mitsuba XML file. + * + * \param scene_filepath path to the Mitsuba XML file + * \return the loaded mesh + */ +Scene load_scene(const char *scene_filepath) +{ + Scene scene; + scene.num_meshes = 0; + + /* Parse xml file */ + xmlDocPtr doc = xmlReadFile(scene_filepath, NULL, 0); + if (!doc) { + fprintf(stderr, "Could not parse file %s\n", scene_filepath); + exit(8); + } + xmlNodePtr root = xmlDocGetRootElement(doc); + xmlNodePtr cur = NULL; + xmlNodePtr child = NULL; + xmlChar *prop; + char* dir = dirname((char*)scene_filepath); + char *mesh_filepath; + for (cur = root->children; cur; cur = cur->next) { + if (cur->type != XML_ELEMENT_NODE + || xmlStrcmp(cur->name, (const xmlChar *)"shape")) + continue; + prop = xmlGetProp(cur, (const xmlChar *)"type"); + if (!prop || xmlStrcmp(prop, (const xmlChar *)"ply")) { + xmlFree(prop); + continue; + } + xmlFree(prop); + /* <shape type="ply" id="... found, now get the mesh */ + for (child = cur->children; child; child = child->next) { + if (child->type != XML_ELEMENT_NODE + || xmlStrcmp(child->name, (const xmlChar *)"string")) + continue; + prop = xmlGetProp(child, (const xmlChar *)"name"); + if (!prop || xmlStrcmp(prop, (const xmlChar *)"filename")) + continue; + xmlFree(prop); + prop = xmlGetProp(child, (const xmlChar *)"value"); + if (!prop) continue; + /* get the mesh filepath */ + mesh_filepath = (char*)malloc(strlen(dir) + xmlStrlen(prop) + 2); + sprintf(mesh_filepath, "%s/%s", dir, prop); + /* load the mesh */ + scene.meshes[scene.num_meshes] = load_mesh_ply(mesh_filepath); + ++scene.num_meshes; + free(mesh_filepath); + xmlFree(prop); + } + } + + return scene; +} + +/* ==== RT ==== */ + +/** Compute Moeleer-Trumbore intersection algorithm. + * + * \param ray the ray + * \param mesh the mesh + * \param t output distance to the hit point. If no hit, t is not modified. + * \param ind output index of the hit triangle. If no hit, i is not modified. + */ +void moeller_trumbore(Ray *ray, Mesh *mesh, float *t, size_t *ind) +{ + /* TODO BVH */ + /* for each triangle */ + Vec3 v1, v2, v3, e1, e2, re2_cross, s, se1_cross; + float d, u, v; + float dist = 1e9; + float dist_tmp; + for (size_t i = 0; i < mesh->num_indices; i += 3) { + v1 = mesh->vertices[mesh->indices[i]]; + v2 = mesh->vertices[mesh->indices[i + 1]]; + v3 = mesh->vertices[mesh->indices[i + 2]]; + e1 = vec3_sub(v2, v1); + e2 = vec3_sub(v3, v1); + re2_cross = vec3_cross(ray->d, e2); + d = vec3_dot(e1, re2_cross); + if (d > -EPS && d < EPS) continue; + s = vec3_sub(ray->o, v1); + u = vec3_dot(s, re2_cross) / d; + if ((u < 0. && fabs(u) > EPS) + || (u > 1. && fabs(u - 1.) > EPS)) + continue; + se1_cross = vec3_cross(s, e1); + v = vec3_dot(ray->d, se1_cross) / d; + if ((v < 0. && fabs(v) > EPS) + || (u + v > 1. && fabs(u + v - 1.) > EPS)) + continue; + dist_tmp = vec3_dot(e2, se1_cross) / d; + if (dist_tmp > EPS && dist_tmp < dist) { + dist = dist_tmp; + *t = dist; + *ind = i; + } + } +} + +/* ==== MAIN FUNCTION ==== */ + +void compute_paths( + IN const char *scene_filepath, /* path to the scene file */ + IN const float *rx_positions, /* shape (num_rx, 3) */ + IN const float *tx_positions, /* shape (num_tx, 3) */ + IN const float *rx_velocities, /* shape (num_rx, 3) */ + IN const float *tx_velocities, /* shape (num_tx, 3) */ + IN float carrier_frequency, /* > 0.0 */ + IN int num_rx, /* number of receivers */ + IN int num_tx, /* number of transmitters */ + IN int num_paths, /* number of paths */ + IN int num_bounces, /* number of bounces */ + OUT float *a, /* output array of gains (num_paths,) */ + OUT int32_t *tau /* output array of delays (num_paths,) */ +) +{ + /* Load the scene */ + Scene scene = load_scene(scene_filepath); + + /* Calculate fibonacci sphere */ + Vec3 *ray_directions = (Vec3*)malloc(num_paths * sizeof(Vec3)); + float k, phi, theta; + for (size_t i = 0; i < num_paths; ++i) { + k = (float)i + .5; + phi = 1. - 2. * k / num_paths; + theta = PI * (1. + sqrt(5.)) * k; + ray_directions[i] = (Vec3){ + cos(theta) * sin(phi), + sin(theta) * sin(phi), + cos(phi) + }; + } + + /* Create num_path rays for each tx */ + /* Shape (num_tx, num_paths) */ + Ray **rays = (Ray**)malloc(num_tx * sizeof(Ray*)); + Vec3 tx_position; + for (size_t i = 0; i < num_tx; ++i) { + rays[i] = (Ray*)malloc(num_paths * sizeof(Ray)); + tx_position = (Vec3){tx_positions[i * 3], tx_positions[i * 3 + 1], tx_positions[i * 3 + 2]}; + for (size_t j = 0; j < num_paths; ++j) { + rays[i][j].o = tx_position; + rays[i][j].d = ray_directions[j]; + } + } + free(ray_directions); + + /* Calculate the paths */ + /* TODO calculate a and tau in moeller_trumbore */ + /* shape (num_bounces, num_tx, num_paths) */ + Vec3 ***hits = (Vec3***)malloc(num_bounces * sizeof(Vec3**)); + size_t ***hit_indices = (size_t***)malloc(num_bounces * sizeof(size_t**)); + float t; + size_t ind; + for (size_t i = 0; i < num_bounces; ++i) { + hits[i] = (Vec3**)malloc(num_tx * sizeof(Vec3*)); + hit_indices[i] = (size_t**)malloc(num_tx * sizeof(size_t*)); + for (size_t j = 0; j < num_tx; ++j) { + hits[i][j] = (Vec3*)malloc(num_paths * sizeof(Vec3)); + hit_indices[i][j] = (size_t*)malloc(num_paths * sizeof(size_t)); + for (size_t k = 0; k < num_paths; ++k) + for (size_t l = 0; l < scene.num_meshes; ++l) { + t = -1.; + ind = -1; + moeller_trumbore(&rays[j][k], scene.meshes[l], &t, &ind); + hits[i][j][k] = vec3_scale(rays[j][k].d, t); + hits[i][j][k] = vec3_add(hits[i][j][k], rays[j][k].o); + rays[j][k].o = hits[i][j][k]; + hit_indices[i][j][k] = ind; + } + } + } + + /* TODO Remove */ + a = hits; + tau = hit_indices; + + /* Free */ + /* Free the hits */ + /* + for (size_t i = 0; i < num_bounces; ++i) { + for (size_t j = 0; j < num_tx; ++j) { + free(hits[i][j]); + free(hit_indices[i][j]); + } + free(hits[i]); + free(hit_indices[i]); + } + free(hits); + free(hit_indices); + */ + /* Free the rays */ + for (size_t i = 0; i < num_tx; ++i) + free(rays[i]); + free(rays); + /* Free the scene */ + for (size_t i = 0; i < scene.num_meshes; ++i) { + free(scene.meshes[i]->vertices); + free(scene.meshes[i]->indices); + free(scene.meshes[i]->normals); + free(scene.meshes[i]); + } +} diff --git a/compute_paths.h b/compute_paths.h @@ -0,0 +1,42 @@ +#ifndef COMPUTE_PATHS_H +#define COMPUTE_PATHS_H + +#include <stdint.h> /* for int32_t */ + +#include "common.h" + +/** Compute gains and delays between in a Mitsuba scene. + * + * Scene must be defined in a Mitsuba .xml JSON format. + * The meshes must be present in the same directory as the .xml file, + * under meshes/ directory in .ply files in PLY format. + * Meshes filenames must be the same as the name of the object in the .xml file. + * + * \param scene_filepath path to a Mitsuba scene .xml file + * \param rx_positions receiver positions, shape (num_rx, 3) + * \param tx_positions transmitter positions, shape (num_tx, 3) + * \param rx_velocities receiver velocities, shape (num_rx, 3) + * \param tx_velocities transmitter velocities, shape (num_tx, 3) + * \param carrier_frequency carrier frequency in Hz. Must be > 0.0 + * \param num_rx number of receivers. Must be > 0 + * \param num_tx number of transmitters. Must be > 0 + * \param num_paths number of paths to compute. Must be > 0 + * \param a output array of gains, shape (num_paths,) + * \param tau output array of delays, shape (num_paths,) +*/ +void compute_paths( + IN const char *scene_filepath, /* path to the scene file */ + IN const float *rx_positions, /* shape (num_rx, 3) */ + IN const float *tx_positions, /* shape (num_tx, 3) */ + IN const float *rx_velocities, /* shape (num_rx, 3) */ + IN const float *tx_velocities, /* shape (num_tx, 3) */ + IN float carrier_frequency, /* > 0.0 */ + IN int num_rx, /* number of receivers */ + IN int num_tx, /* number of transmitters */ + IN int num_paths, /* number of paths */ + IN int num_bounces, /* number of bounces */ + OUT float *a, /* output array of gains (num_paths,) */ + OUT int32_t *tau /* output array of delays (num_paths,) */ +); + +#endif /* COMPUTE_PATHS_H */ diff --git a/py_compute_paths.c b/py_compute_paths.c @@ -0,0 +1,94 @@ +/* This file contains a python binding for compute_paths function. */ + +#include <Python.h> +#include <numpy/arrayobject.h> // Numpy C API +#include "compute_paths.h" // Assuming this header contains the compute_paths function declaration + +// Wrapper function for Python to call compute_paths +static PyObject* py_compute_paths(PyObject* self, PyObject* args) { + const char* scene_filepath; + PyObject* rx_positions_obj; + PyObject* tx_positions_obj; + PyObject* rx_velocities_obj; + PyObject* tx_velocities_obj; + float carrier_frequency; + int num_rx, num_tx, num_paths, num_bounces; + + if (!PyArg_ParseTuple(args, "sO&O&O&O&fiiii", + &scene_filepath, + &rx_positions_obj, + &tx_positions_obj, + &rx_velocities_obj, + &tx_velocities_obj, + &carrier_frequency, + &num_rx, + &num_tx, + &num_paths, + &num_bounces)) + return NULL; + + if (!PyArray_Check(rx_positions_obj) || !PyArray_Check(tx_positions_obj) || + !PyArray_Check(rx_velocities_obj) || !PyArray_Check(tx_velocities_obj)) { + PyErr_SetString(PyExc_TypeError, "All inputs must be numpy arrays."); + return NULL; + } + + npy_float32* rx_positions = (npy_float32*)PyArray_DATA((PyArrayObject*)rx_positions_obj); + npy_float32* tx_positions = (npy_float32*)PyArray_DATA((PyArrayObject*)tx_positions_obj); + npy_float32* rx_velocities = (npy_float32*)PyArray_DATA((PyArrayObject*)rx_velocities_obj); + npy_float32* tx_velocities = (npy_float32*)PyArray_DATA((PyArrayObject*)tx_velocities_obj); + + // Output arrays for gains and delays + float* a = (float*)malloc(num_paths * sizeof(float)); + int32_t* tau = (int32_t*)malloc(num_paths * sizeof(int32_t)); + + if (!a || !tau) { + PyErr_NoMemory(); + return NULL; + } + + // Call the C compute_paths function + compute_paths(scene_filepath, + rx_positions, tx_positions, + rx_velocities, tx_velocities, + carrier_frequency, + num_rx, num_tx, num_paths, num_bounces, + a, tau); + + // Prepare the output tuple (gains, delays) + //npy_intp dims[1] = {num_paths}; + npy_intp dims[] = {num_bounces, num_tx, num_paths}; + PyObject* gains_array = PyArray_SimpleNewFromData(1, dims, NPY_FLOAT32, a); + PyObject* delays_array = PyArray_SimpleNewFromData(1, dims, NPY_INT32, tau); + + // Return the tuple of numpy arrays + PyObject* result = PyTuple_Pack(2, gains_array, delays_array); + + // Clean up + Py_DECREF(gains_array); + Py_DECREF(delays_array); + + return result; +} + +// Method table +/* TODO add args and returns */ +static PyMethodDef module_methods[] = { + {"compute_paths", py_compute_paths, METH_VARARGS, "Compute the paths between transmitters and receivers."}, + {NULL, NULL, 0, NULL} +}; + +// Module definition +static struct PyModuleDef module_definition = { + PyModuleDef_HEAD_INIT, + "compute_paths_module", // Module name + "C extension for computing paths.", // Module docstring + -1, + module_methods // Method table +}; + +// Module initialization function +PyMODINIT_FUNC PyInit_compute_paths_module(void) { + import_array(); // Initialize the numpy API + return PyModule_Create(&module_definition); +} diff --git a/setup.py b/setup.py @@ -0,0 +1,17 @@ +from setuptools import setup, Extension +import numpy + +module = Extension( + 'compute_paths_module', + sources=['compute_paths.c', 'py_compute_paths.c'], + include_dirs=[numpy.get_include()], + libraries=["m", "xml2"], + extra_compile_args=['-O3'], +) + +setup( + name='compute_paths_module', + version='1.0', + description='TODO write description.', # TODO write description + ext_modules=[module], +) diff --git a/test.c b/test.c @@ -0,0 +1,29 @@ +#include "compute_paths.h" + +#include <stdio.h> /* for fprintf */ + +int main(int argc, char **argv) +{ + if (argc < 2) { + fprintf(stderr, "Usage: %s <path_to_scene.xml>\n", argv[0]); + return 1; + } + + float rx_positions[3] = {0.0, 0.0, 0.0}; + float tx_positions[3] = {0.0, 0.0, 0.0}; + float rx_velocities[3] = {0.0, 0.0, 0.0}; + float tx_velocities[3] = {0.0, 0.0, 0.0}; + float carrier_frequency = 2.4e9; /* 2.4 GHz */ + int num_paths = 10000; + float *a = NULL; + int32_t *tau = NULL; + compute_paths( + argv[1], + rx_positions, + tx_positions, + rx_velocities, + tx_velocities, + carrier_frequency, + 1, 1, num_paths, 3, + a, tau); +}