Created
July 23, 2025 05:07
-
-
Save TrevorSundberg/e4066c6d69c7ccb058a39f85da689bbf to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #include <assert.h> | |
| #include <math.h> | |
| #include <stdint.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #define INLINE __attribute__((always_inline)) inline | |
| #if defined(_WIN32) | |
| #define EXPORT __declspec(dllexport) | |
| #else | |
| #define EXPORT __attribute__((visibility("default"))) | |
| #endif | |
| #define X 0 | |
| #define Y 1 | |
| #define Z 2 | |
| #define W 3 | |
| typedef int32_t bool; | |
| // #define USE_FLOAT_NUMBER | |
| #if !defined(USE_FLOAT_NUMBER) && !defined(USE_FLOAT4_NUMBER) | |
| #define USE_FLOAT4_NUMBER | |
| #endif | |
| #ifdef USE_FLOAT4_NUMBER | |
| typedef float component; | |
| typedef __attribute__((ext_vector_type(4))) float number; | |
| typedef __attribute__((ext_vector_type(4))) bool boolean; | |
| typedef __attribute__((ext_vector_type(4))) int32_t integer; | |
| #define NUMBER_COMPONENTS 4 | |
| #define NUMBER_ACCESS(value, component_index) ((value)[component_index]) | |
| #define NUMBER_INDEX(state_index, index_subscripts) [state_index / NUMBER_COMPONENTS] index_subscripts[state_index % NUMBER_COMPONENTS] | |
| #define VECTOR_REPEAT_STRING(string) string ", " string ", " string ", " string | |
| #define VECTOR_REPEAT_COMPONENT(before, value, after) before value.x after, before value.y after, before value.z after, before value.w after | |
| INLINE boolean boolean_standardize(const boolean nonstandard_boolean) { | |
| // OpenCL boolean vectors use -1 instead of +1, so we standardize it to always be +1 | |
| return __builtin_elementwise_abs(nonstandard_boolean); | |
| } | |
| INLINE bool number_nearly_equal_epsilon(number a, number b, number epsilon) { | |
| return __builtin_reduce_add(__builtin_elementwise_abs(a - b) < epsilon) == -4; | |
| } | |
| #endif | |
| #ifdef USE_FLOAT_NUMBER | |
| typedef float component; | |
| typedef float number; | |
| typedef bool boolean; | |
| typedef int32_t integer; | |
| #define NUMBER_COMPONENTS 1 | |
| #define NUMBER_ACCESS(value, component_index) (value) | |
| #define NUMBER_INDEX(state_index, index_subscripts) [state_index] index_subscripts | |
| #define VECTOR_REPEAT_STRING(string) string | |
| #define VECTOR_REPEAT_COMPONENT(before, value, after) before value after | |
| INLINE boolean boolean_standardize(const boolean value) { | |
| return value; | |
| } | |
| INLINE bool number_nearly_equal_epsilon(number a, number b, number epsilon) { | |
| return __builtin_elementwise_abs(a - b) < epsilon; | |
| } | |
| #endif | |
| #define VECTOR_EXPAND(type, value) ((type){VECTOR_REPEAT_COMPONENT(, value, )}) | |
| #define CONSTANT(value) ((number)value) | |
| #define PI CONSTANT(3.14159265358979323846) | |
| #define E CONSTANT(2.71828182845904523536) | |
| #define RADIAN_TO_DEGREE (CONSTANT(180) / PI) | |
| #define INDEXED_GETTER_SETTER_BASE(states_type, member, index_subscripts, ...) \ | |
| EXPORT component states_type##_get_##member(const states_type *const states, const uintptr_t state_index, ##__VA_ARGS__) { \ | |
| return states->member NUMBER_INDEX(state_index, index_subscripts); \ | |
| } \ | |
| EXPORT void states_type##_set_##member(states_type *const states, const uintptr_t state_index, ##__VA_ARGS__, const component value) { \ | |
| states->member NUMBER_INDEX(state_index, index_subscripts) = value; \ | |
| } | |
| #define INDEXED_GETTER_SETTER(states_type, member) \ | |
| INDEXED_GETTER_SETTER_BASE(states_type, member, ) | |
| #define INDEXED_GETTER_SETTER_VECTOR(states_type, member) \ | |
| INDEXED_GETTER_SETTER_BASE(states_type, member, .array[component_index], const uintptr_t component_index) | |
| #define INDEXED_GETTER_SETTER_ARRAY(states_type, member) \ | |
| INDEXED_GETTER_SETTER_BASE(states_type, member, [array_index], const uintptr_t array_index) | |
| #define INDEXED_GETTER_SETTER_ARRAY_VECTOR(states_type, member) \ | |
| INDEXED_GETTER_SETTER_BASE(states_type, member, [array_index].array[component_index], const uintptr_t array_index, const uintptr_t component_index) | |
| #define STATES_FUNCTIONS(name) \ | |
| EXPORT void name##_init(name *const states); \ | |
| EXPORT uintptr_t name##_size(void) { \ | |
| return sizeof(name); \ | |
| } \ | |
| EXPORT name *name##_allocate(void) { \ | |
| name *states = malloc(sizeof(name)); \ | |
| name##_init(states); \ | |
| return states; \ | |
| } \ | |
| EXPORT void name##_free(name *const states) { \ | |
| free(states); \ | |
| } | |
| #define BODY_COUNT 100 | |
| #define BODY_GROUP_COUNT (BODY_COUNT / NUMBER_COMPONENTS) | |
| static_assert(BODY_COUNT % NUMBER_COMPONENTS == 0, "BODY_COUNT must be divisible by NUMBER_COMPONENTS"); | |
| #define MOTOR_COUNT 4 | |
| // Motor configuration for back/front or right/left | |
| #define MOTOR_BR 0 | |
| #define MOTOR_FR 1 | |
| #define MOTOR_BL 2 | |
| #define MOTOR_FL 3 | |
| #define PRIMARY_AXIS_COUNT 3 | |
| #define ALL_AXIS_COUNT 4 | |
| #define AXIS_PITCH 0 // X axis | |
| #define AXIS_YAW 1 // Y axis | |
| #define AXIS_ROLL 2 // Z axis | |
| #define AXIS_THROTTLE 3 // Not included in rates but used in mixing | |
| #define BOX_FACE_COUNT 6 | |
| #define BOX_FACE_PX 0 | |
| #define BOX_FACE_NX 1 | |
| #define BOX_FACE_PY 2 | |
| #define BOX_FACE_NY 3 | |
| #define BOX_FACE_PZ 4 | |
| #define BOX_FACE_NZ 5 | |
| #define EPSILON CONSTANT(1e-4) | |
| #define UNIVERSAL_GAS_CONSTANT CONSTANT(8.3144598) /* J/(mol*K) */ | |
| #define MOLAR_MASS_AIR CONSTANT(0.0289644) /* kg/mol */ | |
| typedef union { | |
| struct | |
| { | |
| number x; | |
| number y; | |
| number z; | |
| }; | |
| number array[3]; | |
| } vector3; | |
| typedef union { | |
| struct | |
| { | |
| number x; | |
| number y; | |
| number z; | |
| number w; | |
| }; | |
| number array[4]; | |
| } quaternion; | |
| typedef union { | |
| struct | |
| { | |
| number m00; | |
| number m01; | |
| number m02; | |
| number m11; | |
| number m12; | |
| number m22; | |
| }; | |
| number array[6]; | |
| } upper_diagonal_matrix3; | |
| typedef struct { | |
| number output; | |
| number alpha; | |
| } low_pass_filter; | |
| INLINE number number_from_bool(const boolean nonstandard_boolean) { | |
| const boolean standardized_boolean = boolean_standardize(nonstandard_boolean); | |
| return VECTOR_EXPAND(number, standardized_boolean); | |
| } | |
| INLINE number choose_number(const number if_true, const number if_false, const boolean nonstandard_boolean) { | |
| number true_multiplier = number_from_bool(nonstandard_boolean); | |
| number false_multiplier = CONSTANT(1) - true_multiplier; | |
| return if_true * true_multiplier + if_false * false_multiplier; | |
| } | |
| // https://dsp.stackexchange.com/a/54088 | |
| INLINE number low_pass_filter_compute_alpha(const number delta_time, const number frequency_hz) { | |
| // Compute wc (omega-c) | |
| number angular_cuttoff_frequency = CONSTANT(2) * PI * frequency_hz; | |
| number y = CONSTANT(1) - __builtin_elementwise_cos(angular_cuttoff_frequency * delta_time); | |
| return -y + __builtin_elementwise_sqrt(y * y + CONSTANT(2) * y); | |
| } | |
| INLINE void low_pass_filter_init(low_pass_filter *filter, const number delta_time, const number frequency_hz) { | |
| filter->output = 0; | |
| filter->alpha = low_pass_filter_compute_alpha(delta_time, frequency_hz); | |
| } | |
| INLINE number quaternion_length(const quaternion value) { | |
| return __builtin_elementwise_sqrt( | |
| value.x * value.x + | |
| value.y * value.y + | |
| value.z * value.z + | |
| value.w * value.w); | |
| } | |
| INLINE quaternion quaternion_normalize(const quaternion value) { | |
| const number magnitude = quaternion_length(value); | |
| return (quaternion){ | |
| value.x / magnitude, | |
| value.y / magnitude, | |
| value.z / magnitude, | |
| value.w / magnitude}; | |
| } | |
| INLINE quaternion quaternion_normalized_inverse(const quaternion q) { | |
| // For normalized quaternion, inverse is just conjugate | |
| return (quaternion){-q.x, -q.y, -q.z, q.w}; | |
| } | |
| INLINE quaternion quaternion_add(const quaternion a, const quaternion b) { | |
| return (quaternion){ | |
| a.x + b.x, | |
| a.y + b.y, | |
| a.z + b.z, | |
| a.w + b.w}; | |
| } | |
| INLINE quaternion quaternion_multiply(const quaternion a, const quaternion b) { | |
| return (quaternion){ | |
| a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y, | |
| a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x, | |
| a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w, | |
| a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z}; | |
| } | |
| INLINE number vector3_dot(const vector3 a, const vector3 b) { | |
| return a.x * b.x + a.y * b.y + a.z * b.z; | |
| } | |
| INLINE number vector3_length_squared(const vector3 value) { | |
| return vector3_dot(value, value); | |
| } | |
| INLINE number vector3_length(const vector3 value) { | |
| return __builtin_elementwise_sqrt(vector3_length_squared(value)); | |
| } | |
| INLINE vector3 vector3_scalar_multiply(const number scalar, const vector3 v) { | |
| return (vector3){ | |
| scalar * v.x, | |
| scalar * v.y, | |
| scalar * v.z}; | |
| } | |
| // If a divisior is large enough, it stays the same, otherwise it becomes 1 to avoid a division by zero | |
| INLINE number number_fix_positive_divisor(const number positive_divisor, const number minimum_positive_divisor) { | |
| number divisor_big_enough = number_from_bool(positive_divisor > minimum_positive_divisor); | |
| number divisor_too_small = CONSTANT(1) - divisor_big_enough; | |
| return positive_divisor * divisor_big_enough + divisor_too_small; | |
| } | |
| INLINE vector3 vector3_normalize(const vector3 value, const number minimum_length) { | |
| const number magnitude = number_fix_positive_divisor(vector3_length(value), minimum_length); | |
| return vector3_scalar_multiply(CONSTANT(1) / magnitude, value); | |
| } | |
| INLINE vector3 vector3_cross(const vector3 a, const vector3 b) { | |
| return (vector3){ | |
| a.y * b.z - a.z * b.y, | |
| a.z * b.x - a.x * b.z, | |
| a.x * b.y - a.y * b.x}; | |
| } | |
| INLINE vector3 vector3_add(const vector3 a, const vector3 b) { | |
| return (vector3){ | |
| a.x + b.x, | |
| a.y + b.y, | |
| a.z + b.z}; | |
| } | |
| INLINE vector3 vector3_subtract(const vector3 a, const vector3 b) { | |
| return (vector3){ | |
| a.x - b.x, | |
| a.y - b.y, | |
| a.z - b.z}; | |
| } | |
| INLINE vector3 vector3_abs(const vector3 value) { | |
| return (vector3){ | |
| __builtin_elementwise_abs(value.x), | |
| __builtin_elementwise_abs(value.y), | |
| __builtin_elementwise_abs(value.z)}; | |
| } | |
| // https://gamedev.stackexchange.com/questions/28395/rotating-vector3-by-a-quaternion | |
| INLINE vector3 rotate_vector_by_quaternion(const vector3 v, const quaternion q) { | |
| vector3 u = {q.x, q.y, q.z}; | |
| number s = q.w; | |
| vector3 projection = vector3_scalar_multiply(2.0f * vector3_dot(u, v), u); | |
| vector3 scaled = vector3_scalar_multiply(s * s - vector3_dot(u, u), v); | |
| vector3 perpendicular = vector3_scalar_multiply(2.0f * s, vector3_cross(u, v)); | |
| return vector3_add(vector3_add(projection, scaled), perpendicular); | |
| } | |
| INLINE vector3 point_velocity_from_offset_world( | |
| const vector3 linear_velocity_world, | |
| const vector3 angular_velocity_world, | |
| const vector3 offset_world) { | |
| return vector3_add(linear_velocity_world, vector3_cross(angular_velocity_world, offset_world)); | |
| } | |
| INLINE vector3 apply_force_get_torque_world( | |
| const vector3 force_world, | |
| const vector3 offset_world) { | |
| return vector3_cross(offset_world, force_world); | |
| } | |
| INLINE uintptr_t ceil_division(uintptr_t x, uintptr_t y) { | |
| return (x + y - 1) / y; | |
| } | |
| INLINE number clamp(number value, number min, number max) { | |
| return __builtin_elementwise_min(max, __builtin_elementwise_max(min, value)); | |
| } | |
| INLINE number smooth_noise(number input, uintptr_t octaves, number radian_frequency_per_octave[], number amplitude_per_octave[]) { | |
| number result = 0; | |
| number total_amplitude = 0; | |
| for (uintptr_t i = 0; i < octaves; ++i) { | |
| result += __builtin_elementwise_sin(input * radian_frequency_per_octave[i]) * amplitude_per_octave[i]; | |
| total_amplitude += amplitude_per_octave[i]; | |
| } | |
| return result / total_amplitude; | |
| } | |
| INLINE number smooth_noise_simple(number input) { | |
| number radian_frequency_per_octave[] = { | |
| CONSTANT(1.00000), | |
| CONSTANT(1.05245), | |
| CONSTANT(2.64983), | |
| CONSTANT(4.33752), | |
| }; | |
| number amplitude_per_octave[] = { | |
| CONSTANT(1.00000), | |
| CONSTANT(1.00000), | |
| CONSTANT(0.59456), | |
| CONSTANT(0.34035), | |
| }; | |
| return smooth_noise(input, 4, radian_frequency_per_octave, amplitude_per_octave); | |
| } | |
| INLINE vector3 smooth_noise_simple_vector3(vector3 input) { | |
| return (vector3){ | |
| smooth_noise_simple(input.x), | |
| smooth_noise_simple(input.y), | |
| smooth_noise_simple(input.z), | |
| }; | |
| } | |
| typedef struct { | |
| uintptr_t count; | |
| bool enable_ground_plane; | |
| number inverse_mass[BODY_GROUP_COUNT]; | |
| vector3 forces_world[BODY_GROUP_COUNT]; | |
| vector3 linear_acceleration_world[BODY_GROUP_COUNT]; | |
| vector3 linear_velocity_world[BODY_GROUP_COUNT]; | |
| vector3 position_world[BODY_GROUP_COUNT]; | |
| upper_diagonal_matrix3 inverse_inertia_tensor_local[BODY_GROUP_COUNT]; | |
| vector3 torques_world[BODY_GROUP_COUNT]; | |
| vector3 angular_velocity_world[BODY_GROUP_COUNT]; | |
| vector3 angular_velocity_local[BODY_GROUP_COUNT]; | |
| quaternion orientation_world[BODY_GROUP_COUNT]; | |
| number air_density[BODY_GROUP_COUNT]; | |
| vector3 drag_box_size[BODY_GROUP_COUNT]; | |
| number drag_coefficient[BODY_GROUP_COUNT][BOX_FACE_COUNT]; | |
| number drag_pillow_effect[BODY_GROUP_COUNT][BOX_FACE_COUNT]; | |
| vector3 wind_velocity_world[BODY_GROUP_COUNT]; | |
| vector3 wind_air_pocket_sample_position_world[BODY_GROUP_COUNT]; | |
| number wind_air_pocket_width[BODY_GROUP_COUNT]; | |
| number wind_air_pocket_amplitude[BODY_GROUP_COUNT]; | |
| number wind_air_pocket_velocity_scale[BODY_GROUP_COUNT]; | |
| vector3 gravity_world[BODY_GROUP_COUNT]; | |
| vector3 debug_output[BODY_GROUP_COUNT]; | |
| } physics_states; | |
| STATES_FUNCTIONS(physics_states) | |
| INDEXED_GETTER_SETTER(physics_states, inverse_mass) | |
| INDEXED_GETTER_SETTER_VECTOR(physics_states, forces_world) | |
| INDEXED_GETTER_SETTER_VECTOR(physics_states, linear_acceleration_world) | |
| INDEXED_GETTER_SETTER_VECTOR(physics_states, linear_velocity_world) | |
| INDEXED_GETTER_SETTER_VECTOR(physics_states, position_world) | |
| INDEXED_GETTER_SETTER_VECTOR(physics_states, inverse_inertia_tensor_local) | |
| INDEXED_GETTER_SETTER_VECTOR(physics_states, torques_world) | |
| INDEXED_GETTER_SETTER_VECTOR(physics_states, angular_velocity_world) | |
| INDEXED_GETTER_SETTER_VECTOR(physics_states, angular_velocity_local) | |
| INDEXED_GETTER_SETTER_VECTOR(physics_states, orientation_world) | |
| INDEXED_GETTER_SETTER(physics_states, air_density) | |
| INDEXED_GETTER_SETTER_VECTOR(physics_states, drag_box_size) | |
| INDEXED_GETTER_SETTER_ARRAY(physics_states, drag_coefficient) | |
| INDEXED_GETTER_SETTER_ARRAY(physics_states, drag_pillow_effect) | |
| INDEXED_GETTER_SETTER_VECTOR(physics_states, wind_velocity_world) | |
| INDEXED_GETTER_SETTER_VECTOR(physics_states, gravity_world) | |
| INDEXED_GETTER_SETTER_VECTOR(physics_states, debug_output) | |
| void physics_states_init_index(physics_states *const physics_states, const uintptr_t group_index) { | |
| physics_states->inverse_mass[group_index] = CONSTANT(1.0 / 0.5); // 500g | |
| physics_states->forces_world[group_index] = (vector3){0, 0, 0}; | |
| physics_states->linear_velocity_world[group_index] = (vector3){0, 0, 0}; | |
| physics_states->linear_acceleration_world[group_index] = (vector3){0, 0, 0}; | |
| physics_states->position_world[group_index] = (vector3){0, 0, 0}; | |
| // physics_states->inverse_inertia_tensor_local[group_index] = (upper_diagonal_matrix3){1, 0, 0, 1, 0, 1}; // identity | |
| physics_states->inverse_inertia_tensor_local[group_index] = (upper_diagonal_matrix3){ | |
| CONSTANT(23.76237), 0, 0, 12, 0, CONSTANT(23.76237)}; | |
| physics_states->torques_world[group_index] = (vector3){0, 0, 0}; | |
| physics_states->angular_velocity_world[group_index] = (vector3){0, 0, 0}; | |
| physics_states->angular_velocity_local[group_index] = (vector3){0, 0, 0}; | |
| physics_states->orientation_world[group_index] = (quaternion){0, 0, 0, 1}; | |
| physics_states->gravity_world[group_index] = (vector3){0, CONSTANT(-9.80665), 0}; | |
| physics_states->air_density[group_index] = 1.225f; // kg/m³ (at sea level) | |
| physics_states->drag_box_size[group_index] = (vector3){CONSTANT(0.4), CONSTANT(0.1), CONSTANT(0.35)}; | |
| for (uintptr_t face = 0; face < BOX_FACE_COUNT; ++face) { | |
| physics_states->drag_coefficient[group_index][face] = CONSTANT(0.5); | |
| physics_states->drag_pillow_effect[group_index][face] = CONSTANT(0.5); | |
| } | |
| physics_states->drag_coefficient[group_index][BOX_FACE_NY] = CONSTANT(1); | |
| physics_states->drag_coefficient[group_index][BOX_FACE_PY] = CONSTANT(1); | |
| physics_states->wind_velocity_world[group_index] = (vector3){0, 0, 0}; | |
| physics_states->wind_air_pocket_sample_position_world[group_index] = (vector3){0, 0, 0}; | |
| physics_states->wind_air_pocket_width[group_index] = 0.5f; | |
| physics_states->wind_air_pocket_amplitude[group_index] = 0.0f; | |
| physics_states->wind_air_pocket_velocity_scale[group_index] = 1.0f; | |
| physics_states->debug_output[group_index] = (vector3){0, 0, 0}; | |
| } | |
| EXPORT void physics_states_init(physics_states *const physics_states) { | |
| physics_states->count = 0; | |
| physics_states->enable_ground_plane = 1; | |
| for (uintptr_t i = 0; i < BODY_GROUP_COUNT; ++i) { | |
| physics_states_init_index(physics_states, i); | |
| } | |
| } | |
| EXPORT void physics_states_set_count(physics_states *const physics_states, const uintptr_t count) { | |
| physics_states->count = count; | |
| } | |
| EXPORT uintptr_t physics_states_get_count(physics_states *const physics_states) { | |
| return physics_states->count; | |
| } | |
| EXPORT void physics_states_set_enable_ground_plane(physics_states *const physics_states, const bool enable) { | |
| physics_states->enable_ground_plane = enable; | |
| } | |
| EXPORT bool physics_states_get_enable_ground_plane(physics_states *const physics_states) { | |
| return physics_states->enable_ground_plane; | |
| } | |
| EXPORT void physics_states_update(physics_states *const physics_states, const component delta_time) { | |
| const number dt = delta_time; | |
| const number dt_half = dt * CONSTANT(0.5); | |
| const number inverse_dt = CONSTANT(1) / dt; | |
| const uintptr_t group_count = ceil_division(physics_states->count, NUMBER_COMPONENTS); | |
| for (uintptr_t i = 0; i < group_count; ++i) { | |
| number inverse_mass = physics_states->inverse_mass[i]; | |
| vector3 *forces_world = &physics_states->forces_world[i]; | |
| vector3 *linear_velocity_world = &physics_states->linear_velocity_world[i]; | |
| vector3 *linear_acceleration_world = &physics_states->linear_acceleration_world[i]; | |
| vector3 *position_world = &physics_states->position_world[i]; | |
| vector3 *torques_world = &physics_states->torques_world[i]; | |
| vector3 *angular_velocity_world = &physics_states->angular_velocity_world[i]; | |
| vector3 *angular_velocity_local = &physics_states->angular_velocity_local[i]; | |
| quaternion *orientation_world = &physics_states->orientation_world[i]; | |
| vector3 *gravity_world = &physics_states->gravity_world[i]; | |
| vector3 drag_box_size = physics_states->drag_box_size[i]; | |
| vector3 drag_box_half_size = vector3_scalar_multiply(CONSTANT(0.5), drag_box_size); | |
| vector3 wind_velocity_world = physics_states->wind_velocity_world[i]; | |
| number air_density = physics_states->air_density[i]; | |
| vector3 *wind_air_pocket_sample_position_world = &physics_states->wind_air_pocket_sample_position_world[i]; | |
| // Integrate wind sample position so that it moves around with the wind | |
| *wind_air_pocket_sample_position_world = vector3_add(*wind_air_pocket_sample_position_world, | |
| vector3_scalar_multiply(dt, wind_velocity_world)); | |
| quaternion orientation_world_to_local = quaternion_normalized_inverse(*orientation_world); | |
| vector3 linear_velocity_local = rotate_vector_by_quaternion( | |
| *linear_velocity_world, | |
| orientation_world_to_local); | |
| // https://projectborealis.com/docs/WADS_rev2.pdf | |
| // https://projectborealis.com/wind-and-air-drag-system/ | |
| // Approximate drag per box face [+X, -X, +Y, -Y, +Z, -Z] | |
| for (uintptr_t face = 0; face < BOX_FACE_COUNT; ++face) { | |
| uintptr_t axis = face / 2; | |
| uintptr_t positive = face % 2 == 0; | |
| // Determine face normal and center offset in local space | |
| vector3 normal_local = (vector3){0, 0, 0}; | |
| normal_local.array[axis] = positive ? +1 : -1; | |
| // World-space normal & center of face | |
| vector3 normal_world = rotate_vector_by_quaternion(normal_local, *orientation_world); | |
| vector3 face_center_offset_world = vector3_scalar_multiply(drag_box_half_size.array[axis], normal_world); | |
| vector3 face_center_world = vector3_add(*position_world, face_center_offset_world); | |
| // Get relative airflow at this point, wind minus body point-velocity | |
| vector3 face_center_velocity_world = point_velocity_from_offset_world(*linear_velocity_world, *angular_velocity_world, face_center_offset_world); | |
| number turbulence_amplitude = physics_states->wind_air_pocket_amplitude[i] + physics_states->wind_air_pocket_velocity_scale[i] * vector3_length(wind_velocity_world); | |
| vector3 turbulence_sample_position_world = vector3_scalar_multiply( | |
| CONSTANT(1) / physics_states->wind_air_pocket_width[i], | |
| vector3_add(*wind_air_pocket_sample_position_world, face_center_world)); | |
| vector3 turbulence_velocity_world = vector3_scalar_multiply( | |
| turbulence_amplitude, smooth_noise_simple_vector3(turbulence_sample_position_world)); | |
| vector3 relative_air_velocity_world = vector3_add(turbulence_velocity_world, vector3_subtract(wind_velocity_world, face_center_velocity_world)); | |
| vector3 realtive_air_direction_world = vector3_normalize(relative_air_velocity_world, EPSILON); | |
| // Keep only faces facing the wind (this will 0 out all calculations for opposing faces) | |
| number air_direction_projected_on_face = __builtin_elementwise_max( | |
| -vector3_dot(normal_world, realtive_air_direction_world), CONSTANT(0)); | |
| // Face area (world-space) depends on which axis is normal | |
| // Note that because we don't implement scale or hierarchies, local box size = world box size | |
| number face_area_world; | |
| switch (axis) { | |
| case 0: // X face | |
| face_area_world = drag_box_size.y * drag_box_size.z; | |
| break; | |
| case 1: // Y face | |
| face_area_world = drag_box_size.x * drag_box_size.z; | |
| break; | |
| case 2: // Z face | |
| face_area_world = drag_box_size.x * drag_box_size.y; | |
| break; | |
| default: | |
| abort(); | |
| } | |
| // Projected area = area * cos(angle between face normal and air direction) | |
| number face_area_projected_world = face_area_world * air_direction_projected_on_face; | |
| // Get this face's pillow alpha | |
| number drag_pillow_effect = physics_states->drag_pillow_effect[i][face]; | |
| // The pressure falls off radially, we compute the average falloff from integration | |
| number drag_pillow_effect_force_multiplier = (PI - CONSTANT(4)) * drag_pillow_effect / CONSTANT(4) + CONSTANT(1); | |
| number drag_coefficient = physics_states->drag_coefficient[i][face]; | |
| // Translational drag magnitude (1/2 * rho * v^2 * Aproj) * coefficient * pillow | |
| number linear_drag_magnitude = | |
| 0.5f * | |
| air_density * | |
| vector3_length_squared(relative_air_velocity_world) * | |
| face_area_projected_world * | |
| drag_coefficient * | |
| drag_pillow_effect_force_multiplier; | |
| // Compute center-of-pressure shift along face (in face-plane) | |
| // Find the tangent of flow on the face | |
| vector3 relative_air_tangent_world = | |
| vector3_normalize(vector3_subtract(realtive_air_direction_world, vector3_scalar_multiply(air_direction_projected_on_face, normal_world)), EPSILON); | |
| vector3 relative_air_tangent_local = rotate_vector_by_quaternion(relative_air_tangent_world, orientation_world_to_local); | |
| // This is an approximation of the half size along the tangent | |
| number half_size_tangent = vector3_dot(vector3_abs(relative_air_tangent_local), drag_box_half_size); | |
| // Shift fraction is affected by pillow effect (0=center, 1=trailing edge) | |
| // We don't have scale or hierarchies, local distance = world distance | |
| number shift_distance = half_size_tangent * drag_pillow_effect; | |
| vector3 shift_world = vector3_scalar_multiply(shift_distance, relative_air_tangent_world); | |
| vector3 center_of_pressure_offset_world = vector3_add(face_center_offset_world, shift_world); | |
| // Apply translational drag | |
| vector3 drag_force_world = vector3_scalar_multiply(-linear_drag_magnitude, normal_world); | |
| vector3 drag_torque_from_force_world = apply_force_get_torque_world(drag_force_world, center_of_pressure_offset_world); | |
| *forces_world = vector3_add(*forces_world, drag_force_world); | |
| *torques_world = vector3_add(*torques_world, drag_torque_from_force_world); | |
| // Rotational drag reduces with the pillow effect | |
| number drag_pillow_effect_rotation_drag = (CONSTANT(15) - CONSTANT(7) * drag_pillow_effect) / CONSTANT(60); | |
| // Rotational speed at that lever | |
| number rotational_speed_squared = vector3_length_squared(*angular_velocity_world) * vector3_length_squared(center_of_pressure_offset_world); | |
| number rotational_drag_magnitude = | |
| 0.5f * | |
| air_density * | |
| rotational_speed_squared * | |
| face_area_projected_world * | |
| drag_coefficient * | |
| drag_pillow_effect_rotation_drag; | |
| vector3 drag_torque_world = vector3_scalar_multiply(rotational_drag_magnitude, vector3_cross(normal_world, center_of_pressure_offset_world)); | |
| *torques_world = vector3_add(*torques_world, drag_torque_world); | |
| } | |
| vector3 prev_linear_velocity_world = *linear_velocity_world; | |
| *linear_velocity_world = vector3_add(*linear_velocity_world, | |
| vector3_scalar_multiply(dt * inverse_mass, *forces_world)); | |
| // Gravity is applied as an acceleration, not as a force | |
| *linear_velocity_world = vector3_add(*linear_velocity_world, | |
| vector3_scalar_multiply(dt, *gravity_world)); | |
| *position_world = vector3_add(*position_world, | |
| vector3_scalar_multiply(dt, *linear_velocity_world)); | |
| vector3 torque_local = rotate_vector_by_quaternion( | |
| *torques_world, | |
| orientation_world_to_local); | |
| // Clear the existing forces and torques so that next iteration we won't use the same ones | |
| *forces_world = (vector3){0, 0, 0}; | |
| *torques_world = (vector3){0, 0, 0}; | |
| // Apply inverse inertia tensor (local space) | |
| vector3 angular_acceleration_local = { | |
| torque_local.x * physics_states->inverse_inertia_tensor_local[i].m00 + | |
| torque_local.y * physics_states->inverse_inertia_tensor_local[i].m01 + | |
| torque_local.z * physics_states->inverse_inertia_tensor_local[i].m02, | |
| torque_local.x * physics_states->inverse_inertia_tensor_local[i].m01 + | |
| torque_local.y * physics_states->inverse_inertia_tensor_local[i].m11 + | |
| torque_local.z * physics_states->inverse_inertia_tensor_local[i].m12, | |
| torque_local.x * physics_states->inverse_inertia_tensor_local[i].m02 + | |
| torque_local.y * physics_states->inverse_inertia_tensor_local[i].m12 + | |
| torque_local.z * physics_states->inverse_inertia_tensor_local[i].m22}; | |
| // Convert angular acceleration back to world-space | |
| vector3 angular_acceleration_world = rotate_vector_by_quaternion( | |
| angular_acceleration_local, | |
| *orientation_world); | |
| // Integrate angular velocity in world-space | |
| *angular_velocity_world = vector3_add( | |
| *angular_velocity_world, | |
| vector3_scalar_multiply(dt, angular_acceleration_world)); | |
| vector3 omega_angular_velocity_local = rotate_vector_by_quaternion( | |
| *angular_velocity_world, | |
| orientation_world_to_local); | |
| // https://www.ashwinnarayan.com/post/how-to-integrate-quaternions/ | |
| quaternion omega_local = (quaternion){ | |
| omega_angular_velocity_local.x * dt_half, | |
| omega_angular_velocity_local.y * dt_half, | |
| omega_angular_velocity_local.z * dt_half, | |
| 0.0f}; | |
| *orientation_world = quaternion_add(*orientation_world, | |
| quaternion_multiply(*orientation_world, omega_local)); | |
| *orientation_world = quaternion_normalize(*orientation_world); | |
| orientation_world_to_local = quaternion_normalized_inverse(*orientation_world); | |
| *angular_velocity_local = rotate_vector_by_quaternion( | |
| *angular_velocity_world, | |
| orientation_world_to_local); | |
| // TODO(trevor): Make this a property or something, apply damping | |
| //*linear_velocity_world = vector3_scalar_multiply(0.9999f, *linear_velocity_world); | |
| //*angular_velocity_world = vector3_scalar_multiply(0.9999f, *angular_velocity_world); | |
| // This is a VERY simple and approximate collision with an infinite ground plane | |
| // This is not intended to be a correct approach to collision whatsoever, it's only made | |
| // so that if you run this physics sim by itself, the drone will have a plane to rest on | |
| // The intention of this library is to be coupled with a physics engine that performs collision | |
| if (physics_states->enable_ground_plane) { | |
| // We abs the comparison because OpenCL vectors return -1 | |
| number positive_y = number_from_bool(position_world->y >= (number)0); | |
| position_world->y *= positive_y; | |
| linear_velocity_world->y *= positive_y; | |
| number smooth_interpolate = __builtin_elementwise_max(positive_y, (number)0.999); | |
| // Slow the drone down (like drag) | |
| linear_velocity_world->x *= smooth_interpolate; | |
| linear_velocity_world->z *= smooth_interpolate; | |
| angular_velocity_world->x *= smooth_interpolate; | |
| angular_velocity_world->y *= smooth_interpolate; | |
| angular_velocity_world->z *= smooth_interpolate; | |
| // Reset the orientation of the drone to the identity if we hit the ground plane | |
| orientation_world->x *= smooth_interpolate; | |
| orientation_world->z *= smooth_interpolate; | |
| *orientation_world = quaternion_normalize(*orientation_world); | |
| } | |
| *linear_acceleration_world = vector3_subtract( | |
| vector3_scalar_multiply(inverse_dt, vector3_subtract(*linear_velocity_world, prev_linear_velocity_world)), | |
| *gravity_world); | |
| } | |
| } | |
| typedef struct { | |
| number motor_outputs[BODY_GROUP_COUNT][MOTOR_COUNT]; | |
| vector3 motor_position_local[BODY_GROUP_COUNT][MOTOR_COUNT]; | |
| number propeller_force[BODY_GROUP_COUNT][MOTOR_COUNT]; | |
| number propeller_torque_rotation[BODY_GROUP_COUNT][MOTOR_COUNT]; | |
| number propeller_torque_arm[BODY_GROUP_COUNT][MOTOR_COUNT]; | |
| vector3 ground_plane_normal_world[BODY_GROUP_COUNT][MOTOR_COUNT]; | |
| number ground_plane_distance_world[BODY_GROUP_COUNT][MOTOR_COUNT]; | |
| number max_motor_with_prop_rpm[BODY_GROUP_COUNT]; | |
| number prop_pitch_meters[BODY_GROUP_COUNT]; | |
| number prop_diameter_meters[BODY_GROUP_COUNT]; | |
| number thrust_to_torque_ratio[BODY_GROUP_COUNT]; | |
| number barometer_reference_height[BODY_GROUP_COUNT]; | |
| number barometer_reference_pressure_pa[BODY_GROUP_COUNT]; | |
| number barometer_reference_temperature_kelvin[BODY_GROUP_COUNT]; | |
| number barometer_pressure_pa[BODY_GROUP_COUNT]; | |
| } drone_states; | |
| STATES_FUNCTIONS(drone_states) | |
| INDEXED_GETTER_SETTER_ARRAY(drone_states, motor_outputs) | |
| INDEXED_GETTER_SETTER_ARRAY_VECTOR(drone_states, motor_position_local) | |
| INDEXED_GETTER_SETTER_ARRAY(drone_states, propeller_force) | |
| INDEXED_GETTER_SETTER_ARRAY(drone_states, propeller_torque_rotation) | |
| INDEXED_GETTER_SETTER_ARRAY(drone_states, propeller_torque_arm) | |
| INDEXED_GETTER_SETTER_ARRAY_VECTOR(drone_states, ground_plane_normal_world) | |
| INDEXED_GETTER_SETTER_ARRAY(drone_states, ground_plane_distance_world) | |
| INDEXED_GETTER_SETTER(drone_states, max_motor_with_prop_rpm) | |
| INDEXED_GETTER_SETTER(drone_states, prop_pitch_meters) | |
| INDEXED_GETTER_SETTER(drone_states, prop_diameter_meters) | |
| INDEXED_GETTER_SETTER(drone_states, thrust_to_torque_ratio) | |
| INDEXED_GETTER_SETTER(drone_states, barometer_reference_height) | |
| INDEXED_GETTER_SETTER(drone_states, barometer_reference_pressure_pa) | |
| INDEXED_GETTER_SETTER(drone_states, barometer_reference_temperature_kelvin) | |
| INDEXED_GETTER_SETTER(drone_states, barometer_pressure_pa) | |
| void drone_states_init_index(drone_states *const drone_states, const uintptr_t group_index) { | |
| for (uintptr_t m = 0; m < MOTOR_COUNT; ++m) { | |
| drone_states->motor_outputs[group_index][m] = 0; | |
| drone_states->propeller_force[group_index][m] = 0; | |
| drone_states->propeller_torque_rotation[group_index][m] = 0; | |
| drone_states->propeller_torque_arm[group_index][m] = 0; | |
| drone_states->ground_plane_normal_world[group_index][m] = (vector3){0, 1, 0}; | |
| drone_states->ground_plane_distance_world[group_index][m] = 0; | |
| } | |
| drone_states->motor_position_local[group_index][0] = (vector3){+0.33, 0, -0.29}; | |
| drone_states->motor_position_local[group_index][1] = (vector3){+0.33, 0, +0.29}; | |
| drone_states->motor_position_local[group_index][2] = (vector3){-0.33, 0, -0.29}; | |
| drone_states->motor_position_local[group_index][3] = (vector3){-0.33, 0, +0.29}; | |
| drone_states->max_motor_with_prop_rpm[group_index] = 10000; | |
| drone_states->prop_pitch_meters[group_index] = 0.1524f; // 6in | |
| drone_states->prop_diameter_meters[group_index] = 0.254f; // 10in | |
| drone_states->thrust_to_torque_ratio[group_index] = 0.4f; | |
| drone_states->barometer_reference_height[group_index] = 0; // Meters above sea level | |
| drone_states->barometer_reference_pressure_pa[group_index] = 101325; // Standard Atmosphere (average at sea level) | |
| drone_states->barometer_reference_temperature_kelvin[group_index] = CONSTANT(288.15); // Standard Temperature in Kelvin at sea level | |
| drone_states->barometer_pressure_pa[group_index] = 101325; // Standard Atmosphere (average at sea level) | |
| } | |
| EXPORT void drone_states_init(drone_states *const drone_states) { | |
| for (uintptr_t i = 0; i < BODY_GROUP_COUNT; ++i) { | |
| drone_states_init_index(drone_states, i); | |
| } | |
| } | |
| component get_motor_direction(bool motors_reversed, uintptr_t motor_index) { | |
| component direction = 1; | |
| if (motor_index == MOTOR_BR || motor_index == MOTOR_FL) { | |
| direction = -1; | |
| } | |
| return motors_reversed ? -direction : direction; | |
| } | |
| EXPORT void drone_states_update(drone_states *const drone_states, physics_states *const physics_states) { | |
| // TODO(trevor): Save values that we can precompute once, such as prop radius, radius^2, etc. | |
| // Anything using values that don't often change too (such as the pitch and diameter). | |
| const uintptr_t group_count = ceil_division(physics_states->count, NUMBER_COMPONENTS); | |
| for (uintptr_t i = 0; i < group_count; ++i) { | |
| vector3 *position_world = &physics_states->position_world[i]; | |
| quaternion *orientation_world = &physics_states->orientation_world[i]; | |
| vector3 *gravity_world = &physics_states->gravity_world[i]; | |
| vector3 *forces_world = &physics_states->forces_world[i]; | |
| vector3 *torques_world = &physics_states->torques_world[i]; | |
| vector3 up_world = rotate_vector_by_quaternion((vector3){0, 1, 0}, *orientation_world); | |
| // Clear the existing forces and we will accumulate new ones | |
| *forces_world = (vector3){0, 0, 0}; | |
| *torques_world = (vector3){0, 0, 0}; | |
| for (uintptr_t m = 0; m < MOTOR_COUNT; ++m) { | |
| vector3 *motor_position_local = &drone_states->motor_position_local[i][m]; | |
| const vector3 motor_offset_world = rotate_vector_by_quaternion(*motor_position_local, *orientation_world); | |
| vector3 propeller_velocity_world = point_velocity_from_offset_world( | |
| physics_states->linear_velocity_world[i], | |
| physics_states->angular_velocity_world[i], | |
| motor_offset_world); | |
| number air_speed_at_prop = vector3_dot(up_world, propeller_velocity_world); | |
| // https://www.electricrcaircraftguy.com/2014/04/propeller-static-dynamic-thrust-equation-background.html | |
| // https://www.tytorobotics.com/blogs/articles/how-to-calculate-propeller-thrust | |
| // https://web.archive.org/web/20190402064809/https://quadstardrones.com/the-equations-for-speed/ | |
| number motor_output = drone_states->motor_outputs[i][m]; | |
| number rotations_per_second = drone_states->max_motor_with_prop_rpm[i] * motor_output / 60.0f; | |
| number rps_pitch_term = rotations_per_second * drone_states->prop_pitch_meters[i]; | |
| number propeller_force = physics_states->air_density[i] * (PI * __builtin_elementwise_pow(drone_states->prop_diameter_meters[i], (number)2) / 4.0f) * | |
| (__builtin_elementwise_pow(rps_pitch_term, (number)2) - rps_pitch_term * air_speed_at_prop) * | |
| __builtin_elementwise_pow(drone_states->prop_diameter_meters[i] / (3.29546f * drone_states->prop_pitch_meters[i]), (number)1.5f); | |
| propeller_force = __builtin_elementwise_max(propeller_force, (number)0); | |
| // Compute ground effect | |
| // Leishman, J. G., Principles of Helicopter Aerodynamics, 2nd. Ed., p.258 - 260, Cambridge University Press, New York, 2006. | |
| // https://cdcl.umd.edu/papers/jahs16.pdf | |
| // https://www.researchgate.net/figure/Flowfield-of-ring-source-potential-flow-model-evaluated-at-various-heights-depicting_fig4_282826775 | |
| // According to the Cheeseman and Bennett model it is only supposed to be applied | |
| // when height ratio is in the range [0.5, 2] and assumes there is landing gear keeping it above that point | |
| // However, our solution is to clamp the height to never be less than half the prop radius | |
| // TODO(trevor): Research extreme ground effect at height ratio < 0.5 | |
| // https://www.sciencedirect.com/science/article/pii/S1000936124000645 | |
| vector3 motor_position_world = vector3_add(*position_world, motor_offset_world); | |
| number prop_radius_meters = drone_states->prop_diameter_meters[i] / (number)2; | |
| number half_prop_radius_meters = prop_radius_meters / (number)2; | |
| number height_above_ground_meters = __builtin_elementwise_max(drone_states->ground_plane_distance_world[i][m] + | |
| vector3_dot(drone_states->ground_plane_normal_world[i][m], motor_position_world), | |
| half_prop_radius_meters); | |
| number normalized_height_ratio = height_above_ground_meters / prop_radius_meters; | |
| number in_ground_effect = number_from_bool(normalized_height_ratio < (number)2); | |
| number height_term = 16 * height_above_ground_meters * height_above_ground_meters; | |
| number ground_effect_scale = height_term / (height_term - prop_radius_meters * prop_radius_meters); | |
| number in_ground_effect_scale = ground_effect_scale * in_ground_effect + ((number)1 - in_ground_effect); | |
| propeller_force *= in_ground_effect_scale; | |
| vector3 propeller_force_world = vector3_scalar_multiply(propeller_force, up_world); | |
| vector3 propeller_torque_arm_world = apply_force_get_torque_world( | |
| propeller_force_world, | |
| motor_offset_world); | |
| // https://mechtex.com/blog/understanding-torque-and-thrust-in-drone-motor | |
| number propeller_torque_rotation = propeller_force * drone_states->thrust_to_torque_ratio[i] * get_motor_direction(0, m); | |
| vector3 propeller_torque_rotation_world = vector3_scalar_multiply(propeller_torque_rotation, up_world); | |
| *forces_world = vector3_add(*forces_world, propeller_force_world); | |
| *torques_world = vector3_add(*torques_world, vector3_add(propeller_torque_rotation_world, propeller_torque_arm_world)); | |
| drone_states->propeller_force[i][m] = propeller_force; | |
| drone_states->propeller_torque_rotation[i][m] = propeller_torque_rotation; | |
| drone_states->propeller_torque_arm[i][m] = vector3_length(propeller_torque_arm_world); | |
| } | |
| physics_states->debug_output[i] = physics_states->torques_world[i]; | |
| // https://en.wikipedia.org/wiki/Barometric_formula | |
| number gravity_magnitude = vector3_length(*gravity_world); | |
| vector3 gravity_up_direction = vector3_scalar_multiply(1 / -number_fix_positive_divisor(gravity_magnitude, EPSILON), *gravity_world); | |
| number height = vector3_dot(gravity_up_direction, *position_world); | |
| number height_above_reference = height - drone_states->barometer_reference_height[i]; | |
| drone_states->barometer_pressure_pa[i] = | |
| drone_states->barometer_reference_pressure_pa[i] * | |
| __builtin_elementwise_pow(E, -gravity_magnitude * MOLAR_MASS_AIR * height_above_reference / (UNIVERSAL_GAS_CONSTANT * drone_states->barometer_reference_temperature_kelvin[i])); | |
| } | |
| } | |
| typedef union { | |
| struct | |
| { | |
| number pitch; | |
| number yaw; | |
| number roll; | |
| number throttle; | |
| }; | |
| number array[4]; | |
| } motor_mix; | |
| typedef struct { | |
| number arm_command[BODY_GROUP_COUNT]; | |
| number mode_command[BODY_GROUP_COUNT]; | |
| number throttle_low[BODY_GROUP_COUNT]; | |
| number throttle_mid[BODY_GROUP_COUNT]; | |
| number throttle_hover_point[BODY_GROUP_COUNT]; | |
| number axis_command[BODY_GROUP_COUNT][ALL_AXIS_COUNT]; | |
| number axis_expo[BODY_GROUP_COUNT][ALL_AXIS_COUNT]; | |
| number rates_center_sensitivity[BODY_GROUP_COUNT][PRIMARY_AXIS_COUNT]; | |
| number rates_max_rate[BODY_GROUP_COUNT][PRIMARY_AXIS_COUNT]; | |
| motor_mix motor_mix[BODY_GROUP_COUNT][MOTOR_COUNT]; | |
| // If this is non-zero, then we disable the PID and use this as a direct scalar for stick inputs | |
| number no_pid_stick_scale[BODY_GROUP_COUNT]; | |
| number pid_p[BODY_GROUP_COUNT][PRIMARY_AXIS_COUNT]; | |
| number pid_i[BODY_GROUP_COUNT][PRIMARY_AXIS_COUNT]; | |
| number pid_d[BODY_GROUP_COUNT][PRIMARY_AXIS_COUNT]; | |
| number pid_error[BODY_GROUP_COUNT][PRIMARY_AXIS_COUNT]; | |
| number pid_error_sum[BODY_GROUP_COUNT][PRIMARY_AXIS_COUNT]; | |
| number pid_p_term[BODY_GROUP_COUNT][PRIMARY_AXIS_COUNT]; | |
| number pid_i_term[BODY_GROUP_COUNT][PRIMARY_AXIS_COUNT]; | |
| number pid_d_term[BODY_GROUP_COUNT][PRIMARY_AXIS_COUNT]; | |
| } controller_states; | |
| STATES_FUNCTIONS(controller_states) | |
| INDEXED_GETTER_SETTER(controller_states, arm_command) | |
| INDEXED_GETTER_SETTER(controller_states, mode_command) | |
| INDEXED_GETTER_SETTER(controller_states, throttle_low) | |
| INDEXED_GETTER_SETTER(controller_states, throttle_mid) | |
| INDEXED_GETTER_SETTER(controller_states, throttle_hover_point) | |
| INDEXED_GETTER_SETTER_ARRAY(controller_states, axis_command) | |
| INDEXED_GETTER_SETTER_ARRAY(controller_states, axis_expo) | |
| INDEXED_GETTER_SETTER_ARRAY(controller_states, rates_center_sensitivity) | |
| INDEXED_GETTER_SETTER_ARRAY(controller_states, rates_max_rate) | |
| INDEXED_GETTER_SETTER(controller_states, no_pid_stick_scale) | |
| INDEXED_GETTER_SETTER_ARRAY_VECTOR(controller_states, motor_mix) | |
| INDEXED_GETTER_SETTER_ARRAY(controller_states, pid_p) | |
| INDEXED_GETTER_SETTER_ARRAY(controller_states, pid_i) | |
| INDEXED_GETTER_SETTER_ARRAY(controller_states, pid_d) | |
| INDEXED_GETTER_SETTER_ARRAY(controller_states, pid_error) | |
| INDEXED_GETTER_SETTER_ARRAY(controller_states, pid_error_sum) | |
| INDEXED_GETTER_SETTER_ARRAY(controller_states, pid_p_term) | |
| INDEXED_GETTER_SETTER_ARRAY(controller_states, pid_i_term) | |
| INDEXED_GETTER_SETTER_ARRAY(controller_states, pid_d_term) | |
| void controller_states_init_index(controller_states *const controller_states, const uintptr_t group_index) { | |
| controller_states->arm_command[group_index] = -1; | |
| controller_states->mode_command[group_index] = -1; | |
| controller_states->throttle_low[group_index] = CONSTANT(0.05); | |
| controller_states->throttle_mid[group_index] = 0.4; | |
| controller_states->throttle_hover_point[group_index] = 0; | |
| controller_states->axis_command[group_index][AXIS_THROTTLE] = -1; | |
| controller_states->axis_expo[group_index][AXIS_THROTTLE] = 0.7; | |
| controller_states->no_pid_stick_scale[group_index] = 0; | |
| for (uintptr_t axis = 0; axis < PRIMARY_AXIS_COUNT; ++axis) { | |
| controller_states->axis_command[group_index][axis] = 0; | |
| controller_states->axis_expo[group_index][axis] = 0; | |
| controller_states->rates_center_sensitivity[group_index][axis] = 70; | |
| controller_states->rates_max_rate[group_index][axis] = 670; | |
| controller_states->pid_error[group_index][axis] = 0; | |
| controller_states->pid_error_sum[group_index][axis] = 0; | |
| controller_states->pid_p_term[group_index][axis] = 0; | |
| controller_states->pid_i_term[group_index][axis] = 0; | |
| controller_states->pid_d_term[group_index][axis] = 0; | |
| } | |
| controller_states->pid_p[group_index][AXIS_PITCH] = CONSTANT(0.005); | |
| controller_states->pid_i[group_index][AXIS_PITCH] = CONSTANT(0.00001); | |
| controller_states->pid_d[group_index][AXIS_PITCH] = CONSTANT(0.005); | |
| controller_states->pid_p[group_index][AXIS_YAW] = CONSTANT(0.003); | |
| controller_states->pid_i[group_index][AXIS_YAW] = CONSTANT(0.000005); | |
| controller_states->pid_d[group_index][AXIS_YAW] = CONSTANT(0.003); | |
| controller_states->pid_p[group_index][AXIS_ROLL] = CONSTANT(0.005); | |
| controller_states->pid_i[group_index][AXIS_ROLL] = CONSTANT(0.00001); | |
| controller_states->pid_d[group_index][AXIS_ROLL] = CONSTANT(0.005); | |
| // Setup default quadcopter motor mixing: P Y R T | |
| controller_states->motor_mix[group_index][MOTOR_BR] = (motor_mix){+1, -1, -1, +1}; | |
| controller_states->motor_mix[group_index][MOTOR_FR] = (motor_mix){-1, +1, -1, +1}; | |
| controller_states->motor_mix[group_index][MOTOR_BL] = (motor_mix){+1, +1, +1, +1}; | |
| controller_states->motor_mix[group_index][MOTOR_FL] = (motor_mix){-1, -1, +1, +1}; | |
| } | |
| EXPORT void controller_states_init(controller_states *const controller_states) { | |
| for (uintptr_t i = 0; i < BODY_GROUP_COUNT; ++i) { | |
| controller_states_init_index(controller_states, i); | |
| } | |
| } | |
| number controller_states_angular_rate_degrees(number command, number center_sensitivity, number max_rate, number expo) { | |
| // This is based on "Actual" rates mode, the easiest to configure and understand | |
| number expo_curved = __builtin_elementwise_abs(command) * (__builtin_elementwise_pow(command, CONSTANT(5)) * expo + command * (1 - expo)); | |
| number stick_movement = __builtin_elementwise_max(CONSTANT(0), max_rate - center_sensitivity); | |
| return command * center_sensitivity + stick_movement * expo_curved; | |
| } | |
| EXPORT float controller_states_angular_rate_degrees_query(float command, float center_sensitivity, float max_rate, float expo) { | |
| // This is not the most efficient since it uses vector operations but we don't intend to query this function often | |
| return NUMBER_ACCESS(controller_states_angular_rate_degrees(command, center_sensitivity, max_rate, expo), 0); | |
| } | |
| EXPORT void controller_states_update(controller_states *const controller_states, physics_states *const physics_states, drone_states *const drone_states) { | |
| const uintptr_t group_count = ceil_division(physics_states->count, NUMBER_COMPONENTS); | |
| for (uintptr_t i = 0; i < group_count; ++i) { | |
| vector3 *angular_velocity_local = &physics_states->angular_velocity_local[i]; | |
| number normalized_throttle = controller_states->axis_command[i][AXIS_THROTTLE] * CONSTANT(0.5) + CONSTANT(0.5); | |
| number is_armed = number_from_bool(controller_states->arm_command[i] > CONSTANT(0)); | |
| number is_throttle_active = number_from_bool(normalized_throttle > controller_states->throttle_low[i]); | |
| number per_axis_command[PRIMARY_AXIS_COUNT]; | |
| for (uintptr_t axis = 0; axis < PRIMARY_AXIS_COUNT; ++axis) { | |
| number current_angular_velocity_degrees = angular_velocity_local->array[axis] * RADIAN_TO_DEGREE; | |
| // We need to map from stick direction to our reference frame | |
| // For example when we pitch forward, the stick is going in the positive direction, and similarly the rotation | |
| // around the X axis is also positive (same wih yaw). However, when we roll to the right (positive stick) the | |
| // rotation is negative around the Z. This is due to us using a left handed coordinate system. | |
| const number stick_to_axis_direction = axis == AXIS_ROLL ? -1 : 1; | |
| number target_angular_velocity_degrees = controller_states_angular_rate_degrees( | |
| controller_states->axis_command[i][axis] * stick_to_axis_direction, | |
| controller_states->rates_center_sensitivity[i][axis], | |
| controller_states->rates_max_rate[i][axis], | |
| controller_states->axis_expo[i][axis]); | |
| number *pid_error = &controller_states->pid_error[i][axis]; | |
| number *pid_error_sum = &controller_states->pid_error_sum[i][axis]; | |
| number error = (target_angular_velocity_degrees - current_angular_velocity_degrees) * is_armed; | |
| number error_delta = error - *pid_error; | |
| number error_sum = (*pid_error_sum + error) * is_armed * is_throttle_active; | |
| *pid_error = error; | |
| *pid_error_sum = error_sum; | |
| number p_term = controller_states->pid_p[i][axis] * error; | |
| number i_term = controller_states->pid_i[i][axis] * error_sum; | |
| number d_term = controller_states->pid_d[i][axis] * error_delta; | |
| controller_states->pid_p_term[i][axis] = p_term; | |
| controller_states->pid_i_term[i][axis] = i_term; | |
| controller_states->pid_d_term[i][axis] = d_term; | |
| number use_pid = number_from_bool(controller_states->no_pid_stick_scale[i] == CONSTANT(0)); | |
| number pid_command = p_term + i_term + d_term; | |
| number sticks_command = controller_states->no_pid_stick_scale[i] * | |
| (target_angular_velocity_degrees / controller_states->rates_max_rate[i][axis]); | |
| // The sticks_command will already be 0 if we're using the pid | |
| number command = use_pid * pid_command + sticks_command; | |
| per_axis_command[axis] = clamp(command * stick_to_axis_direction, CONSTANT(-1), CONSTANT(1)); | |
| } | |
| // Motor mixing | |
| for (uintptr_t m = 0; m < MOTOR_COUNT; ++m) { | |
| number motor_output = 0; | |
| for (uintptr_t axis = 0; axis < PRIMARY_AXIS_COUNT; ++axis) { | |
| motor_output += controller_states->motor_mix[i][m].array[axis] * per_axis_command[axis]; | |
| } | |
| motor_output += controller_states->motor_mix[i][m].array[AXIS_THROTTLE] * normalized_throttle; | |
| drone_states->motor_outputs[i][m] = clamp(motor_output * is_armed, CONSTANT(0), CONSTANT(1)); | |
| } | |
| } | |
| } | |
| #ifdef PHYSICS_MAIN | |
| #define TEST_EPSILON 1e-5f | |
| INLINE bool number_nearly_equal(number a, number b) { | |
| return number_nearly_equal_epsilon(a, b, TEST_EPSILON); | |
| } | |
| INLINE bool component_nearly_equal(component a, component b) { | |
| return __builtin_elementwise_abs(a - b) < TEST_EPSILON; | |
| } | |
| INLINE bool vector3_nearly_equal(vector3 a, vector3 b) { | |
| return number_nearly_equal(a.x, b.x) && | |
| number_nearly_equal(a.y, b.y) && | |
| number_nearly_equal(a.z, b.z); | |
| } | |
| INLINE bool quaternion_nearly_equal(quaternion a, quaternion b) { | |
| return number_nearly_equal(a.x, b.x) && | |
| number_nearly_equal(a.y, b.y) && | |
| number_nearly_equal(a.z, b.z) && | |
| number_nearly_equal(a.w, b.w); | |
| } | |
| INLINE void test(const uintptr_t result, const char *const file, const uintptr_t line, const char *const code) { | |
| if (!result) { | |
| printf("%s:%lu: failed test: %s\n", file, line, code); | |
| abort(); | |
| } | |
| } | |
| #define TEST(result) test(result, __FILE__, __LINE__, #result) | |
| void tests() { | |
| // Test component_nearly_equal | |
| { | |
| TEST(component_nearly_equal(+1.0f, +1.0f)); | |
| TEST(component_nearly_equal(-1.0f, -1.0f)); | |
| TEST(component_nearly_equal(+0.0f, +0.0f)); | |
| TEST(!component_nearly_equal(+0.0f, +1.0f)); | |
| TEST(!component_nearly_equal(+1.0f, -1.0f)); | |
| } | |
| // Test number_nearly_equal | |
| { | |
| TEST(number_nearly_equal(+1.0f, +1.0f)); | |
| TEST(number_nearly_equal(-1.0f, -1.0f)); | |
| TEST(number_nearly_equal(+0.0f, +0.0f)); | |
| TEST(!number_nearly_equal(+0.0f, +1.0f)); | |
| TEST(!number_nearly_equal(+1.0f, -1.0f)); | |
| } | |
| // Test quaternion_length | |
| { | |
| TEST(number_nearly_equal(quaternion_length((quaternion){0, 0, 0, 0}), 0)); | |
| TEST(number_nearly_equal(quaternion_length((quaternion){0, 0, 0, 1}), 1)); | |
| TEST(number_nearly_equal(quaternion_length((quaternion){1, 2, 2, 1}), 3.16227766017f)); | |
| TEST(number_nearly_equal(quaternion_length((quaternion){2, 1, 1, 2}), 3.16227766017f)); | |
| } | |
| // Test quaternion_normalize | |
| { | |
| TEST(quaternion_nearly_equal(quaternion_normalize((quaternion){0, 0, 0, 1}), (quaternion){0, 0, 0, 1})); | |
| TEST(quaternion_nearly_equal(quaternion_normalize((quaternion){1, 1, 1, 1}), (quaternion){0.5f, 0.5f, 0.5f, 0.5f})); | |
| TEST(quaternion_nearly_equal(quaternion_normalize((quaternion){0, 3, 0, 4}), (quaternion){0, 0.6f, 0, 0.8f})); | |
| } | |
| // Test quaternion_normalized_inverse | |
| { | |
| TEST(quaternion_nearly_equal(quaternion_normalized_inverse((quaternion){0, 0, 0, 1}), (quaternion){0, 0, 0, 1})); | |
| TEST(quaternion_nearly_equal(quaternion_normalized_inverse((quaternion){0.2f, -0.4f, 0.6f, 0.65f}), (quaternion){-0.2f, 0.4f, -0.6f, 0.65f})); | |
| TEST(quaternion_nearly_equal(quaternion_normalized_inverse((quaternion){0, 1, 0, 0}), (quaternion){0, -1, 0, 0})); | |
| } | |
| // Test vector3_length_squared | |
| { | |
| TEST(number_nearly_equal(vector3_length_squared((vector3){0, 0, 0}), 0)); | |
| TEST(number_nearly_equal(vector3_length_squared((vector3){0, 0, 1}), 1)); | |
| TEST(number_nearly_equal(vector3_length_squared((vector3){1, 2, 1}), 6)); | |
| TEST(number_nearly_equal(vector3_length_squared((vector3){2, 1, 1}), 6)); | |
| } | |
| // Test vector3_length | |
| { | |
| TEST(number_nearly_equal(vector3_length((vector3){0, 0, 0}), 0)); | |
| TEST(number_nearly_equal(vector3_length((vector3){0, 0, 1}), 1)); | |
| TEST(number_nearly_equal(vector3_length((vector3){1, 2, 1}), 2.44948974278f)); | |
| TEST(number_nearly_equal(vector3_length((vector3){2, 1, 1}), 2.44948974278f)); | |
| } | |
| // Test vector3_normalize | |
| { | |
| TEST(vector3_nearly_equal(vector3_normalize((vector3){0, 0, 1}, CONSTANT(0)), (vector3){0, 0, 1})); | |
| TEST(vector3_nearly_equal(vector3_normalize((vector3){1, 1, 1}, CONSTANT(0)), (vector3){0.57735026919f, 0.57735026919f, 0.57735026919f})); | |
| TEST(vector3_nearly_equal(vector3_normalize((vector3){0.1, 0.1, 0.1}, CONSTANT(0.5)), (vector3){0.1, 0.1, 0.1})); | |
| } | |
| // Test vector3_dot | |
| { | |
| TEST(number_nearly_equal(vector3_dot((vector3){+0, +0, +0}, (vector3){+0, +0, +0}), 0)); | |
| TEST(number_nearly_equal(vector3_dot((vector3){+1, +1, +1}, (vector3){+0, +0, +0}), 0)); | |
| TEST(number_nearly_equal(vector3_dot((vector3){+0, +0, +0}, (vector3){+1, +1, +1}), 0)); | |
| TEST(number_nearly_equal(vector3_dot((vector3){+1, +1, +1}, (vector3){+1, +1, +1}), 3)); | |
| TEST(number_nearly_equal(vector3_dot((vector3){+1, +3, -5}, (vector3){+4, -2, -1}), 3)); | |
| } | |
| // Test vector3_cross | |
| { | |
| TEST(vector3_nearly_equal(vector3_cross((vector3){+1, +0, +0}, (vector3){+0, +1, +0}), (vector3){+0, +0, +1})); | |
| TEST(vector3_nearly_equal(vector3_cross((vector3){+1, +0, +0}, (vector3){+0, +0, +1}), (vector3){+0, -1, +0})); | |
| TEST(vector3_nearly_equal(vector3_cross((vector3){-1, -1, -1}, (vector3){+1, +2, +3}), (vector3){-1, +2, -1})); | |
| TEST(vector3_nearly_equal(vector3_cross((vector3){+1, +1, +1}, (vector3){+1, +1, +1}), (vector3){+0, +0, +0})); | |
| TEST(vector3_nearly_equal(vector3_cross((vector3){+2, +3, +4}, (vector3){+5, +6, +7}), (vector3){-3, +6, -3})); | |
| } | |
| // Test vector3_add | |
| { | |
| TEST(vector3_nearly_equal(vector3_add((vector3){+0, +0, +0}, (vector3){+0, +0, +0}), (vector3){+0, +0, +0})); | |
| TEST(vector3_nearly_equal(vector3_add((vector3){+0, +0, +0}, (vector3){+1, +1, +1}), (vector3){+1, +1, +1})); | |
| TEST(vector3_nearly_equal(vector3_add((vector3){+1, +1, +1}, (vector3){+0, +0, +0}), (vector3){+1, +1, +1})); | |
| TEST(vector3_nearly_equal(vector3_add((vector3){+1, +1, +1}, (vector3){+1, +1, +1}), (vector3){+2, +2, +2})); | |
| TEST(vector3_nearly_equal(vector3_add((vector3){+1, +1, +1}, (vector3){-1, -1, -1}), (vector3){+0, +0, +0})); | |
| TEST(vector3_nearly_equal(vector3_add((vector3){+1, +2, +3}, (vector3){+4, +5, +6}), (vector3){+5, +7, +9})); | |
| } | |
| // Test vector3_subtract | |
| { | |
| TEST(vector3_nearly_equal(vector3_subtract((vector3){+0, +0, +0}, (vector3){+0, +0, +0}), (vector3){+0, +0, +0})); | |
| TEST(vector3_nearly_equal(vector3_subtract((vector3){+0, +0, +0}, (vector3){+1, +1, +1}), (vector3){-1, -1, -1})); | |
| TEST(vector3_nearly_equal(vector3_subtract((vector3){+1, +1, +1}, (vector3){+0, +0, +0}), (vector3){+1, +1, +1})); | |
| } | |
| // Test vector3_scalar_multiply | |
| { | |
| TEST(vector3_nearly_equal(vector3_scalar_multiply(0, (vector3){+1, +1, +1}), (vector3){+0, +0, +0})); | |
| TEST(vector3_nearly_equal(vector3_scalar_multiply(4, (vector3){+0, -1, +2}), (vector3){+0, -4, +8})); | |
| } | |
| // Test vector3_abs | |
| { | |
| TEST(vector3_nearly_equal(vector3_abs((vector3){+1, +1, +1}), (vector3){+1, +1, +1})); | |
| TEST(vector3_nearly_equal(vector3_abs((vector3){-1, -1, -1}), (vector3){+1, +1, +1})); | |
| TEST(vector3_nearly_equal(vector3_abs((vector3){+0, +1, -2}), (vector3){+0, +1, +2})); | |
| } | |
| // Test rotate_vector_by_quaternion: Identity rotation | |
| { | |
| vector3 vec = {1, 2, 3}; | |
| quaternion identity = {0, 0, 0, 1}; | |
| vector3 result = rotate_vector_by_quaternion(vec, identity); | |
| TEST(vector3_nearly_equal(result, vec)); | |
| } | |
| // Test rotate_vector_by_quaternion: 180-degree rotation around Y axis | |
| { | |
| quaternion rot_y_180 = {0, 1, 0, 0}; | |
| vector3 result = rotate_vector_by_quaternion((vector3){1, 0, 0}, rot_y_180); | |
| TEST(vector3_nearly_equal(result, (vector3){-1, 0, 0})); | |
| } | |
| // Test rotate_vector_by_quaternion: 90-degree rotation around X axis | |
| { | |
| quaternion rot_x_90 = {sqrtf(0.5f), 0, 0, sqrtf(0.5f)}; | |
| vector3 result = rotate_vector_by_quaternion((vector3){0, 1, 0}, rot_x_90); | |
| TEST(vector3_nearly_equal(result, (vector3){0, 0, 1})); | |
| } | |
| // Test rotate_vector_by_quaternion: 90-degree rotation around Z axis | |
| { | |
| quaternion rot_z_90 = {0, 0, sqrtf(0.5f), sqrtf(0.5f)}; | |
| vector3 result = rotate_vector_by_quaternion((vector3){1, 0, 0}, rot_z_90); | |
| TEST(vector3_nearly_equal(result, (vector3){0, 1, 0})); | |
| } | |
| // Test quaternion_add | |
| { | |
| TEST(quaternion_nearly_equal(quaternion_add((quaternion){0, 0, 0, 0}, (quaternion){1, 1, 1, 1}), (quaternion){1, 1, 1, 1})); | |
| } | |
| // Test quaternion_multiply | |
| { | |
| // Identity quaternion multiplication | |
| const quaternion identity = (quaternion){0, 0, 0, 1}; | |
| const quaternion q = (quaternion){0.5f, -0.5f, 0.5f, -0.5f}; | |
| TEST(quaternion_nearly_equal( | |
| quaternion_multiply(identity, q), q)); | |
| TEST(quaternion_nearly_equal( | |
| quaternion_multiply(q, identity), q)); | |
| // 90-degree rotation around X then Y | |
| const quaternion rot_x_90 = quaternion_normalize((quaternion){sqrtf(0.5f), 0, 0, sqrtf(0.5f)}); | |
| const quaternion rot_y_90 = quaternion_normalize((quaternion){0, sqrtf(0.5f), 0, sqrtf(0.5f)}); | |
| TEST(quaternion_nearly_equal(quaternion_multiply(rot_y_90, rot_x_90), quaternion_normalize((quaternion){0.5f, 0.5f, -0.5f, 0.5f}))); | |
| // Multiply quaternion by its inverse should yield identity | |
| TEST(quaternion_nearly_equal(quaternion_multiply(q, quaternion_normalized_inverse(q)), identity)); | |
| } | |
| // Test controller_states_angular_rate_degrees | |
| { | |
| TEST(number_nearly_equal(controller_states_angular_rate_degrees(CONSTANT(+0.0), 70, 670, 0), 0)); | |
| TEST(number_nearly_equal(controller_states_angular_rate_degrees(CONSTANT(+1.0), 70, 670, 0), +670)); | |
| TEST(number_nearly_equal(controller_states_angular_rate_degrees(CONSTANT(-1.0), 70, 670, 0), -670)); | |
| TEST(number_nearly_equal(controller_states_angular_rate_degrees(CONSTANT(+1.0), 70, 670, 1), +670)); | |
| TEST(number_nearly_equal(controller_states_angular_rate_degrees(CONSTANT(-1.0), 70, 670, 1), -670)); | |
| TEST(number_nearly_equal(controller_states_angular_rate_degrees(CONSTANT(+0.1), 70, 670, 0), +13)); | |
| TEST(number_nearly_equal(controller_states_angular_rate_degrees(CONSTANT(-0.1), 70, 670, 0), -13)); | |
| } | |
| } | |
| int main(void) { | |
| tests(); | |
| const uintptr_t PHYSICS_HZ = 8000; | |
| const float PHSICS_FRAME_TIME = 1.0f / (float)PHYSICS_HZ; | |
| physics_states physics_states; | |
| drone_states drone_states; | |
| physics_states_init(&physics_states); | |
| drone_states_init(&drone_states); | |
| physics_states_set_count(&physics_states, 1); | |
| physics_states_set_position_world(&physics_states, 0, Y, 15.0f); | |
| physics_states_set_angular_velocity_world(&physics_states, 0, X, 3.0f); | |
| // 90 degree rotation in the Z | |
| /* | |
| physics_states_set_orientation_world(&physics_states, 0, X, 0); | |
| physics_states_set_orientation_world(&physics_states, 0, Y, 0); | |
| physics_states_set_orientation_world(&physics_states, 0, Z, 0.707106829); | |
| physics_states_set_orientation_world(&physics_states, 0, W, 0.707106829); | |
| //*/ | |
| // One second of simulation | |
| for (uintptr_t i = 0; i < PHYSICS_HZ; ++i) { | |
| drone_states_update(&drone_states, &physics_states); | |
| physics_states_update(&physics_states, PHSICS_FRAME_TIME); | |
| } | |
| printf("POSITION Y: %f\n", physics_states_get_position_world(&physics_states, 0, Y)); | |
| printf("ANGULAR VELOCITY X: %f\n", physics_states_get_angular_velocity_world(&physics_states, 0, X)); | |
| printf("BAROMETER: %f\n", drone_states_get_barometer_pressure_pa(&drone_states, 0)); | |
| return 0; | |
| } | |
| #endif |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment