#c
#creative-coding
#raylib
Posteado:
5 minutos
Simulación de tejido usando integración de verlet.
Simulación de tejido usando la integración de Verlet. Basado directamente en esta implementación en odin, basado a su vez en otro artículo del mismo autor en C++.
La implementación en C es muy sencilla, el método de integración es realmente intuitivo, en el segundo artículo referenciado en el párrafo anterior está muy bien explicado. Para la creación de los gráficos uso raylib, el proyecto liderado por un desarrollador español más importante que hay junto con Asahi Linux del que yo tenga noticia.
No he compilado a WASM (opción que permite raylib, aún no probado), por lo que lo que se muestra a continuación es un gif animado.
typedef struct point {
Vector2 pos;
Vector2 prev_pos;
Vector2 init_pos;
bool is_pinned;
bool is_selected;
} Point;
typedef struct stick {
Point *p0;
Point *p1;
bool is_teared;
} Stick;
typedef struct cloth {
Point **points;
Stick **sticks;
int n_points;
int n_sticks;
bool is_teared;
} Cloth;
Cloth *make_cloth(int w, int h, int spacing, float start_x, float start_y) {
Cloth *cloth = malloc(sizeof(Cloth));
int n_points = (h + 1) * (w + 1);
int n_sticks = h * w * 2 + h + w;
cloth->n_points = n_points;
cloth->n_sticks = n_sticks;
cloth->points = malloc(n_points * sizeof(Point *));
cloth->sticks = malloc(n_sticks * sizeof(Stick *));
int current_stick_idx = -1;
int current_point_idx = -1;
for (int y = 0; y <= h; y++) {
for (int x = 0; x <= w; x++) {
Point *p = malloc(sizeof(Point));
Vector2 init_pos = {.x = start_x + x * spacing,
.y = start_y + y * spacing};
p->init_pos = init_pos;
p->pos = p->init_pos;
p->prev_pos = p->init_pos;
p->is_pinned = y == 0;
cloth->points[++current_point_idx] = p;
if (x != 0) {
Stick *stick = malloc(sizeof(Stick));
stick->p0 = cloth->points[current_point_idx - 1];
stick->p1 = cloth->points[current_point_idx];
cloth->sticks[++current_stick_idx] = stick;
}
if (y != 0) {
Stick *stick = malloc(sizeof(Stick));
stick->p0 = cloth->points[(y - 1) * (w + 1) + x];
stick->p1 = cloth->points[y * (w + 1) + x];
cloth->sticks[++current_stick_idx] = stick;
}
}
}
cloth->n_sticks = current_stick_idx;
cloth->is_teared = false;
return cloth;
}
void free_cloth(Cloth *cloth) {
for (int i = 0; i < cloth->n_points; i++) {
free(cloth->points[i]);
}
for (int i = 0; i < cloth->n_sticks; i++) {
free(cloth->sticks[i]);
}
free(cloth->points);
free(cloth->sticks);
free(cloth);
}
void update_point(Point *p, float delta_time, int spacing, float drag,
Vector2 *acceleration) {
if (p->is_pinned) {
p->pos = p->init_pos;
return;
}
Vector2 current_pos = p->pos;
Vector2 update = Vector2Add(
Vector2Scale(Vector2Subtract(p->pos, p->prev_pos), (1.0 - drag)),
Vector2Scale(*acceleration, delta_time * delta_time));
p->pos.x += update.x;
p->pos.y += update.y;
p->prev_pos = current_pos;
}
void update_stick(Stick *s, int spacing, float elasticity) {
Vector2 delta = Vector2Subtract(s->p1->pos, s->p0->pos);
float distance = Vector2Length(delta);
if (distance > elasticity) {
s->is_teared = true;
return;
}
Vector2 correction =
Vector2Scale(delta, ((distance - spacing) / distance * 0.5) * 0.5);
if (!s->p0->is_pinned && !s->is_teared) {
s->p0->pos.x += correction.x;
s->p0->pos.y += correction.y;
}
if (!s->p1->is_pinned && !s->is_teared) {
s->p1->pos.x -= correction.x;
s->p1->pos.y -= correction.y;
}
}
void update_cloth(Cloth *cloth, float dt, int spacing, float drag,
Vector2 *acceleration, float elasticity) {
for (int i = 0; i < cloth->n_points; i++) {
update_point(cloth->points[i], dt, spacing, drag, acceleration);
}
for (int i = 0; i < cloth->n_sticks; i++) {
update_stick(cloth->sticks[i], spacing, elasticity);
}
}
Color get_color_from_tension(float distance, float elasticity, int spacing) {
Color color;
if (distance <= spacing) {
Color c = {44, 222, 130, 255};
color = c;
} else if (distance <= spacing * 1.33) {
float t = (distance - spacing) / (spacing * 0.33);
Color c = {Lerp(44, 255, 5), Lerp(222, 255, t), Lerp(130, 0, t), 255};
color = c;
} else {
float t = (distance - spacing * 1.33) / (elasticity - spacing * 1.33);
Color c = {Lerp(255, 222, t), Lerp(255, 44, t), Lerp(0, 44, t), 255};
color = c;
}
return color;
}
void draw_stick(Stick *s, int spacing, float elasticity) {
if (s->is_teared)
return;
float distance = Vector2Distance(s->p0->pos, s->p1->pos);
Color color = s->p1->is_selected || s->p1->is_selected
? BLUE
: get_color_from_tension(distance, elasticity, spacing);
DrawLineV(s->p0->pos, s->p1->pos, color);
}
void draw_cloth(Cloth *c, int spacing, float elasticity) {
for (int i = 0; i < c->n_sticks; i++) {
draw_stick(c->sticks[i], spacing, elasticity);
}
}
void set_cursor_size(float *cursor_size) {
int step_size = 5;
if (GetMouseWheelMove() > 0) {
cursor_size += step_size;
} else if (GetMouseWheelMove() < 0 && !(*cursor_size <= step_size)) {
cursor_size -= step_size;
}
}
void interact_with_points(Cloth *c, float *cursor_size) {
static Vector2 prev_mouse_pos;
Vector2 mouse_pos = GetMousePosition();
Vector2 max_delta = {100};
Vector2 min_delta = {0};
Vector2 mouse_delta = Vector2Clamp(Vector2Subtract(mouse_pos, prev_mouse_pos),
min_delta, max_delta);
for (int i = 0; i < c->n_points; i++) {
Point *p = c->points[i];
float distance = Vector2Distance(mouse_pos, p->pos);
if (distance < *cursor_size) {
if (IsMouseButtonDown(MOUSE_BUTTON_LEFT)) {
p->prev_pos = Vector2Add(p->prev_pos, mouse_delta);
p->pos = Vector2Add(p->pos, mouse_delta);
}
p->is_selected = true;
} else {
p->is_selected = false;
}
}
prev_mouse_pos = mouse_pos;
}
void handle_mouse_interaction(Cloth *c, float *cursor_size) {
set_cursor_size(cursor_size);
interact_with_points(c, cursor_size);
}
int main(void) {
// InitWindow(400, 224, "Template-4.0.0");
InitWindow(1000, 600, "Cloth Simulation 2D");
// ToggleBorderlessWindowed();
int screen_width = GetScreenWidth();
int screen_height = GetScreenHeight();
SetTargetFPS(60);
int cloth_width = 80;
int cloth_height = 40;
int cloth_spacing = 10;
int start_x = (screen_width - (cloth_width * cloth_spacing)) / 2;
int start_y = screen_height / 12;
float drag = 0.005;
float elasticity = 100.0;
Vector2 gravity = {0.0, 980.0};
float fixed_delta_time = 1.0 / 300;
float accumulator = 0.0;
float cursor_size = 30.0;
Cloth *cloth =
make_cloth(cloth_width, cloth_height, cloth_spacing, start_x, start_y);
while (!WindowShouldClose()) {
handle_mouse_interaction(cloth, &cursor_size);
accumulator += GetFrameTime();
// IMPORTANTE
// Con accumulator gestionamos el tiempo de la simulación. Para cada frame
// añadimos el tiempo desde el frame anterior a dicho acumulador. Mientras
// que sea menor que el paso de tiempo, procesamos la ropa sin actualizar el
// dibujo, de forma que la física sí se simula de forma consistente aunque
// no se pinten todos los pasos.
while (accumulator >= fixed_delta_time) {
update_cloth(cloth, fixed_delta_time, cloth_spacing, drag, &gravity,
elasticity);
accumulator -= fixed_delta_time;
}
BeginDrawing();
ClearBackground(RAYWHITE);
draw_cloth(cloth, cloth_spacing, elasticity);
EndDrawing();
}
CloseWindow();
free_cloth(cloth);
return 0;
}