commit dd58a721ec7ec8194298569de551a21a735f420a Author: Synthasmagoria Date: Wed Sep 23 15:54:19 2026 +0200 Initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..dc58ab5 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +Custom projectile system for GameMaker. +Makes it easier to make 3d particles. + +![](images/demo.mp4) diff --git a/build-linux.sh b/build-linux.sh new file mode 100755 index 0000000..f4a9b74 --- /dev/null +++ b/build-linux.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euxo pipefail + +odin build . -build-mode:shared -target:linux_amd64 -o:speed -extra-linker-flags:-fPIC \ +-out:../../project/extensions/exK3PlusProjectileEffectManager/libk3plus_projectile_effect_manager.so +../bin/regenerate_extension . ../../project/extensions/exK3PlusProjectileEffectManager/exK3PlusProjectileEffectManager.yy diff --git a/build-windows.bat b/build-windows.bat new file mode 100644 index 0000000..c2e8ac3 --- /dev/null +++ b/build-windows.bat @@ -0,0 +1,4 @@ +set OUT="..\..\project\extensions\exK3PlusProjectileEffectManager\libk3plus_projectile_effect_manager.dll" +set EXTENSION="..\..\project\extensions\exK3PlusProjectileEffectManager\exK3PlusProjectileEffectManager.yy" +odin build . -build-mode:shared -target:windows_amd64 -o:speed -out:%OUT% +..\bin\regenerate_extension.exe . %EXTENSION% diff --git a/images/demo.mp4 b/images/demo.mp4 new file mode 100644 index 0000000..7225a41 Binary files /dev/null and b/images/demo.mp4 differ diff --git a/main.odin b/main.odin new file mode 100644 index 0000000..5a1447a --- /dev/null +++ b/main.odin @@ -0,0 +1,347 @@ +package main + +import "base:runtime" +import "core:fmt" +import "core:math" + +double :: f64 +v2 :: [2]f32 +v3 :: [3]f32 +v4 :: [4]f32 +Color :: [4]byte + +System :: struct { + particle_number: int, + index: int, + tpage_size: int, + vertices: [^]Vertex, + instances: [^]Instance, +} + +Instance :: struct { + life: int, + image_uv_width: f32, + image_index, image_speed, image_number: f32, + position, speed, gravity: v3, + image_blend, image_blend_add: v4, + scale, grow: v2, + image_angle, image_angle_speed: f32, + hsv, hsv_add: v3, +} + +Vertex :: struct { + position: v3, + color: Color, + texcoord: v2, + offset: v3, + scale: v2, + angle: f32, + rotation_direction: v3, + animation: f32, + hsv: v3, +} + +system_current: ^System + +pem_show_error :: proc(message: string, loc: runtime.Source_Code_Location) { + context = context + fmt.println("Error (", loc.procedure, ") L", loc.line, ": ", message, sep = "") +} + +@(export) +pem_get_size :: proc "c" (number: double) -> double { + number := int(number) + system := size_of(System) + vertices := _projectile_system_get_vertex_buffer_size(number) + instances := size_of(Instance) * number + return double(system + vertices + instances) +} + +@(export) +pem_get_vertex_buffer_size :: proc "c" (number: double) -> double { + return double(_projectile_system_get_vertex_buffer_size(int(number))) +} + +_projectile_system_get_vertex_buffer_size :: proc "c" (number: int) -> int { + return size_of(Vertex) * 6 * number +} + +@(export) +pem_get_vertex_buffer_offset :: proc "c" () -> double { + return double(_projectile_system_get_vertex_buffer_offset()) +} + +_projectile_system_get_vertex_buffer_offset :: proc "c" () -> int { + return size_of(System) +} + +@(export) +pem_init :: proc "c" (memory: [^]byte, particle_number, tpage_size: double) { + context = runtime.default_context() + particle_number := int(particle_number) + system := cast(^System)memory + system.particle_number = particle_number + system.tpage_size = int(tpage_size) + system.index = 0 + vertex_buffer_offset := _projectile_system_get_vertex_buffer_offset() + system.vertices = cast([^]Vertex)&memory[vertex_buffer_offset] + instance_buffer_offset := vertex_buffer_offset + _projectile_system_get_vertex_buffer_size(particle_number) + system.instances = cast([^]Instance)&memory[instance_buffer_offset] +} + +@(export) +_pem_update :: proc "c" (system: ^System) { + vertices := system.vertices + instances := system.instances + for i in 0 ..< system.particle_number { + instances[i].life = max(instances[i].life - 1, -1) + if instances[i].life == 0 { + instances[i].grow = v2{0.0, 0.0} + instances[i].image_blend.a = 0.0 + instances[i].image_blend_add.a = 0.0 + } + + instances[i].speed += instances[i].gravity + instances[i].position += instances[i].speed + instances[i].image_blend += instances[i].image_blend_add + instances[i].scale += instances[i].grow + instances[i].image_angle += instances[i].image_angle_speed + instances[i].image_index = math.wrap(instances[i].image_index + instances[i].image_speed, instances[i].image_number) + instances[i].hsv = v3 { + instances[i].hsv.x + instances[i].hsv_add.x, + clamp(instances[i].hsv.y + instances[i].hsv_add.y, 0.0, 2.0), + clamp(instances[i].hsv.z + instances[i].hsv_add.z, 0.0, 2.0), + } + offset := i * 6 + #unroll for j in 0 ..< 6 { + vertices[offset + j].color = { + u8(clamp(instances[i].image_blend.r, 0, 255)), + u8(clamp(instances[i].image_blend.g, 0, 255)), + u8(clamp(instances[i].image_blend.b, 0, 255)), + u8(clamp(instances[i].image_blend.a, 0, 255)), + } + vertices[offset + j].offset = instances[i].position + vertices[offset + j].scale = instances[i].scale + vertices[offset + j].angle = instances[i].image_angle + vertices[offset + j].animation = math.floor(instances[i].image_index) * instances[i].image_uv_width + vertices[offset + j].hsv = instances[i].hsv + } + } +} + +@(export) +_pem_set :: proc "c" (system: ^System) { + system_current = system +} + +Origin :: enum { + TopLeft, + TopMiddle, + TopRight, + MiddleLeft, + Middle, + MiddleRight, + BottomLeft, + BottomMiddle, + BottomRight, +} + +@(export) +pem_instance_create_origin :: proc "c" (x, y, w, h, origin, uv_left, uv_top, uv_right, uv_bottom, image_number: double) -> double { + xorigin, yorigin: double + origin := Origin(int(origin)) + switch origin { + case .TopLeft: + xorigin = 0.0; yorigin = 0.0 + case .TopMiddle: + xorigin = w / 2.0; yorigin = 0.0 + case .TopRight: + xorigin = w; yorigin = 0.0 + case .MiddleLeft: + xorigin = 0.0; yorigin = h / 2.0 + case .Middle: + xorigin = w / 2.0; yorigin = h / 2.0 + case .MiddleRight: + xorigin = w; yorigin = h / 2.0 + case .BottomLeft: + xorigin = 0.0; yorigin = h + case .BottomMiddle: + xorigin = w; yorigin = h + case .BottomRight: + xorigin = w / 2.0; yorigin = h + } + return pem_instance_create(x, y, w, h, xorigin, yorigin, uv_left, uv_top, uv_right, uv_bottom, image_number) +} + +@(export) +pem_instance_create :: proc "c" (x, y, w, h, xorigin, yorigin, uv_left, uv_top, uv_right, uv_bottom, image_number: double) -> double { + position := v3{f32(x), f32(y), 1.0} + size := v3{f32(w), f32(h), 1.0} + origin := v3{f32(xorigin), f32(yorigin), 0.0} + uv_left := f32(uv_left) + uv_right := f32(uv_right) + uv_top := f32(uv_top) + uv_bottom := f32(uv_bottom) + image_number := f32(max(0.0, image_number)) + + uv_tl := v2{uv_left, uv_top} + uv_br := v2{uv_left + (uv_right - uv_left) / image_number, uv_bottom} + + index := system_current.index + offset := system_current.index * 6 + system_current.index = (system_current.index + 1) % system_current.particle_number + system_current.instances[index] = { + life = -1, + image_uv_width = (uv_right - uv_left) / image_number, + image_index = 0.0, + image_number = image_number, + image_speed = 1.0, + position = position, + image_blend = {255.0, 255.0, 255.0, 255.0}, + scale = {1.0, 1.0}, + image_angle = 0.0, + hsv = {0.0, 1.0, 1.0}, + } + + tl := v3{0.0, 0.0, 1.0} + tr := v3{1.0, 0.0, 1.0} + bl := v3{0.0, 1.0, 1.0} + br := v3{1.0, 1.0, 1.0} + pos := [6]v3{tl, bl, tr, tr, bl, br} + #unroll for i in 0 ..< 6 { + system_current.vertices[offset + i] = { + position = pos[i] * size - origin, + color = {255, 255, 255, 255}, + texcoord = math.lerp(uv_tl, uv_br, pos[i].xy), + offset = system_current.instances[index].position, + scale = system_current.instances[index].scale, + angle = system_current.instances[index].image_angle, + rotation_direction = v3{0.0, 0.0, -1.0}, + hsv = system_current.instances[index].hsv, + } + } + return double(index) +} + +@(export) +pem_instance_set_life :: proc "c" (id, life: double) { + system_current.instances[int(id)].life = int(life) +} + +@(export) +pem_instance_set_life_to_animation :: proc "c" (id, animation_count: double) { + id := int(id) + image_number_from_index := system_current.instances[id].image_number - system_current.instances[id].image_index + if image_number_from_index <= 0.0 { + context = runtime.default_context() + pem_show_error("image number left is 0", #location()) + system_current.instances[id].life = -1 + return + } + system_current.instances[id].life = int(image_number_from_index / system_current.instances[id].image_speed) * int(animation_count) +} + +@(export) +pem_instance_set_life_to_alpha_fade :: proc "c" (id: double) { + id := int(id) + a := system_current.instances[id].image_blend.a + a_add := system_current.instances[id].image_blend_add.a + if a <= 0.0 { + context = runtime.default_context() + pem_show_error("alpha started at 0.0 or lower", #location()) + system_current.instances[id].life = -1 + return + } + frames := int((a / a_add) * -1.0) + if frames <= 0 { + context = runtime.default_context() + pem_show_error("alpha never reaches 0.0", #location()) + system_current.instances[id].life = -1 + return + } + system_current.instances[id].life = frames +} + +@(export) +pem_instance_set_life_to_shrink :: proc "c" (id: double) { + id := int(id) + scale := v2{system_current.instances[id].scale.x, system_current.instances[id].scale.y} + grow := v2{system_current.instances[id].grow.x, system_current.instances[id].grow.y} + frames := v2{scale.x / grow.x * -1.0, scale.y / grow.y * -1.0} + frames_max := max(frames.x, frames.y) + if frames_max <= 0.0 { + context = runtime.default_context() + pem_show_error("instance never shrinks in on itself", #location()) + system_current.instances[id].life = -1 + return + } + system_current.instances[id].life = int(frames_max) +} + +@(export) +pem_instance_set_image_blend :: proc "c" (id, r, g, b, a: double) { + system_current.instances[int(id)].image_blend = {f32(r), f32(g), f32(b), f32(a)} * 255.0 +} + +@(export) +pem_instance_set_image_blend_add :: proc "c" (id, r, g, b, a: double) { + system_current.instances[int(id)].image_blend_add = {f32(r), f32(g), f32(b), f32(a)} * 255.0 +} + +@(export) +pem_instance_set_speed :: proc "c" (id, hspeed, vspeed: double) { + system_current.instances[int(id)].speed = {f32(hspeed), f32(vspeed), 0.0} +} + +@(export) +pem_instance_set_gravity :: proc "c" (id, x, y: double) { + system_current.instances[int(id)].gravity = {f32(x), f32(y), 0.0} +} + +@(export) +pem_instance_set_scale :: proc "c" (id, xscale, yscale: double) { + system_current.instances[int(id)].scale = {f32(xscale), f32(yscale)} +} + +@(export) +pem_instance_set_grow :: proc "c" (id, xgrow, ygrow: double) { + system_current.instances[int(id)].grow = {f32(xgrow), f32(ygrow)} +} + +@(export) +pem_instance_set_image_angle :: proc "c" (id, angle: double) { + system_current.instances[int(id)].image_angle = f32(angle) * math.RAD_PER_DEG +} + +@(export) +pem_instance_set_image_angle_speed :: proc "c" (id, angular_velocity: double) { + system_current.instances[int(id)].image_angle_speed = f32(angular_velocity) * math.RAD_PER_DEG +} + +@(export) +pem_instance_set_image_angle_direction :: proc "c" (id, x, y, z: double) { + offset := int(id) * 6 + #unroll for i in 0 ..< 6 { + system_current.vertices[offset + i].rotation_direction = {f32(x), f32(y), f32(z)} + } +} + +@(export) +pem_instance_set_image_index :: proc "c" (id, index: double) { + system_current.instances[int(id)].image_index = f32(index) +} + +@(export) +pem_instance_set_image_speed :: proc "c" (id, speed: double) { + system_current.instances[int(id)].image_speed = f32(speed) +} + +@(export) +pem_instance_set_hsv :: proc "c" (id, h, s, v: double) { + system_current.instances[int(id)].hsv = v3{f32(h) / 360.0, f32(s) / 100.0, f32(v) / 100.0} +} + +@(export) +pem_instance_set_hsv_add :: proc "c" (id, h, s, v: double) { + system_current.instances[int(id)].hsv_add = v3{f32(h) / 360.0, f32(s) / 100.0, f32(v) / 100.0} +} diff --git a/scrProjectileEffectManager.gml b/scrProjectileEffectManager.gml new file mode 100644 index 0000000..7ed214c --- /dev/null +++ b/scrProjectileEffectManager.gml @@ -0,0 +1,184 @@ +///@param {Id.Buffer} memory +///@param {Pointer} system +///@param {Id.vertex_format} vertex_format +///@param {Id.vertex_buffer} vertex_buffer +///@param {Real} projectile_count +///@param {Struct.Texturepage} texturepage +///@param {Id.DsList} effects +function _ProjectileEffectSystem(memory, system, vertex_format, vertex_buffer, projectile_count, texturepage, effects) constructor { + self.memory = memory; + self.system = system; + self.vertex_format = vertex_format; + self.vertex_buffer = vertex_buffer; + self.projectile_count = projectile_count; + self.texturepage = texturepage; + self.effects = effects; +} + +///@param {Function} create_func +///@param {Real} interval +///@param {Id.DsList} list +function _ProjectileEffectSystemEffect(create_func, interval, list) constructor { + self.interval = interval; + self.time = 0; + self.instances = list; + self.create_func = create_func; +} + +///@param {Real} projectile_count +///@return {_ProjectileEffectSystem} +function pem_create(projectile_count, texturepage) { + vertex_format_begin(); + vertex_format_add_position_3d(); + vertex_format_add_color(); + vertex_format_add_texcoord(); + vertex_format_add_custom(vertex_type_float3, vertex_usage_color); // Position + vertex_format_add_custom(vertex_type_float2, vertex_usage_color); // Scale + vertex_format_add_custom(vertex_type_float1, vertex_usage_color); // Angle + vertex_format_add_custom(vertex_type_float3, vertex_usage_color); // Rotation direction + vertex_format_add_custom(vertex_type_float1, vertex_usage_color); // Animation + vertex_format_add_custom(vertex_type_float3, vertex_usage_color); // Hue saturation value + var _vertex_format = vertex_format_end(); + + var _buffer = buffer_create(pem_get_size(projectile_count), buffer_fixed, 1); + buffer_fill(_buffer, 0, buffer_u32, 0, buffer_get_size(_buffer)); + var _ptr = buffer_get_address(_buffer); + pem_init(_ptr, projectile_count, TEXTUREPAGE_SIZE); + var _vertex_buffer = vertex_create_buffer_from_buffer_ext( + _buffer, + _vertex_format, + pem_get_vertex_buffer_offset(), + projectile_count * 6); + return new _ProjectileEffectSystem(_buffer, _ptr, _vertex_format, _vertex_buffer, projectile_count, texturepage, ds_list_create()); +} + +///@param {Struct._ProjectileEffectSystem} system +///@param {Function} create_func +///@param {Real} interval +function pem_effect_add(system, create_func, interval) { + if (interval <= 0.0) { + printerr("Interval cannot be equal or lower than 0.0", ERROR_LOC); + return; + } + var _effect = new _ProjectileEffectSystemEffect(create_func, interval, ds_list_create()); + ds_list_add(system.effects, _effect); +} + +///@param {Struct._ProjectileEffectSystem} system +///@param {Real} index +///@param {Id.Instance} inst +function pem_effect_instance_add(system, index, inst) { + ds_list_add(system.effects[|index].instances, inst); +} + +///@param {Struct._ProjectileEffectSystemEffect} effect +function _pem_effect_destroy(effect) { + ds_list_destroy(effect.instances); +} + +///@param {Struct._ProjectileEffectSystem} system +function pem_update_vertex_buffer(system) { + vertex_update_buffer_from_buffer( + system.vertex_buffer, + 0, + system.memory, + pem_get_vertex_buffer_offset(), + pem_get_vertex_buffer_size(system.projectile_count)); +} + +///@param {Struct._ProjectileEffectSystem} system +function pem_destroy(system) { + buffer_delete(system.memory); + vertex_format_delete(system.vertex_format); + vertex_delete_buffer(system.vertex_buffer); + for (var i = 0, n = ds_list_size(system.effects); i < n; i++) { + _pem_effect_destroy(system.effects[|i]); + } + ds_list_destroy(system.effects); +} + +///@param {Struct._ProjectileEffectSystem} system +function pem_update(system) { + for (var i = 0, n = ds_list_size(system.effects); i < n; i++) { + var _effect = system.effects[|i]; + _effect.time += 1.0; + while (_effect.time >= _effect.interval) { + _effect.time -= _effect.interval; + for (var j = 0, nn = ds_list_size(_effect.instances); j < nn; j++) { + if (!instance_exists(_effect.instances[|j])) { + ds_list_delete(_effect.instances, j); + j--; + nn--; + continue; + } + _effect.create_func(system, _effect.instances[|j]); + } + } + } + _pem_update(system.system); + pem_update_vertex_buffer(system); +} + +///@param {Struct._ProjectileEffectSystem} system +function pem_set(system) { + _pem_set(system.system); +} + +///@param {Struct._ProjectileEffectSystem} system +function pem_draw(system) { + shader_set(shdProjectileEffect); + vertex_submit(system.vertex_buffer, pr_trianglelist, system.texturepage.texture); + shader_reset(); +} + +///@param {Struct._ProjectileEffectSystem} system +///@param {Real} x +///@param {Real} y +///@param {Asset.GMSprite} sprite_index +function pem_instance_create_from_sprite(system, x, y, sprite_index) { + var _info = texturepage_sprite_get_info(system.texturepage, sprite_index); + if (_info == -1) { + printerr($"Couldn't create effect instance with sprite {sprite_get_name(sprite_index)}. It wasn't on the texture page", ERROR_LOC); + return; + } + return pem_instance_create_from_info(x, y, _info); +} + +///@param {Struct._ProjectileEffectSystem} system +///@param {Real} x +///@param {Real} y +///@param {Real} sprite_origin +///@param {Asset.GMSprite} sprite_index +function pem_instance_create_from_sprite_ext(system, x, y, origin, sprite_index) { + var _info = texturepage_sprite_get_info(system.texturepage, sprite_index); + if (_info == -1) { + printerr("Couldn't create effect instance with sprite {sprite_get_name(sprite_index)}. It wasn't on the texture page", ERROR_LOC); + return; + } + return pem_instance_create_from_info_ext(x, y, origin, _info); +} + +///@param {Real} x +///@param {Real} y +///@param {Struct.TexturepageEntryInfo} info +function pem_instance_create_from_info(x, y, info) { + return pem_instance_create( + x, y, + sprite_get_width(info.sprite_index), sprite_get_height(info.sprite_index), + sprite_get_xoffset(info.sprite_index), sprite_get_yoffset(info.sprite_index), + info.uvs[UvData.Left], info.uvs[UvData.Top], info.uvs[UvData.Right], info.uvs[UvData.Bottom], + sprite_get_number(info.sprite_index)); +} + +///@param {Real} x +///@param {Real} y +///@param {Real} sprite_origin +///@param {Struct.TexturepageEntryInfo} info +function pem_instance_create_from_info_ext(x, y, sprite_origin, info) { + return pem_instance_create_origin( + x, y, + sprite_get_width(info.sprite_index), sprite_get_height(info.sprite_index), + sprite_origin, + info.uvs[UvData.Left], info.uvs[UvData.Top], info.uvs[UvData.Right], info.uvs[UvData.Bottom], + sprite_get_number(info.sprite_index)); +} diff --git a/shdProjectileEffect.fsh b/shdProjectileEffect.fsh new file mode 100644 index 0000000..82bc971 --- /dev/null +++ b/shdProjectileEffect.fsh @@ -0,0 +1,31 @@ +varying vec2 v_vTexcoord; +varying vec4 v_vColour; +varying vec3 v_vHsv; + +vec3 rgb2hsv(vec3 c) { + vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0); + vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g)); + vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r)); + float d = q.x - min(q.w, q.y); + float e = 1.0e-10; + return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x); +} + +vec3 hsv2rgb(vec3 c) { + vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); + return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); +} + +void main() { + vec4 col = v_vColour * texture2D(gm_BaseTexture, v_vTexcoord); + vec3 hsv = rgb2hsv(col.rgb); + hsv = vec3( + fract(hsv.r + v_vHsv.r), + clamp(hsv.g + v_vHsv.g, 0.0, 1.0), + clamp(hsv.b + v_vHsv.b, 0.0, 1.0) + ); + + col.rgb = hsv2rgb(hsv); + gl_FragColor = col; +} diff --git a/shdProjectileEffect.vsh b/shdProjectileEffect.vsh new file mode 100644 index 0000000..6e18713 --- /dev/null +++ b/shdProjectileEffect.vsh @@ -0,0 +1,41 @@ +attribute vec3 in_Position; +attribute vec4 in_Colour; +attribute vec2 in_TextureCoord; +attribute vec3 in_Colour0; +attribute vec2 in_Colour1; +attribute float in_Colour2; +attribute vec3 in_Colour3; +attribute float in_Colour4; +attribute vec3 in_Colour5; + +#define in_Offset in_Colour0 +#define in_Scale in_Colour1 +#define in_Angle in_Colour2 +#define in_RotationDirection in_Colour3 +#define in_Animation in_Colour4 +#define in_Hsv in_Colour5 + +varying vec2 v_vTexcoord; +varying vec4 v_vColour; +varying vec3 v_vHsv; + +mat4 rotation_matrix_3d(vec3 axis, float angle) { + float s = sin(angle); + float c = cos(angle); + float oc = 1.0 - c; + + return mat4( + oc * axis.x * axis.x + c, oc * axis.x * axis.y - axis.z * s, oc * axis.z * axis.x + axis.y * s, 0.0, + oc * axis.x * axis.y + axis.z * s, oc * axis.y * axis.y + c, oc * axis.y * axis.z - axis.x * s, 0.0, + oc * axis.z * axis.x - axis.y * s, oc * axis.y * axis.z + axis.x * s, oc * axis.z * axis.z + c, 0.0, + 0.0, 0.0, 0.0, 1.0); +} + +void main() { + mat4 rotmat = rotation_matrix_3d(in_RotationDirection, in_Angle); + vec4 pos = vec4(in_Position.xy * in_Scale, in_Position.z, 1.0) * rotmat + vec4(in_Offset, 0.0); + gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * pos; + v_vColour = in_Colour; + v_vTexcoord = in_TextureCoord + vec2(in_Animation, 0.0); + v_vHsv = in_Hsv; +}