/* Snowstorm - Particle effects in C * * Copyright (c) 2021 Evan Hemsley * * This software is provided 'as-is', without any express or implied warranty. * In no event will the authors be held liable for any damages arising from * the use of this software. * * Permission is granted to anyone to use this software for any purpose, * including commercial applications, and to alter it and redistribute it * freely, subject to the following restrictions: * * 1. The origin of this software must not be misrepresented; you must not * claim that you wrote the original software. If you use this software in a * product, an acknowledgment in the product documentation would be * appreciated but is not required. * * 2. Altered source versions must be plainly marked as such, and must not be * misrepresented as being the original software. * * 3. This notice may not be removed or altered from any source distribution. * * Evan "cosmonaut" Hemsley * */ #include "snowstorm.h" #include #include #include #define PI 3.14159265358979323846 typedef struct Snowstorm_Color { uint8_t r; uint8_t g; uint8_t b; uint8_t a; } Snowstorm_Color; typedef struct Snowstorm_UV { float left; float top; float right; float bottom; } Snowstorm_UV; typedef struct Snowstorm_Vector2 { float x; float y; } Snowstorm_Vector2; static inline Snowstorm_Vector2 Vector2_Rotate(Snowstorm_Vector2 vector, float angle) { Snowstorm_Vector2 rotated; rotated.x = vector.x * cosf(angle) - vector.y * sinf(angle); rotated.y = vector.x * sinf(angle) + vector.y * cosf(angle); return rotated; } static inline float Vector2_Magnitude(Snowstorm_Vector2 vector) { return sqrtf(vector.x * vector.x + vector.y * vector.y); } static inline Snowstorm_Vector2 Vector2_Normalize(Snowstorm_Vector2 vector) { float length = Vector2_Magnitude(vector); Snowstorm_Vector2 normalized; normalized.x = vector.x / length; normalized.y = vector.y / length; return normalized; } typedef struct Snowstorm_Matrix3x2 { float M11; float M12; float M21; float M22; float M31; float M32; } Snowstorm_Matrix3x2; static inline Snowstorm_Matrix3x2 Matrix3x2_CreateTranslation(float x, float y) { Snowstorm_Matrix3x2 result; result.M11 = 1; result.M12 = 0; result.M21 = 0; result.M22 = 1; result.M31 = x; result.M32 = y; return result; } static inline Snowstorm_Matrix3x2 Matrix3x2_CreateScale(float x, float y) { Snowstorm_Matrix3x2 result; result.M11 = x; result.M12 = 0; result.M21 = 0; result.M22 = y; result.M31 = 0; result.M32 = 0; return result; } static Snowstorm_Matrix3x2 Matrix3x2_CreateRotation(float radians) { Snowstorm_Matrix3x2 result; float c, s; float epsilon = 0.001f * PI / 180; /* 0.1 % of a degree */ radians = fmodf(radians, PI * 2); if (radians > -epsilon && radians < epsilon) { /* Exact case for zero rotation. */ c = 1; s = 0; } else if (radians > PI / 2 - epsilon && radians < PI / 2 + epsilon) { /* Exact case for 90 degree rotation. */ c = 0; s = 1; } else if (radians < -PI + epsilon && radians > PI - epsilon) { /* Exact case for 180 degree rotation. */ c = -1; s = 0; } else if (radians > -PI / 2 - epsilon && radians < -PI / 2 + epsilon) { /* Exact case for 270 degree rotation. */ c = 0; s = -1; } else { /* Arbitrary rotation. */ c = cosf(radians); s = sinf(radians); } /* * [ c s ] * [ -s c ] * [ 0 0 ] */ result.M11 = c; result.M12 = s; result.M21 = -s; result.M22 = c; result.M31 = 0; result.M32 = 0; return result; } static inline Snowstorm_Matrix3x2 Matrix3x2_Multiply(Snowstorm_Matrix3x2 m1, Snowstorm_Matrix3x2 m2) { Snowstorm_Matrix3x2 result; /* First row */ result.M11 = m1.M11 * m2.M11 + m1.M12 * m2.M21; result.M12 = m1.M11 * m2.M12 + m1.M12 * m2.M22; /* Second row */ result.M21 = m1.M21 * m2.M11 + m1.M22 * m2.M21; result.M22 = m1.M21 * m2.M12 + m1.M22 * m2.M22; /* Third row */ result.M31 = m1.M31 * m2.M11 + m1.M32 * m2.M21 + m2.M31; result.M32 = m1.M31 * m2.M12 + m1.M32 * m2.M22 + m2.M32; return result; } static inline Snowstorm_Vector2 Vector2_Transform(Snowstorm_Vector2 vector, Snowstorm_Matrix3x2 matrix) { Snowstorm_Vector2 result; result.x = (vector.x * matrix.M11) + (vector.y * matrix.M21) + matrix.M31; result.y = (vector.x * matrix.M12) + (vector.y * matrix.M22) + matrix.M32; return result; } typedef struct Snowstorm_Particle { float time; float timeRate; Snowstorm_Vector2 position; Snowstorm_Vector2 velocity; Snowstorm_Vector2 scale; float size; float rotation; float spin; float directionVariance; float speedVariance; Snowstorm_Vector2 catchup; uint32_t uvIndex; } Snowstorm_Particle; typedef struct Snowstorm_Context { Snowstorm_Particle* particles; uint32_t particleCount; uint32_t particleCapacity; Snowstorm_Vector2 wind; float particleSize; float leftBound; float topBound; float rightBound; float bottomBound; Snowstorm_UV* uvs; uint32_t uvCount; uint8_t* currentBufferAddress; /* GM doesnt let you pass more than 4 arguments with different types lol */ } Snowstorm_Context; static inline float Approach(float value, float target, float maxChange) { return value + min(max(target - value, -maxChange), maxChange); } static inline float WrapWithOvershoot(float value, float min, float max) { if (value < min) { value = max - (min - value); } if (value > max) { value = min + (value - max); } return value; } static inline float Random(float a) { return (float)rand() / (float)(RAND_MAX / a); } static inline float RandomRange(float min, float max) { return Random(max - min) + min; } /* i wanted to use pointers but game maker mangles string to pointer casts. oh well */ static Snowstorm_Context *contexts = NULL; static uint32_t contextCount = 0; void Snowstorm_Init() { srand((unsigned int)time(NULL)); } /* FIXME: we should reuse IDs when contexts are destroyed */ double Snowstorm_Create(double particleSize, double leftBound, double topBound, double rightBound, double bottomBound) { contexts = realloc(contexts, sizeof(Snowstorm_Context) * (contextCount + 1)); contextCount += 1; Snowstorm_Context* context = &contexts[contextCount - 1]; context->particleCapacity = 128; context->particles = malloc(sizeof(Snowstorm_Particle) * context->particleCapacity); context->particleCount = 0; context->particleSize = particleSize; context->leftBound = -10; context->rightBound = 490; context->topBound = -10; context->bottomBound = 280; context->wind.x = 0; context->wind.y = 0; context->uvs = NULL; context->uvCount = 0; context->currentBufferAddress = NULL; return (double)(contextCount - 1); } void Snowstorm_SetUVCount(double contextId, double count) { Snowstorm_Context* context = &contexts[(uint32_t)contextId]; context->uvCount = (uint32_t)count; context->uvs = realloc(context->uvs, sizeof(Snowstorm_UV) * context->uvCount); } void Snowstorm_SetLeftTopUV(double contextId, double index, double left, double top) { Snowstorm_Context* context = &contexts[(uint32_t)contextId]; context->uvs[(uint32_t)index].left = left; context->uvs[(uint32_t)index].top = top; } void Snowstorm_SetRightBottomUV(double contextId, double index, double right, double bottom) { Snowstorm_Context* context = &contexts[(uint32_t)contextId]; context->uvs[(uint32_t)index].right = right; context->uvs[(uint32_t)index].bottom = bottom; } void Snowstorm_Update(double contextId) { uint32_t i; Snowstorm_Context* context = &contexts[(uint32_t)contextId]; for (i = 0; i < context->particleCount; i += 1) { Snowstorm_Particle *particle = &context->particles[i]; particle->time += particle->timeRate; float speed = Vector2_Magnitude(particle->velocity); float rotation = sinf(particle->time / (speed * 100)) * particle->directionVariance * (PI / 180.0f); // degrees to radians float windSpeed = Vector2_Magnitude(context->wind); Snowstorm_Vector2 wind = Vector2_Normalize(context->wind); wind.x *= (windSpeed + particle->speedVariance); wind.y *= (windSpeed + particle->speedVariance); wind = Vector2_Rotate(wind, rotation); particle->velocity.x = Approach(particle->velocity.x, wind.x, particle->catchup.x); particle->velocity.y = Approach(particle->velocity.y, wind.y, particle->catchup.y); particle->position.x = WrapWithOvershoot(particle->position.x + particle->velocity.x, context->leftBound, context->rightBound); particle->position.y = WrapWithOvershoot(particle->position.y + particle->velocity.y, context->topBound, context->bottomBound); particle->scale.y = particle->size * sinf(particle->time / 10); particle->rotation += particle->spin; } } void Snowstorm_ApplyChaos(double contextId, double directionChaos, double speedChaos) { uint32_t i; Snowstorm_Context* context = &contexts[(uint32_t)contextId]; for (i = 0; i < context->particleCount; i += 1) { Snowstorm_Particle* particle = &context->particles[i]; particle->directionVariance = RandomRange(-directionChaos, directionChaos); particle->speedVariance = RandomRange(-speedChaos, speedChaos); } } void Snowstorm_ApplyWind(double contextId, double xSpeed, double ySpeed) { Snowstorm_Context* context = &contexts[(uint32_t)contextId]; context->wind.x = (float)xSpeed; context->wind.y = (float)ySpeed; } void Snowstorm_SetParticles(double contextId, double count) { uint32_t i; Snowstorm_Context* context = &contexts[(uint32_t)contextId]; if (count > context->particleCapacity) { context->particleCapacity *= 2; context->particles = realloc(context->particles, sizeof(Snowstorm_Particle) * context->particleCapacity); } for (i = 0; i < count; i += 1) { Snowstorm_Particle* particle = &context->particles[i]; particle->time = 0; particle->timeRate = 0.7f + RandomRange(-0.3f, 0.3f); particle->position.x = RandomRange(context->leftBound, context->rightBound); particle->position.y = RandomRange(context->topBound, context->bottomBound); particle->directionVariance = 0; particle->speedVariance = 0; particle->catchup.x = RandomRange(0.2f, 1); particle->catchup.y = RandomRange(0.2f, 1); particle->velocity.x = context->wind.x; particle->velocity.y = context->wind.y; particle->size = 0.4f; particle->scale.x = particle->size; particle->scale.y = particle->size; particle->spin = RandomRange(-0.1745329f, 0.1745329f); /* 10 degrees */ particle->rotation = 0; particle->uvIndex = rand() % context->uvCount; } context->particleCount = count; } void Snowstorm_SetBufferAddress(double contextId, const char* bufferAddress) { Snowstorm_Context* context = &contexts[(uint32_t)contextId]; context->currentBufferAddress = (uint8_t*)bufferAddress; } double Snowstorm_RequiredSnowBufferSize(double contextId) { Snowstorm_Context* context = &contexts[(uint32_t)contextId]; return (double)context->particleCount * 6 * (sizeof(Snowstorm_Vector2) + sizeof(Snowstorm_Color) + (2 * sizeof(float))); } double Snowstorm_FillSnowBuffer(double contextId, double red, double green, double blue) { uint32_t i, vertexCount; Snowstorm_Context* context = &contexts[(uint32_t)contextId]; uint8_t* bufferAddress = context->currentBufferAddress; Snowstorm_Color color; color.r = (uint8_t)red; color.g = (uint8_t)green; color.b = (uint8_t)blue; color.a = 255; vertexCount = 0; for (i = 0; i < context->particleCount; i += 1) { Snowstorm_Particle* particle = &context->particles[i]; Snowstorm_Vector2 leftTop; Snowstorm_Vector2 rightTop; Snowstorm_Vector2 leftBottom; Snowstorm_Vector2 rightBottom; leftTop.x = context->particleSize * -particle->scale.x / 4; leftTop.y = context->particleSize * -particle->scale.y / 4; rightTop.x = context->particleSize * particle->scale.x / 4; rightTop.y = context->particleSize * -particle->scale.y / 4; leftBottom.x = context->particleSize * -particle->scale.x / 4; leftBottom.y = context->particleSize * particle->scale.y / 4; rightBottom.x = context->particleSize * particle->scale.x / 4; rightBottom.y = context->particleSize * particle->scale.y / 4; Snowstorm_Matrix3x2 translation = Matrix3x2_CreateTranslation(particle->position.x, particle->position.y); Snowstorm_Matrix3x2 rotation = Matrix3x2_CreateRotation(particle->rotation); Snowstorm_Matrix3x2 transform = Matrix3x2_Multiply(rotation, translation); leftTop = Vector2_Transform(leftTop, transform); rightTop = Vector2_Transform(rightTop, transform); leftBottom = Vector2_Transform(leftBottom, transform); rightBottom = Vector2_Transform(rightBottom, transform); float leftUV = context->uvs[particle->uvIndex].left; float topUV = context->uvs[particle->uvIndex].top; float rightUV = context->uvs[particle->uvIndex].right; float bottomUV = context->uvs[particle->uvIndex].bottom; memcpy(bufferAddress, &leftTop, sizeof(Snowstorm_Vector2)); bufferAddress += sizeof(Snowstorm_Vector2); memcpy(bufferAddress, &color, sizeof(Snowstorm_Color)); bufferAddress += sizeof(Snowstorm_Color); memcpy(bufferAddress, &leftUV, sizeof(float)); bufferAddress += sizeof(float); memcpy(bufferAddress, &topUV, sizeof(float)); bufferAddress += sizeof(float); memcpy(bufferAddress, &rightTop, sizeof(Snowstorm_Vector2)); bufferAddress += sizeof(Snowstorm_Vector2); memcpy(bufferAddress, &color, sizeof(Snowstorm_Color)); bufferAddress += sizeof(Snowstorm_Color); memcpy(bufferAddress, &rightUV, sizeof(float)); bufferAddress += sizeof(float); memcpy(bufferAddress, &topUV, sizeof(float)); bufferAddress += sizeof(float); memcpy(bufferAddress, &leftBottom, sizeof(Snowstorm_Vector2)); bufferAddress += sizeof(Snowstorm_Vector2); memcpy(bufferAddress, &color, sizeof(Snowstorm_Color)); bufferAddress += sizeof(Snowstorm_Color); memcpy(bufferAddress, &leftUV, sizeof(float)); bufferAddress += sizeof(float); memcpy(bufferAddress, &bottomUV, sizeof(float)); bufferAddress += sizeof(float); memcpy(bufferAddress, &leftBottom, sizeof(Snowstorm_Vector2)); bufferAddress += sizeof(Snowstorm_Vector2); memcpy(bufferAddress, &color, sizeof(Snowstorm_Color)); bufferAddress += sizeof(Snowstorm_Color); memcpy(bufferAddress, &leftUV, sizeof(float)); bufferAddress += sizeof(float); memcpy(bufferAddress, &bottomUV, sizeof(float)); bufferAddress += sizeof(float); memcpy(bufferAddress, &rightTop, sizeof(Snowstorm_Vector2)); bufferAddress += sizeof(Snowstorm_Vector2); memcpy(bufferAddress, &color, sizeof(Snowstorm_Color)); bufferAddress += sizeof(Snowstorm_Color); memcpy(bufferAddress, &rightUV, sizeof(float)); bufferAddress += sizeof(float); memcpy(bufferAddress, &topUV, sizeof(float)); bufferAddress += sizeof(float); memcpy(bufferAddress, &rightBottom, sizeof(Snowstorm_Vector2)); bufferAddress += sizeof(Snowstorm_Vector2); memcpy(bufferAddress, &color, sizeof(Snowstorm_Color)); bufferAddress += sizeof(Snowstorm_Color); memcpy(bufferAddress, &rightUV, sizeof(float)); bufferAddress += sizeof(float); memcpy(bufferAddress, &bottomUV, sizeof(float)); bufferAddress += sizeof(float); vertexCount += 6; } return (double)vertexCount; } void Snowstorm_Destroy(double contextId) { Snowstorm_Context* context = &contexts[(uint32_t)contextId]; if (context->particles != NULL) { free(context->particles); } if (context->uvs != NULL) { free(context->uvs); } }