From 87bff164f19d94d4ddc431d8ffa436428cceebae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl?= Date: Mon, 19 Oct 2026 20:59:15 +0200 Subject: [PATCH] init --- .gitignore | 20 + .gitmodules | 12 + CMakeLists.txt | 205 + README.md | 20 + shaders/apply.comp | 27 + shaders/areas.glsl | 26 + shaders/biome_mods.glsl | 18 + shaders/burn.comp | 192 + shaders/clip_half.comp | 23 + shaders/common.glsl | 84 + shaders/displace.comp | 90 + shaders/edits.comp | 191 + shaders/entities.comp | 722 + shaders/ents_prev.comp | 16 + shaders/explode_rays.comp | 124 + shaders/far.glsl | 18 + shaders/fog.comp | 112 + shaders/fx_anims.comp | 69 + shaders/fx_common.glsl | 208 + shaders/fx_hand.comp | 102 + shaders/fx_host.glsl | 7 + shaders/fx_particles.comp | 263 + shaders/fx_state.glsl | 133 + shaders/glow.comp | 99 + shaders/intent.comp | 266 + shaders/light_cells.comp | 23 + shaders/lua_native_runtime.glsl | 121 + shaders/lua_native_terrain.glsl | 15 + shaders/paint.comp | 21 + shaders/particle_req.comp | 38 + shaders/procgrad.glsl | 119 + shaders/proj_lua.comp | 25 + shaders/proj_lua_dispatch.glsl | 50 + shaders/proj_spawn.comp | 91 + shaders/proj_step.comp | 722 + shaders/projectiles.comp | 75 + shaders/react.comp | 127 + shaders/render.comp | 457 + shaders/rigid_burn.comp | 123 + shaders/rigid_commit.comp | 11 + shaders/rigid_common.glsl | 71 + shaders/rigid_events.comp | 53 + shaders/rigid_frame.comp | 67 + shaders/rigid_raster.comp | 28 + shaders/rigid_solve.comp | 310 + shaders/simplex3.glsl | 23 + shaders/sky_cols.comp | 18 + shaders/sprites.comp | 132 + shaders/terrain.comp | 451 + shaders/vines.comp | 224 + src/ai/pathfind.cpp | 362 + src/ai/pathfind.h | 94 + src/app/clip.h | 544 + src/app/gif_editor.h | 420 + src/app/gif_meta.h | 192 + src/app/hud.h | 652 + src/app/inventory.h | 216 + src/app/main.cpp | 13847 +++ src/app/perf.h | 151 + src/audio/audio_ipc.h | 34 + src/audio/fmod_audio.cpp | 88 + src/audio/fmod_audio.h | 39 + src/core/fastlz.h | 60 + src/core/materials.cpp | 186 + src/core/materials.h | 75 + src/core/parallel.h | 29 + src/core/plz.h | 36 + src/core/png.h | 73 + src/core/random.h | 147 + src/core/wak.cpp | 121 + src/core/wak.h | 47 + src/core/xml.cpp | 127 + src/core/xml.h | 34 + src/entity/component_defaults.inc | 1780 + src/entity/electricity.cpp | 152 + src/entity/electricity.h | 33 + src/entity/entity.cpp | 857 + src/entity/entity.h | 217 + src/entity/game_services.h | 93 + src/entity/herds.cpp | 47 + src/entity/herds.h | 14 + src/entity/material_mix.h | 55 + src/entity/runtime.cpp | 1274 + src/entity/runtime.h | 250 + src/entity/runtime_natives.cpp | 945 + src/entity/spawn_comp.h | 11 + src/entity/status.cpp | 233 + src/entity/status.h | 66 + src/gun/effects.cpp | 156 + src/gun/effects.h | 84 + src/gun/gpu_fx.cpp | 272 + src/gun/gpu_fx.h | 73 + src/gun/gun.cpp | 283 + src/gun/gun.h | 110 + src/gun/projectiles.cpp | 484 + src/gun/projectiles.h | 177 + src/lua/api_names.inc | 406 + src/lua/entity_api.h | 97 + src/lua/entity_natives.cpp | 514 + src/lua/gpu_lua.cpp | 456 + src/lua/gpu_lua.h | 34 + src/lua/gpu_lua_profiles.h | 24 + src/lua/gpu_lua_proto.c | 48 + src/lua/gpu_lua_proto.h | 35 + src/lua/gpu_lua_sources.h.in | 25 + src/lua/gpu_program_generated.h | 17 + src/lua/host_natives.cpp | 373 + src/lua/lua_host.cpp | 791 + src/lua/lua_host.h | 194 + src/lua/mod_loader.cpp | 91 + src/lua/mod_loader.h | 41 + src/phys/contact_solver.cpp | 99 + src/phys/contact_solver.h | 32 + src/phys/convex_distance.cpp | 132 + src/phys/convex_distance.h | 26 + src/phys/dynamic_tree.cpp | 87 + src/phys/dynamic_tree.h | 27 + src/phys/entity_physics.cpp | 67 + src/phys/entity_physics.h | 50 + src/phys/joint_solver.cpp | 183 + src/phys/joint_solver.h | 32 + src/phys/phys_system.cpp | 754 + src/phys/phys_system.h | 162 + src/phys/poly_decompose.cpp | 318 + src/phys/poly_decompose.h | 25 + src/phys/recovered_units.cpp | 145 + src/phys/recovered_units.h | 71 + src/phys/rigid_collision.cpp | 189 + src/phys/rigid_collision.h | 51 + src/phys/rigid_contact_world.cpp | 111 + src/phys/rigid_contact_world.h | 21 + src/phys/rigid_dynamics.cpp | 108 + src/phys/rigid_dynamics.h | 46 + src/phys/rigid_fracture.cpp | 61 + src/phys/rigid_fracture.h | 24 + src/phys/rigid_world_step.cpp | 140 + src/phys/rigid_world_step.h | 29 + src/phys/rigid_world_toi.cpp | 133 + src/phys/rigid_world_toi.h | 96 + src/phys/shape_queries.cpp | 88 + src/phys/shape_queries.h | 19 + src/phys/terrain_outline.cpp | 183 + src/phys/terrain_outline.h | 32 + src/phys/terrain_world.cpp | 87 + src/phys/terrain_world.h | 35 + src/phys/world.cpp | 203 + src/phys/world.h | 81 + src/physics/gpu_bodies.cpp | 199 + src/physics/gpu_bodies.h | 79 + src/physics/rigid.cpp | 308 + src/physics/rigid.h | 71 + src/world/area_setup.cpp | 370 + src/world/area_setup.h | 51 + src/world/area_store.cpp | 277 + src/world/area_store.h | 85 + src/world/biome_materials.cpp | 201 + src/world/biome_materials.h | 39 + src/world/biome_modifiers.cpp | 182 + src/world/biome_modifiers.h | 57 + src/world/biomes.cpp | 196 + src/world/biomes.h | 74 + src/world/cave_bitmap.cpp | 470 + src/world/cave_bitmap.h | 67 + src/world/chunk_spawn.cpp | 147 + src/world/chunk_spawn.h | 45 + src/world/noise.cpp | 85 + src/world/noise.h | 13 + src/world/noise_tables.inc | 7 + src/world/petri.cpp | 50 + src/world/petri.h | 26 + src/world/proc_biome.cpp | 157 + src/world/proc_biome.h | 23 + src/world/reactions.cpp | 94 + src/world/reactions.h | 30 + src/world/residency.h | 148 + src/world/sky.h | 66 + src/world/terrain.cpp | 162 + src/world/terrain.h | 61 + src/world/wang_gen.cpp | 80 + src/world/wang_gen.h | 45 + third_party/c-blosc2 | 1 + third_party/libtess2 | 1 + third_party/luajit/COPYRIGHT | 60 + third_party/luajit/build_luajit.bat | 11 + third_party/luajit/dynasm/Examples/run.sh | 14 + .../luajit/dynasm/Examples/test_z_inst.c | 488 + third_party/luajit/dynasm/dasm_arm.h | 458 + third_party/luajit/dynasm/dasm_arm.lua | 1125 + third_party/luajit/dynasm/dasm_arm64.h | 519 + third_party/luajit/dynasm/dasm_arm64.lua | 1166 + third_party/luajit/dynasm/dasm_mips.h | 420 + third_party/luajit/dynasm/dasm_mips.lua | 1008 + third_party/luajit/dynasm/dasm_mips64.lua | 12 + third_party/luajit/dynasm/dasm_ppc.h | 420 + third_party/luajit/dynasm/dasm_ppc.lua | 1919 + third_party/luajit/dynasm/dasm_proto.h | 83 + third_party/luajit/dynasm/dasm_s390x.h | 547 + third_party/luajit/dynasm/dasm_s390x.lua | 1634 + third_party/luajit/dynasm/dasm_x64.lua | 12 + third_party/luajit/dynasm/dasm_x86.h | 509 + third_party/luajit/dynasm/dasm_x86.lua | 2360 + third_party/luajit/dynasm/dynasm.lua | 1094 + third_party/luajit/src/.gitignore | 7 + third_party/luajit/src/Makefile | 725 + third_party/luajit/src/Makefile.dep | 246 + third_party/luajit/src/host/.gitignore | 3 + third_party/luajit/src/host/README | 4 + third_party/luajit/src/host/buildvm.c | 520 + third_party/luajit/src/host/buildvm.h | 105 + third_party/luajit/src/host/buildvm_asm.c | 412 + third_party/luajit/src/host/buildvm_fold.c | 229 + third_party/luajit/src/host/buildvm_lib.c | 457 + third_party/luajit/src/host/buildvm_libbc.h | 56 + third_party/luajit/src/host/buildvm_peobj.c | 392 + third_party/luajit/src/host/genlibbc.lua | 197 + third_party/luajit/src/host/genminilua.lua | 429 + third_party/luajit/src/host/minilua.c | 7770 ++ third_party/luajit/src/jit/.gitignore | 1 + third_party/luajit/src/jit/bc.lua | 228 + third_party/luajit/src/jit/bcsave.lua | 666 + third_party/luajit/src/jit/dis_arm.lua | 689 + third_party/luajit/src/jit/dis_arm64.lua | 1216 + third_party/luajit/src/jit/dis_arm64be.lua | 12 + third_party/luajit/src/jit/dis_mips.lua | 443 + third_party/luajit/src/jit/dis_mips64.lua | 17 + third_party/luajit/src/jit/dis_mips64el.lua | 17 + third_party/luajit/src/jit/dis_mipsel.lua | 17 + third_party/luajit/src/jit/dis_ppc.lua | 591 + third_party/luajit/src/jit/dis_x64.lua | 17 + third_party/luajit/src/jit/dis_x86.lua | 953 + third_party/luajit/src/jit/dump.lua | 715 + third_party/luajit/src/jit/p.lua | 311 + third_party/luajit/src/jit/v.lua | 170 + third_party/luajit/src/jit/zone.lua | 45 + third_party/luajit/src/lauxlib.h | 161 + third_party/luajit/src/lib_aux.c | 374 + third_party/luajit/src/lib_base.c | 716 + third_party/luajit/src/lib_bit.c | 180 + third_party/luajit/src/lib_debug.c | 405 + third_party/luajit/src/lib_ffi.c | 872 + third_party/luajit/src/lib_init.c | 55 + third_party/luajit/src/lib_io.c | 539 + third_party/luajit/src/lib_jit.c | 800 + third_party/luajit/src/lib_math.c | 226 + third_party/luajit/src/lib_os.c | 293 + third_party/luajit/src/lib_package.c | 704 + third_party/luajit/src/lib_string.c | 748 + third_party/luajit/src/lib_table.c | 392 + third_party/luajit/src/lj.supp | 41 + third_party/luajit/src/lj_alloc.c | 1490 + third_party/luajit/src/lj_alloc.h | 17 + third_party/luajit/src/lj_api.c | 1301 + third_party/luajit/src/lj_arch.h | 618 + third_party/luajit/src/lj_asm.c | 2416 + third_party/luajit/src/lj_asm.h | 17 + third_party/luajit/src/lj_asm_arm.h | 2212 + third_party/luajit/src/lj_asm_arm64.h | 2105 + third_party/luajit/src/lj_asm_mips.h | 2659 + third_party/luajit/src/lj_asm_ppc.h | 2251 + third_party/luajit/src/lj_asm_x86.h | 3121 + third_party/luajit/src/lj_bc.c | 14 + third_party/luajit/src/lj_bc.h | 265 + third_party/luajit/src/lj_bcdump.h | 68 + third_party/luajit/src/lj_bcread.c | 457 + third_party/luajit/src/lj_bcwrite.c | 361 + third_party/luajit/src/lj_buf.c | 232 + third_party/luajit/src/lj_buf.h | 103 + third_party/luajit/src/lj_carith.c | 437 + third_party/luajit/src/lj_carith.h | 38 + third_party/luajit/src/lj_ccall.c | 1234 + third_party/luajit/src/lj_ccall.h | 209 + third_party/luajit/src/lj_ccallback.c | 796 + third_party/luajit/src/lj_ccallback.h | 25 + third_party/luajit/src/lj_cconv.c | 754 + third_party/luajit/src/lj_cconv.h | 70 + third_party/luajit/src/lj_cdata.c | 299 + third_party/luajit/src/lj_cdata.h | 78 + third_party/luajit/src/lj_char.c | 43 + third_party/luajit/src/lj_char.h | 42 + third_party/luajit/src/lj_clib.c | 431 + third_party/luajit/src/lj_clib.h | 29 + third_party/luajit/src/lj_cparse.c | 1898 + third_party/luajit/src/lj_cparse.h | 65 + third_party/luajit/src/lj_crecord.c | 1892 + third_party/luajit/src/lj_crecord.h | 38 + third_party/luajit/src/lj_ctype.c | 637 + third_party/luajit/src/lj_ctype.h | 461 + third_party/luajit/src/lj_debug.c | 829 + third_party/luajit/src/lj_debug.h | 77 + third_party/luajit/src/lj_def.h | 361 + third_party/luajit/src/lj_dispatch.c | 557 + third_party/luajit/src/lj_dispatch.h | 156 + third_party/luajit/src/lj_emit_arm.h | 357 + third_party/luajit/src/lj_emit_arm64.h | 419 + third_party/luajit/src/lj_emit_mips.h | 295 + third_party/luajit/src/lj_emit_ppc.h | 238 + third_party/luajit/src/lj_emit_x86.h | 573 + third_party/luajit/src/lj_err.c | 858 + third_party/luajit/src/lj_err.h | 41 + third_party/luajit/src/lj_errmsg.h | 191 + third_party/luajit/src/lj_ff.h | 18 + third_party/luajit/src/lj_ffrecord.c | 1285 + third_party/luajit/src/lj_ffrecord.h | 24 + third_party/luajit/src/lj_frame.h | 311 + third_party/luajit/src/lj_func.c | 187 + third_party/luajit/src/lj_func.h | 24 + third_party/luajit/src/lj_gc.c | 854 + third_party/luajit/src/lj_gc.h | 134 + third_party/luajit/src/lj_gdbjit.c | 817 + third_party/luajit/src/lj_gdbjit.h | 22 + third_party/luajit/src/lj_ir.c | 494 + third_party/luajit/src/lj_ir.h | 591 + third_party/luajit/src/lj_ircall.h | 362 + third_party/luajit/src/lj_iropt.h | 162 + third_party/luajit/src/lj_jit.h | 505 + third_party/luajit/src/lj_lex.c | 509 + third_party/luajit/src/lj_lex.h | 86 + third_party/luajit/src/lj_lib.c | 310 + third_party/luajit/src/lj_lib.h | 116 + third_party/luajit/src/lj_load.c | 168 + third_party/luajit/src/lj_mcode.c | 381 + third_party/luajit/src/lj_mcode.h | 30 + third_party/luajit/src/lj_meta.c | 477 + third_party/luajit/src/lj_meta.h | 38 + third_party/luajit/src/lj_obj.c | 50 + third_party/luajit/src/lj_obj.h | 997 + third_party/luajit/src/lj_opt_dce.c | 78 + third_party/luajit/src/lj_opt_fold.c | 2555 + third_party/luajit/src/lj_opt_loop.c | 449 + third_party/luajit/src/lj_opt_mem.c | 935 + third_party/luajit/src/lj_opt_narrow.c | 654 + third_party/luajit/src/lj_opt_sink.c | 251 + third_party/luajit/src/lj_opt_split.c | 870 + third_party/luajit/src/lj_parse.c | 2728 + third_party/luajit/src/lj_parse.h | 18 + third_party/luajit/src/lj_profile.c | 368 + third_party/luajit/src/lj_profile.h | 21 + third_party/luajit/src/lj_record.c | 2651 + third_party/luajit/src/lj_record.h | 45 + third_party/luajit/src/lj_snap.c | 916 + third_party/luajit/src/lj_snap.h | 34 + third_party/luajit/src/lj_state.c | 302 + third_party/luajit/src/lj_state.h | 35 + third_party/luajit/src/lj_str.c | 244 + third_party/luajit/src/lj_str.h | 29 + third_party/luajit/src/lj_strfmt.c | 472 + third_party/luajit/src/lj_strfmt.h | 125 + third_party/luajit/src/lj_strfmt_num.c | 592 + third_party/luajit/src/lj_strscan.c | 547 + third_party/luajit/src/lj_strscan.h | 39 + third_party/luajit/src/lj_tab.c | 772 + third_party/luajit/src/lj_tab.h | 78 + third_party/luajit/src/lj_target.h | 166 + third_party/luajit/src/lj_target_arm.h | 270 + third_party/luajit/src/lj_target_arm64.h | 332 + third_party/luajit/src/lj_target_mips.h | 377 + third_party/luajit/src/lj_target_ppc.h | 280 + third_party/luajit/src/lj_target_s390x.h | 81 + third_party/luajit/src/lj_target_x86.h | 362 + third_party/luajit/src/lj_trace.c | 919 + third_party/luajit/src/lj_trace.h | 55 + third_party/luajit/src/lj_traceerr.h | 61 + third_party/luajit/src/lj_udata.c | 34 + third_party/luajit/src/lj_udata.h | 14 + third_party/luajit/src/lj_vm.h | 120 + third_party/luajit/src/lj_vmevent.c | 58 + third_party/luajit/src/lj_vmevent.h | 59 + third_party/luajit/src/lj_vmmath.c | 152 + third_party/luajit/src/ljamalg.c | 97 + third_party/luajit/src/lua.h | 404 + third_party/luajit/src/lua.hpp | 9 + third_party/luajit/src/luaconf.h | 160 + third_party/luajit/src/luajit.c | 608 + third_party/luajit/src/luajit.h | 81 + third_party/luajit/src/lualib.h | 44 + third_party/luajit/src/msvcbuild.bat | 122 + third_party/luajit/src/ps4build.bat | 123 + third_party/luajit/src/psvitabuild.bat | 93 + third_party/luajit/src/vm_arm.dasc | 4593 + third_party/luajit/src/vm_arm64.dasc | 3988 + third_party/luajit/src/vm_mips.dasc | 5264 + third_party/luajit/src/vm_mips64.dasc | 5112 + third_party/luajit/src/vm_ppc.dasc | 6053 ++ third_party/luajit/src/vm_s390x.dasc | 4262 + third_party/luajit/src/vm_x64.dasc | 4909 + third_party/luajit/src/vm_x86.dasc | 5808 ++ third_party/luajit/src/x64/Makefile | 13 + .../luajit/src/x64/src/lj_str_hash_x64.h | 271 + third_party/luajit/src/x64/test/Makefile | 48 + third_party/luajit/src/x64/test/benchmark.cxx | 278 + third_party/luajit/src/x64/test/test.cpp | 73 + .../luajit/src/x64/test/test_str_comp.lua | 67 + third_party/luajit/src/x64/test/test_util.cxx | 21 + third_party/luajit/src/x64/test/test_util.hpp | 57 + .../luajit/src/x64/test/unit/ffi/test_abi.lua | 10 + .../x64/test/unit/ffi/test_line_directive.lua | 15 + .../unit/ffi/test_pragma_pack_pushpop.lua | 12 + .../x64/test/unit/ffi/test_var_attribute.lua | 22 + third_party/luajit/src/x64/test/unit_test.sh | 22 + third_party/luajit/src/xb1build.bat | 101 + third_party/luajit/src/xedkbuild.bat | 92 + third_party/lz4 | 1 + third_party/noita_random/LICENSE | 21 + third_party/noita_random/noita_random.cpp | 72 + third_party/noita_random/wang/MurmurHash3.cpp | 334 + third_party/noita_random/wang/MurmurHash3.h | 37 + .../noita_random/wang/coalmine_hax.cpp | 79106 ++++++++++++++++ third_party/noita_random/wang/jps.hh | 1539 + third_party/noita_random/wang/pix.cpp | 105 + third_party/noita_random/wang/png.cpp | 87 + third_party/noita_random/wang/search.cpp | 328 + third_party/noita_random/wang/stb_hbwang.h | 1396 + third_party/noita_random/wang/stb_image.h | 7987 ++ .../noita_random/wang/stb_image_write.h | 1724 + third_party/noita_random/wang/wang.cpp | 940 + third_party/zstd | 1 + 416 files changed, 279512 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 shaders/apply.comp create mode 100644 shaders/areas.glsl create mode 100644 shaders/biome_mods.glsl create mode 100644 shaders/burn.comp create mode 100644 shaders/clip_half.comp create mode 100644 shaders/common.glsl create mode 100644 shaders/displace.comp create mode 100644 shaders/edits.comp create mode 100644 shaders/entities.comp create mode 100644 shaders/ents_prev.comp create mode 100644 shaders/explode_rays.comp create mode 100644 shaders/far.glsl create mode 100644 shaders/fog.comp create mode 100644 shaders/fx_anims.comp create mode 100644 shaders/fx_common.glsl create mode 100644 shaders/fx_hand.comp create mode 100644 shaders/fx_host.glsl create mode 100644 shaders/fx_particles.comp create mode 100644 shaders/fx_state.glsl create mode 100644 shaders/glow.comp create mode 100644 shaders/intent.comp create mode 100644 shaders/light_cells.comp create mode 100644 shaders/lua_native_runtime.glsl create mode 100644 shaders/lua_native_terrain.glsl create mode 100644 shaders/paint.comp create mode 100644 shaders/particle_req.comp create mode 100644 shaders/procgrad.glsl create mode 100644 shaders/proj_lua.comp create mode 100644 shaders/proj_lua_dispatch.glsl create mode 100644 shaders/proj_spawn.comp create mode 100644 shaders/proj_step.comp create mode 100644 shaders/projectiles.comp create mode 100644 shaders/react.comp create mode 100644 shaders/render.comp create mode 100644 shaders/rigid_burn.comp create mode 100644 shaders/rigid_commit.comp create mode 100644 shaders/rigid_common.glsl create mode 100644 shaders/rigid_events.comp create mode 100644 shaders/rigid_frame.comp create mode 100644 shaders/rigid_raster.comp create mode 100644 shaders/rigid_solve.comp create mode 100644 shaders/simplex3.glsl create mode 100644 shaders/sky_cols.comp create mode 100644 shaders/sprites.comp create mode 100644 shaders/terrain.comp create mode 100644 shaders/vines.comp create mode 100644 src/ai/pathfind.cpp create mode 100644 src/ai/pathfind.h create mode 100644 src/app/clip.h create mode 100644 src/app/gif_editor.h create mode 100644 src/app/gif_meta.h create mode 100644 src/app/hud.h create mode 100644 src/app/inventory.h create mode 100644 src/app/main.cpp create mode 100644 src/app/perf.h create mode 100644 src/audio/audio_ipc.h create mode 100644 src/audio/fmod_audio.cpp create mode 100644 src/audio/fmod_audio.h create mode 100644 src/core/fastlz.h create mode 100644 src/core/materials.cpp create mode 100644 src/core/materials.h create mode 100644 src/core/parallel.h create mode 100644 src/core/plz.h create mode 100644 src/core/png.h create mode 100644 src/core/random.h create mode 100644 src/core/wak.cpp create mode 100644 src/core/wak.h create mode 100644 src/core/xml.cpp create mode 100644 src/core/xml.h create mode 100644 src/entity/component_defaults.inc create mode 100644 src/entity/electricity.cpp create mode 100644 src/entity/electricity.h create mode 100644 src/entity/entity.cpp create mode 100644 src/entity/entity.h create mode 100644 src/entity/game_services.h create mode 100644 src/entity/herds.cpp create mode 100644 src/entity/herds.h create mode 100644 src/entity/material_mix.h create mode 100644 src/entity/runtime.cpp create mode 100644 src/entity/runtime.h create mode 100644 src/entity/runtime_natives.cpp create mode 100644 src/entity/spawn_comp.h create mode 100644 src/entity/status.cpp create mode 100644 src/entity/status.h create mode 100644 src/gun/effects.cpp create mode 100644 src/gun/effects.h create mode 100644 src/gun/gpu_fx.cpp create mode 100644 src/gun/gpu_fx.h create mode 100644 src/gun/gun.cpp create mode 100644 src/gun/gun.h create mode 100644 src/gun/projectiles.cpp create mode 100644 src/gun/projectiles.h create mode 100644 src/lua/api_names.inc create mode 100644 src/lua/entity_api.h create mode 100644 src/lua/entity_natives.cpp create mode 100644 src/lua/gpu_lua.cpp create mode 100644 src/lua/gpu_lua.h create mode 100644 src/lua/gpu_lua_profiles.h create mode 100644 src/lua/gpu_lua_proto.c create mode 100644 src/lua/gpu_lua_proto.h create mode 100644 src/lua/gpu_lua_sources.h.in create mode 100644 src/lua/gpu_program_generated.h create mode 100644 src/lua/host_natives.cpp create mode 100644 src/lua/lua_host.cpp create mode 100644 src/lua/lua_host.h create mode 100644 src/lua/mod_loader.cpp create mode 100644 src/lua/mod_loader.h create mode 100644 src/phys/contact_solver.cpp create mode 100644 src/phys/contact_solver.h create mode 100644 src/phys/convex_distance.cpp create mode 100644 src/phys/convex_distance.h create mode 100644 src/phys/dynamic_tree.cpp create mode 100644 src/phys/dynamic_tree.h create mode 100644 src/phys/entity_physics.cpp create mode 100644 src/phys/entity_physics.h create mode 100644 src/phys/joint_solver.cpp create mode 100644 src/phys/joint_solver.h create mode 100644 src/phys/phys_system.cpp create mode 100644 src/phys/phys_system.h create mode 100644 src/phys/poly_decompose.cpp create mode 100644 src/phys/poly_decompose.h create mode 100644 src/phys/recovered_units.cpp create mode 100644 src/phys/recovered_units.h create mode 100644 src/phys/rigid_collision.cpp create mode 100644 src/phys/rigid_collision.h create mode 100644 src/phys/rigid_contact_world.cpp create mode 100644 src/phys/rigid_contact_world.h create mode 100644 src/phys/rigid_dynamics.cpp create mode 100644 src/phys/rigid_dynamics.h create mode 100644 src/phys/rigid_fracture.cpp create mode 100644 src/phys/rigid_fracture.h create mode 100644 src/phys/rigid_world_step.cpp create mode 100644 src/phys/rigid_world_step.h create mode 100644 src/phys/rigid_world_toi.cpp create mode 100644 src/phys/rigid_world_toi.h create mode 100644 src/phys/shape_queries.cpp create mode 100644 src/phys/shape_queries.h create mode 100644 src/phys/terrain_outline.cpp create mode 100644 src/phys/terrain_outline.h create mode 100644 src/phys/terrain_world.cpp create mode 100644 src/phys/terrain_world.h create mode 100644 src/phys/world.cpp create mode 100644 src/phys/world.h create mode 100644 src/physics/gpu_bodies.cpp create mode 100644 src/physics/gpu_bodies.h create mode 100644 src/physics/rigid.cpp create mode 100644 src/physics/rigid.h create mode 100644 src/world/area_setup.cpp create mode 100644 src/world/area_setup.h create mode 100644 src/world/area_store.cpp create mode 100644 src/world/area_store.h create mode 100644 src/world/biome_materials.cpp create mode 100644 src/world/biome_materials.h create mode 100644 src/world/biome_modifiers.cpp create mode 100644 src/world/biome_modifiers.h create mode 100644 src/world/biomes.cpp create mode 100644 src/world/biomes.h create mode 100644 src/world/cave_bitmap.cpp create mode 100644 src/world/cave_bitmap.h create mode 100644 src/world/chunk_spawn.cpp create mode 100644 src/world/chunk_spawn.h create mode 100644 src/world/noise.cpp create mode 100644 src/world/noise.h create mode 100644 src/world/noise_tables.inc create mode 100644 src/world/petri.cpp create mode 100644 src/world/petri.h create mode 100644 src/world/proc_biome.cpp create mode 100644 src/world/proc_biome.h create mode 100644 src/world/reactions.cpp create mode 100644 src/world/reactions.h create mode 100644 src/world/residency.h create mode 100644 src/world/sky.h create mode 100644 src/world/terrain.cpp create mode 100644 src/world/terrain.h create mode 100644 src/world/wang_gen.cpp create mode 100644 src/world/wang_gen.h create mode 160000 third_party/c-blosc2 create mode 160000 third_party/libtess2 create mode 100644 third_party/luajit/COPYRIGHT create mode 100644 third_party/luajit/build_luajit.bat create mode 100644 third_party/luajit/dynasm/Examples/run.sh create mode 100644 third_party/luajit/dynasm/Examples/test_z_inst.c create mode 100644 third_party/luajit/dynasm/dasm_arm.h create mode 100644 third_party/luajit/dynasm/dasm_arm.lua create mode 100644 third_party/luajit/dynasm/dasm_arm64.h create mode 100644 third_party/luajit/dynasm/dasm_arm64.lua create mode 100644 third_party/luajit/dynasm/dasm_mips.h create mode 100644 third_party/luajit/dynasm/dasm_mips.lua create mode 100644 third_party/luajit/dynasm/dasm_mips64.lua create mode 100644 third_party/luajit/dynasm/dasm_ppc.h create mode 100644 third_party/luajit/dynasm/dasm_ppc.lua create mode 100644 third_party/luajit/dynasm/dasm_proto.h create mode 100644 third_party/luajit/dynasm/dasm_s390x.h create mode 100644 third_party/luajit/dynasm/dasm_s390x.lua create mode 100644 third_party/luajit/dynasm/dasm_x64.lua create mode 100644 third_party/luajit/dynasm/dasm_x86.h create mode 100644 third_party/luajit/dynasm/dasm_x86.lua create mode 100644 third_party/luajit/dynasm/dynasm.lua create mode 100644 third_party/luajit/src/.gitignore create mode 100644 third_party/luajit/src/Makefile create mode 100644 third_party/luajit/src/Makefile.dep create mode 100644 third_party/luajit/src/host/.gitignore create mode 100644 third_party/luajit/src/host/README create mode 100644 third_party/luajit/src/host/buildvm.c create mode 100644 third_party/luajit/src/host/buildvm.h create mode 100644 third_party/luajit/src/host/buildvm_asm.c create mode 100644 third_party/luajit/src/host/buildvm_fold.c create mode 100644 third_party/luajit/src/host/buildvm_lib.c create mode 100644 third_party/luajit/src/host/buildvm_libbc.h create mode 100644 third_party/luajit/src/host/buildvm_peobj.c create mode 100644 third_party/luajit/src/host/genlibbc.lua create mode 100644 third_party/luajit/src/host/genminilua.lua create mode 100644 third_party/luajit/src/host/minilua.c create mode 100644 third_party/luajit/src/jit/.gitignore create mode 100644 third_party/luajit/src/jit/bc.lua create mode 100644 third_party/luajit/src/jit/bcsave.lua create mode 100644 third_party/luajit/src/jit/dis_arm.lua create mode 100644 third_party/luajit/src/jit/dis_arm64.lua create mode 100644 third_party/luajit/src/jit/dis_arm64be.lua create mode 100644 third_party/luajit/src/jit/dis_mips.lua create mode 100644 third_party/luajit/src/jit/dis_mips64.lua create mode 100644 third_party/luajit/src/jit/dis_mips64el.lua create mode 100644 third_party/luajit/src/jit/dis_mipsel.lua create mode 100644 third_party/luajit/src/jit/dis_ppc.lua create mode 100644 third_party/luajit/src/jit/dis_x64.lua create mode 100644 third_party/luajit/src/jit/dis_x86.lua create mode 100644 third_party/luajit/src/jit/dump.lua create mode 100644 third_party/luajit/src/jit/p.lua create mode 100644 third_party/luajit/src/jit/v.lua create mode 100644 third_party/luajit/src/jit/zone.lua create mode 100644 third_party/luajit/src/lauxlib.h create mode 100644 third_party/luajit/src/lib_aux.c create mode 100644 third_party/luajit/src/lib_base.c create mode 100644 third_party/luajit/src/lib_bit.c create mode 100644 third_party/luajit/src/lib_debug.c create mode 100644 third_party/luajit/src/lib_ffi.c create mode 100644 third_party/luajit/src/lib_init.c create mode 100644 third_party/luajit/src/lib_io.c create mode 100644 third_party/luajit/src/lib_jit.c create mode 100644 third_party/luajit/src/lib_math.c create mode 100644 third_party/luajit/src/lib_os.c create mode 100644 third_party/luajit/src/lib_package.c create mode 100644 third_party/luajit/src/lib_string.c create mode 100644 third_party/luajit/src/lib_table.c create mode 100644 third_party/luajit/src/lj.supp create mode 100644 third_party/luajit/src/lj_alloc.c create mode 100644 third_party/luajit/src/lj_alloc.h create mode 100644 third_party/luajit/src/lj_api.c create mode 100644 third_party/luajit/src/lj_arch.h create mode 100644 third_party/luajit/src/lj_asm.c create mode 100644 third_party/luajit/src/lj_asm.h create mode 100644 third_party/luajit/src/lj_asm_arm.h create mode 100644 third_party/luajit/src/lj_asm_arm64.h create mode 100644 third_party/luajit/src/lj_asm_mips.h create mode 100644 third_party/luajit/src/lj_asm_ppc.h create mode 100644 third_party/luajit/src/lj_asm_x86.h create mode 100644 third_party/luajit/src/lj_bc.c create mode 100644 third_party/luajit/src/lj_bc.h create mode 100644 third_party/luajit/src/lj_bcdump.h create mode 100644 third_party/luajit/src/lj_bcread.c create mode 100644 third_party/luajit/src/lj_bcwrite.c create mode 100644 third_party/luajit/src/lj_buf.c create mode 100644 third_party/luajit/src/lj_buf.h create mode 100644 third_party/luajit/src/lj_carith.c create mode 100644 third_party/luajit/src/lj_carith.h create mode 100644 third_party/luajit/src/lj_ccall.c create mode 100644 third_party/luajit/src/lj_ccall.h create mode 100644 third_party/luajit/src/lj_ccallback.c create mode 100644 third_party/luajit/src/lj_ccallback.h create mode 100644 third_party/luajit/src/lj_cconv.c create mode 100644 third_party/luajit/src/lj_cconv.h create mode 100644 third_party/luajit/src/lj_cdata.c create mode 100644 third_party/luajit/src/lj_cdata.h create mode 100644 third_party/luajit/src/lj_char.c create mode 100644 third_party/luajit/src/lj_char.h create mode 100644 third_party/luajit/src/lj_clib.c create mode 100644 third_party/luajit/src/lj_clib.h create mode 100644 third_party/luajit/src/lj_cparse.c create mode 100644 third_party/luajit/src/lj_cparse.h create mode 100644 third_party/luajit/src/lj_crecord.c create mode 100644 third_party/luajit/src/lj_crecord.h create mode 100644 third_party/luajit/src/lj_ctype.c create mode 100644 third_party/luajit/src/lj_ctype.h create mode 100644 third_party/luajit/src/lj_debug.c create mode 100644 third_party/luajit/src/lj_debug.h create mode 100644 third_party/luajit/src/lj_def.h create mode 100644 third_party/luajit/src/lj_dispatch.c create mode 100644 third_party/luajit/src/lj_dispatch.h create mode 100644 third_party/luajit/src/lj_emit_arm.h create mode 100644 third_party/luajit/src/lj_emit_arm64.h create mode 100644 third_party/luajit/src/lj_emit_mips.h create mode 100644 third_party/luajit/src/lj_emit_ppc.h create mode 100644 third_party/luajit/src/lj_emit_x86.h create mode 100644 third_party/luajit/src/lj_err.c create mode 100644 third_party/luajit/src/lj_err.h create mode 100644 third_party/luajit/src/lj_errmsg.h create mode 100644 third_party/luajit/src/lj_ff.h create mode 100644 third_party/luajit/src/lj_ffrecord.c create mode 100644 third_party/luajit/src/lj_ffrecord.h create mode 100644 third_party/luajit/src/lj_frame.h create mode 100644 third_party/luajit/src/lj_func.c create mode 100644 third_party/luajit/src/lj_func.h create mode 100644 third_party/luajit/src/lj_gc.c create mode 100644 third_party/luajit/src/lj_gc.h create mode 100644 third_party/luajit/src/lj_gdbjit.c create mode 100644 third_party/luajit/src/lj_gdbjit.h create mode 100644 third_party/luajit/src/lj_ir.c create mode 100644 third_party/luajit/src/lj_ir.h create mode 100644 third_party/luajit/src/lj_ircall.h create mode 100644 third_party/luajit/src/lj_iropt.h create mode 100644 third_party/luajit/src/lj_jit.h create mode 100644 third_party/luajit/src/lj_lex.c create mode 100644 third_party/luajit/src/lj_lex.h create mode 100644 third_party/luajit/src/lj_lib.c create mode 100644 third_party/luajit/src/lj_lib.h create mode 100644 third_party/luajit/src/lj_load.c create mode 100644 third_party/luajit/src/lj_mcode.c create mode 100644 third_party/luajit/src/lj_mcode.h create mode 100644 third_party/luajit/src/lj_meta.c create mode 100644 third_party/luajit/src/lj_meta.h create mode 100644 third_party/luajit/src/lj_obj.c create mode 100644 third_party/luajit/src/lj_obj.h create mode 100644 third_party/luajit/src/lj_opt_dce.c create mode 100644 third_party/luajit/src/lj_opt_fold.c create mode 100644 third_party/luajit/src/lj_opt_loop.c create mode 100644 third_party/luajit/src/lj_opt_mem.c create mode 100644 third_party/luajit/src/lj_opt_narrow.c create mode 100644 third_party/luajit/src/lj_opt_sink.c create mode 100644 third_party/luajit/src/lj_opt_split.c create mode 100644 third_party/luajit/src/lj_parse.c create mode 100644 third_party/luajit/src/lj_parse.h create mode 100644 third_party/luajit/src/lj_profile.c create mode 100644 third_party/luajit/src/lj_profile.h create mode 100644 third_party/luajit/src/lj_record.c create mode 100644 third_party/luajit/src/lj_record.h create mode 100644 third_party/luajit/src/lj_snap.c create mode 100644 third_party/luajit/src/lj_snap.h create mode 100644 third_party/luajit/src/lj_state.c create mode 100644 third_party/luajit/src/lj_state.h create mode 100644 third_party/luajit/src/lj_str.c create mode 100644 third_party/luajit/src/lj_str.h create mode 100644 third_party/luajit/src/lj_strfmt.c create mode 100644 third_party/luajit/src/lj_strfmt.h create mode 100644 third_party/luajit/src/lj_strfmt_num.c create mode 100644 third_party/luajit/src/lj_strscan.c create mode 100644 third_party/luajit/src/lj_strscan.h create mode 100644 third_party/luajit/src/lj_tab.c create mode 100644 third_party/luajit/src/lj_tab.h create mode 100644 third_party/luajit/src/lj_target.h create mode 100644 third_party/luajit/src/lj_target_arm.h create mode 100644 third_party/luajit/src/lj_target_arm64.h create mode 100644 third_party/luajit/src/lj_target_mips.h create mode 100644 third_party/luajit/src/lj_target_ppc.h create mode 100644 third_party/luajit/src/lj_target_s390x.h create mode 100644 third_party/luajit/src/lj_target_x86.h create mode 100644 third_party/luajit/src/lj_trace.c create mode 100644 third_party/luajit/src/lj_trace.h create mode 100644 third_party/luajit/src/lj_traceerr.h create mode 100644 third_party/luajit/src/lj_udata.c create mode 100644 third_party/luajit/src/lj_udata.h create mode 100644 third_party/luajit/src/lj_vm.h create mode 100644 third_party/luajit/src/lj_vmevent.c create mode 100644 third_party/luajit/src/lj_vmevent.h create mode 100644 third_party/luajit/src/lj_vmmath.c create mode 100644 third_party/luajit/src/ljamalg.c create mode 100644 third_party/luajit/src/lua.h create mode 100644 third_party/luajit/src/lua.hpp create mode 100644 third_party/luajit/src/luaconf.h create mode 100644 third_party/luajit/src/luajit.c create mode 100644 third_party/luajit/src/luajit.h create mode 100644 third_party/luajit/src/lualib.h create mode 100644 third_party/luajit/src/msvcbuild.bat create mode 100644 third_party/luajit/src/ps4build.bat create mode 100644 third_party/luajit/src/psvitabuild.bat create mode 100644 third_party/luajit/src/vm_arm.dasc create mode 100644 third_party/luajit/src/vm_arm64.dasc create mode 100644 third_party/luajit/src/vm_mips.dasc create mode 100644 third_party/luajit/src/vm_mips64.dasc create mode 100644 third_party/luajit/src/vm_ppc.dasc create mode 100644 third_party/luajit/src/vm_s390x.dasc create mode 100644 third_party/luajit/src/vm_x64.dasc create mode 100644 third_party/luajit/src/vm_x86.dasc create mode 100644 third_party/luajit/src/x64/Makefile create mode 100644 third_party/luajit/src/x64/src/lj_str_hash_x64.h create mode 100644 third_party/luajit/src/x64/test/Makefile create mode 100644 third_party/luajit/src/x64/test/benchmark.cxx create mode 100644 third_party/luajit/src/x64/test/test.cpp create mode 100644 third_party/luajit/src/x64/test/test_str_comp.lua create mode 100644 third_party/luajit/src/x64/test/test_util.cxx create mode 100644 third_party/luajit/src/x64/test/test_util.hpp create mode 100644 third_party/luajit/src/x64/test/unit/ffi/test_abi.lua create mode 100644 third_party/luajit/src/x64/test/unit/ffi/test_line_directive.lua create mode 100644 third_party/luajit/src/x64/test/unit/ffi/test_pragma_pack_pushpop.lua create mode 100644 third_party/luajit/src/x64/test/unit/ffi/test_var_attribute.lua create mode 100644 third_party/luajit/src/x64/test/unit_test.sh create mode 100644 third_party/luajit/src/xb1build.bat create mode 100644 third_party/luajit/src/xedkbuild.bat create mode 160000 third_party/lz4 create mode 100644 third_party/noita_random/LICENSE create mode 100644 third_party/noita_random/noita_random.cpp create mode 100644 third_party/noita_random/wang/MurmurHash3.cpp create mode 100644 third_party/noita_random/wang/MurmurHash3.h create mode 100644 third_party/noita_random/wang/coalmine_hax.cpp create mode 100644 third_party/noita_random/wang/jps.hh create mode 100644 third_party/noita_random/wang/pix.cpp create mode 100644 third_party/noita_random/wang/png.cpp create mode 100644 third_party/noita_random/wang/search.cpp create mode 100644 third_party/noita_random/wang/stb_hbwang.h create mode 100644 third_party/noita_random/wang/stb_image.h create mode 100644 third_party/noita_random/wang/stb_image_write.h create mode 100644 third_party/noita_random/wang/wang.cpp create mode 160000 third_party/zstd diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8688066 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +build/ +build-cpu/ +__pycache__/ +analysis/atlas/*.sqlite +analysis/atlas/*.jsonl +analysis/*.log +third_party/luajit/src/*.lib +third_party/luajit/src/*.exe +third_party/luajit/src/*.exp +third_party/luajit/src/*.obj +third_party/luajit/src/*.manifest +third_party/luajit/src/buildvm_*.h +third_party/luajit/src/jit/vmdef.lua +demo/world_save.bin +demo/world_save.bin +clips/ +build_audio32/ + +tools/spell_lab_auto/jobs/all/_todo_* +demo diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..0e2d737 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,12 @@ +[submodule "third_party/c-blosc2"] + path = third_party/c-blosc2 + url = https://github.com/Blosc/c-blosc2.git +[submodule "third_party/lz4"] + path = third_party/lz4 + url = https://github.com/lz4/lz4.git +[submodule "third_party/libtess2"] + path = third_party/libtess2 + url = https://github.com/memononen/libtess2.git +[submodule "third_party/zstd"] + path = third_party/zstd + url = https://github.com/facebook/zstd.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..433803d --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,205 @@ +cmake_minimum_required(VERSION 3.20) +project(noita_gpu LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +if(MSVC) # Release keeps its optimisations and writes a PDB (the crash handler of noita_gpu prints a symbolised stack) + add_compile_options($<$:/Zi>) + add_link_options($<$:/DEBUG:FULL> $<$:/OPT:REF> $<$:/OPT:ICF>) +endif() + +add_library(ngcore STATIC + src/core/wak.cpp + src/core/xml.cpp + src/core/materials.cpp) +target_include_directories(ngcore PUBLIC src) +if(MSVC) + target_compile_options(ngcore PRIVATE /W4 /permissive-) +endif() + +# ---- GPU app (needs the Vulkan SDK: set VULKAN_SDK or -DVULKAN_SDK=...) ---- +if(NOT VULKAN_SDK AND DEFINED ENV{VULKAN_SDK}) + set(VULKAN_SDK $ENV{VULKAN_SDK}) +endif() +set(ENV{VULKAN_SDK} "${VULKAN_SDK}") +find_package(Vulkan REQUIRED) +find_program(GLSLC glslc HINTS ${VULKAN_SDK}/Bin REQUIRED) + +# Runtime Lua -> GLSL -> SPIR-V. Embed engine declarations, not generated Lua. +file(READ ${CMAKE_SOURCE_DIR}/shaders/common.glsl NG_LUA_COMMON) +string(REGEX MATCH "layout\\(push_constant\\)[^}]*} pc;" NG_LUA_PC "${NG_LUA_COMMON}") +file(READ ${CMAKE_SOURCE_DIR}/shaders/fx_common.glsl NG_LUA_FX) +string(REGEX MATCH "struct Ent [^{]*\\{[^}]*};" NG_LUA_ENT "${NG_LUA_FX}") +file(READ ${CMAKE_SOURCE_DIR}/shaders/fx_state.glsl NG_LUA_STATE) +string(REGEX MATCH "struct FProj [^{]*\\{[^}]*};" NG_LUA_PROJ "${NG_LUA_STATE}") +file(READ ${CMAKE_SOURCE_DIR}/shaders/lua_native_runtime.glsl NG_LUA_RUNTIME) +file(READ ${CMAKE_SOURCE_DIR}/shaders/lua_native_terrain.glsl NG_LUA_TERRAIN) +file(READ ${CMAKE_SOURCE_DIR}/shaders/proj_lua_dispatch.glsl NG_LUA_DISPATCH) +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS + shaders/common.glsl shaders/fx_common.glsl shaders/fx_state.glsl + shaders/lua_native_runtime.glsl shaders/lua_native_terrain.glsl shaders/proj_lua_dispatch.glsl) +if(NOT NG_LUA_PC OR NOT NG_LUA_ENT OR NOT NG_LUA_PROJ) + message(FATAL_ERROR "Cannot extract the engine ABI for runtime Lua shaders") +endif() +configure_file(src/lua/gpu_lua_sources.h.in generated/gpu_lua_sources.h @ONLY) +find_path(SHADERC_INCLUDE_DIR shaderc/shaderc.h HINTS ${VULKAN_SDK}/Include REQUIRED) +find_file(SHADERC_RUNTIME shaderc_shared.dll HINTS ${VULKAN_SDK}/Bin REQUIRED) +add_library(nggpulua STATIC src/lua/gpu_lua.cpp src/lua/gpu_lua_proto.c) +target_include_directories(nggpulua PUBLIC src PRIVATE ${CMAKE_BINARY_DIR}/generated ${SHADERC_INCLUDE_DIR}) +target_link_libraries(nggpulua PUBLIC luajit_static) + +# ---- Blosc2 (world save format: byte-shuffled, LZ4/Zstd-compressed tiles); lz4/zstd sources are local checkouts ---- +set(BUILD_SHARED OFF CACHE BOOL "" FORCE) +set(BUILD_STATIC ON CACHE BOOL "" FORCE) +set(BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(BUILD_FUZZERS OFF CACHE BOOL "" FORCE) +set(BUILD_BENCHMARKS OFF CACHE BOOL "" FORCE) +set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +set(BUILD_PLUGINS OFF CACHE BOOL "" FORCE) +set(DEACTIVATE_ZLIB ON CACHE BOOL "" FORCE) +set(BLOSC_ENABLE_ZFP OFF CACHE STRING "" FORCE) +set(BLOSC_LZ4_SOURCE_DIR ${CMAKE_SOURCE_DIR}/third_party/lz4 CACHE PATH "" FORCE) +set(BLOSC_ZSTD_SOURCE_DIR ${CMAKE_SOURCE_DIR}/third_party/zstd CACHE PATH "" FORCE) +set(BLOSC_MERGE_BUNDLED_STATIC OFF CACHE BOOL "" FORCE) # (the in-place merge of lz4 / zstd into libblosc2 ran on every build: blosc/CMakeLists.txt) +add_subdirectory(third_party/c-blosc2 EXCLUDE_FROM_ALL) +add_library(ngblosc INTERFACE) +target_link_libraries(ngblosc INTERFACE blosc2_static) +target_include_directories(ngblosc INTERFACE ${CMAKE_SOURCE_DIR}/third_party/c-blosc2/include) + +add_executable(noita_gpu src/app/main.cpp src/audio/fmod_audio.cpp) +target_link_libraries(noita_gpu PRIVATE ngspawn ngblosc nggpulua Vulkan::Vulkan) +# the short git hash of the sources at configure time (the GIF metadata's "build", docs/GIF_METADATA.md) +execute_process(COMMAND git rev-parse --short HEAD WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE NG_GIT_HASH OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) +if(NOT NG_GIT_HASH) + set(NG_GIT_HASH "dev") +endif() +target_compile_definitions(noita_gpu PRIVATE NG_BUILD="${NG_GIT_HASH}") +if(MSVC) + target_compile_options(noita_gpu PRIVATE /W4 /permissive- /bigobj) +endif() + +set(SHADERS intent apply paint render terrain sprites entities projectiles edits react burn rigid_solve rigid_commit rigid_frame rigid_events proj_spawn proj_step fx_particles fx_anims ents_prev fx_hand displace rigid_raster explode_rays rigid_burn vines sky_cols glow fog particle_req light_cells clip_half) +# shaders are rebuilt when their source or any included .glsl changes, then copied next to the executable +file(GLOB SHADER_INCLUDES ${CMAKE_SOURCE_DIR}/shaders/*.glsl ${CMAKE_SOURCE_DIR}/shaders/generated/*.glsl) +set(SPV_FILES "") +# Offline reference artifacts only; the game uses native C++ startup translation. +find_package(Python3 COMPONENTS Interpreter QUIET) +if(Python3_Interpreter_FOUND) + add_custom_target(lua_gpu_generate + COMMAND ${Python3_EXECUTABLE} -m tools.lua_gpu.engine + --data-root ${CMAKE_SOURCE_DIR}/.. + --out-dir ${CMAKE_SOURCE_DIR}/shaders/generated + --header ${CMAKE_SOURCE_DIR}/src/lua/gpu_program_generated.h + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMENT "Generating offline Lua GPU reference artifacts" + VERBATIM) +endif() +foreach(s ${SHADERS}) + add_custom_command(OUTPUT ${CMAKE_BINARY_DIR}/spv/${s}.spv + COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/spv + COMMAND ${GLSLC} --target-env=vulkan1.1 -O -I ${CMAKE_SOURCE_DIR}/shaders + ${CMAKE_SOURCE_DIR}/shaders/${s}.comp -o ${CMAKE_BINARY_DIR}/spv/${s}.spv + DEPENDS ${CMAKE_SOURCE_DIR}/shaders/${s}.comp ${SHADER_INCLUDES} + COMMENT "glslc ${s}.comp") + list(APPEND SPV_FILES ${CMAKE_BINARY_DIR}/spv/${s}.spv) + add_custom_command(TARGET noita_gpu POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_BINARY_DIR}/spv/${s}.spv $/${s}.spv) +endforeach() +add_custom_target(shaders ALL DEPENDS ${SPV_FILES}) +# building only the shaders (e.g. the `shaders` project in Visual Studio) must also refresh the demo folder and the exe folder +foreach(s ${SHADERS}) + add_custom_command(TARGET shaders POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_BINARY_DIR}/spv/${s}.spv ${CMAKE_SOURCE_DIR}/demo/${s}.spv + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_BINARY_DIR}/spv/${s}.spv ${CMAKE_BINARY_DIR}/$/${s}.spv) +endforeach() +# `cmake --install` / the INSTALL project: the demo folder gets the exe and the shaders +install(TARGETS noita_gpu RUNTIME DESTINATION ${CMAKE_SOURCE_DIR}/demo) +install(FILES ${SPV_FILES} DESTINATION ${CMAKE_SOURCE_DIR}/demo) +# keep the demo folder (run_*.bat) in step with the build +add_custom_command(TARGET noita_gpu POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different $ ${CMAKE_SOURCE_DIR}/demo/noita_gpu.exe) +foreach(s ${SHADERS}) + add_custom_command(TARGET noita_gpu POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_BINARY_DIR}/spv/${s}.spv ${CMAKE_SOURCE_DIR}/demo/${s}.spv) +endforeach() +add_dependencies(noita_gpu shaders) + +# noita_spelltest.exe: the same sources and engine, built to run only the spell test room (NG_SPELLTEST_ONLY in main.cpp); it uses the same shaders +add_executable(noita_spelltest src/app/main.cpp src/audio/fmod_audio.cpp) +target_link_libraries(noita_spelltest PRIVATE ngspawn ngblosc nggpulua Vulkan::Vulkan) +target_compile_definitions(noita_spelltest PRIVATE NG_BUILD="${NG_GIT_HASH}" NG_SPELLTEST_ONLY=1) +if(MSVC) + target_compile_options(noita_spelltest PRIVATE /W4 /permissive- /bigobj) +endif() +add_dependencies(noita_spelltest shaders) +foreach(app noita_gpu noita_spelltest) + add_custom_command(TARGET ${app} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${SHADERC_RUNTIME} $/shaderc_shared.dll + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${SHADERC_RUNTIME} ${CMAKE_SOURCE_DIR}/demo/shaderc_shared.dll) +endforeach() +install(FILES ${SHADERC_RUNTIME} DESTINATION ${CMAKE_SOURCE_DIR}/demo) +add_custom_command(TARGET noita_spelltest POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different $ ${CMAKE_SOURCE_DIR}/demo/noita_spelltest.exe) + +# pack_spelltest (not part of ALL): build/pack/noita_spelltest/ and build/pack/noita_spelltest_.zip, to run with the Steam Noita (packaging/spelltest/README.txt): +# the exe, the shaders, audio_host.exe (the 32-bit sound helper, built here in build_audio32) and the C++ runtime DLLs; never data.wak or FMOD +# cmake --build build --config Release --target pack_spelltest +set(CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP TRUE) +set(CMAKE_INSTALL_UCRT_LIBRARIES FALSE) +include(InstallRequiredSystemLibraries) # (only to find the redistributable DLLs: CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS) +string(REPLACE ";" "|" NG_PACK_SHADERS "${SHADERS}") +string(REPLACE ";" "|" NG_PACK_CRT "${CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS}") +set(NG_AUDIO32_DIR ${CMAKE_SOURCE_DIR}/build_audio32) +add_custom_target(pack_spelltest + COMMAND ${CMAKE_COMMAND} -S ${CMAKE_SOURCE_DIR}/tools/audio_host -B ${NG_AUDIO32_DIR} -G "${CMAKE_GENERATOR}" -A Win32 + COMMAND ${CMAKE_COMMAND} --build ${NG_AUDIO32_DIR} --config Release + COMMAND ${CMAKE_COMMAND} -DEXE=$ -DSPV_DIR=${CMAKE_BINARY_DIR}/spv "-DSHADERS=${NG_PACK_SHADERS}" + -DAUDIO=${NG_AUDIO32_DIR}/Release/audio_host.exe -DREADME=${CMAKE_SOURCE_DIR}/packaging/spelltest/README.txt "-DCRT=${NG_PACK_CRT}" + -DSHADERC=${SHADERC_RUNTIME} + -DOUT=${CMAKE_BINARY_DIR}/pack -DVERSION=${NG_GIT_HASH} -P ${CMAKE_SOURCE_DIR}/packaging/spelltest/pack.cmake + COMMENT "packing the spell room" + VERBATIM) +add_dependencies(pack_spelltest noita_spelltest shaders) + +enable_testing() + +# ---- world generation (vendored noita_random, MIT) ---- +add_library(ngworld STATIC src/world/wang_gen.cpp src/world/biomes.cpp src/world/petri.cpp src/world/noise.cpp src/world/biome_materials.cpp src/world/cave_bitmap.cpp src/world/proc_biome.cpp src/world/terrain.cpp src/world/reactions.cpp) +target_link_libraries(ngworld PUBLIC ngcore) +target_include_directories(ngworld PUBLIC src PRIVATE third_party/noita_random) +if(MSVC) + target_compile_options(ngworld PRIVATE /W0 /bigobj /permissive) +endif() + +add_library(ngspawn STATIC src/ai/pathfind.cpp src/world/chunk_spawn.cpp src/world/area_setup.cpp src/world/area_store.cpp src/world/biome_modifiers.cpp src/entity/entity.cpp src/entity/runtime.cpp src/entity/runtime_natives.cpp src/entity/electricity.cpp src/entity/status.cpp src/gun/gun.cpp src/gun/projectiles.cpp src/gun/effects.cpp src/gun/gpu_fx.cpp src/physics/rigid.cpp src/physics/gpu_bodies.cpp) +target_link_libraries(ngspawn PUBLIC ngworld nglua) + +# ---- Lua host (LuaJIT built from third_party/luajit with MSVC) ---- +set(LUAJIT_DIR ${CMAKE_SOURCE_DIR}/third_party/luajit) +if(NOT EXISTS ${LUAJIT_DIR}/src/lua51.lib) + message(STATUS "Building LuaJIT (static, x64)...") + execute_process(COMMAND cmd /c ${LUAJIT_DIR}/build_luajit.bat OUTPUT_QUIET ERROR_QUIET RESULT_VARIABLE LJ_RC) + if(NOT LJ_RC EQUAL 0) + message(FATAL_ERROR "LuaJIT build failed; run third_party/luajit/build_luajit.bat to see why") + endif() +endif() +add_library(luajit_static STATIC IMPORTED) +set_target_properties(luajit_static PROPERTIES IMPORTED_LOCATION ${LUAJIT_DIR}/src/lua51.lib + INTERFACE_INCLUDE_DIRECTORIES ${LUAJIT_DIR}/src) +add_library(nglua STATIC src/lua/lua_host.cpp src/lua/entity_natives.cpp src/lua/host_natives.cpp src/lua/mod_loader.cpp src/entity/herds.cpp) +target_link_libraries(nglua PUBLIC ngcore luajit_static) +if(MSVC) + target_compile_options(nglua PRIVATE /W4 /permissive-) +endif() + +# CPU rigid-body engine: the recovered Box2D of the original, used by the game through src/phys. +set(NG_CPU_PHYS_SOURCES src/phys/entity_physics.cpp src/phys/rigid_dynamics.cpp src/phys/rigid_collision.cpp src/phys/convex_distance.cpp src/phys/rigid_world_toi.cpp src/phys/dynamic_tree.cpp + src/phys/contact_solver.cpp src/phys/rigid_contact_world.cpp src/phys/rigid_world_step.cpp src/phys/joint_solver.cpp src/phys/shape_queries.cpp src/phys/recovered_units.cpp src/phys/world.cpp src/phys/rigid_fracture.cpp src/phys/terrain_outline.cpp src/phys/terrain_world.cpp src/phys/poly_decompose.cpp src/phys/phys_system.cpp) +file(GLOB NG_TESS_SOURCES third_party/libtess2/Source/*.c) # libtess2 (SGI free software licence B), the tessellator of the original +add_library(ngphys STATIC ${NG_CPU_PHYS_SOURCES} ${NG_TESS_SOURCES}) +target_link_libraries(ngspawn PUBLIC ngphys) +target_include_directories(ngphys PUBLIC cpu src third_party/libtess2/Include) +target_include_directories(ngphys PRIVATE third_party/libtess2/Source) + + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..52d2f70 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# noita_gpu documentation + +A GPU (Vulkan compute) reimplementation of Noita's engine in C++, loading the original game's `data.wak`. +Goal: rule-faithful (not bit-exact) falling-sand simulation on the GPU, with the original world generation +and the original Lua scripts driving content. + +## Build and run + +Requirements: Visual Studio 2022 (MSVC), CMake 3.20+, Vulkan SDK. +The Vulkan SDK location is passed as `-DVULKAN_SDK=...` (or the `VULKAN_SDK` environment variable). LuaJIT is +built automatically on first configure from `third_party/luajit`. + +``` +cmake -S . -B build -G "Visual Studio 17 2022" -A x64 -DVULKAN_SDK=F:/workspace/tools/VulkanSDK +cmake --build build --config Release +``` + +## Demo + +`demo/` holds a ready-to-run build of `noita_gpu` (exe + compiled shaders) and two launchers: `run_world.bat` (real Mines terrain, optional seed argument) and `run_sandbox.bat`. They read `data.wak` from two folders up (`..\..\data.wak`). Refresh it after a rebuild by copying `build/Release/noita_gpu.exe` and `*.spv`. diff --git a/shaders/apply.comp b/shaders/apply.comp new file mode 100644 index 0000000..3d22a73 --- /dev/null +++ b/shaders/apply.comp @@ -0,0 +1,27 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 8, local_size_y = 8) in; +#include "common.glsl" + +// Performs the moves that won claim arbitration. A mover wins only if it holds the highest +// priority on both its source and its target cell, so the winners' cell pairs are disjoint and +// no atomics or ordering are needed here. The displaced occupant takes the mover's old slot +// (the original's swap-based move primitive, FUN_0070cc00). +void main() { + ivec2 p = ivec2(gl_GlobalInvocationID.xy); + if (p.x >= pc.gw || p.y >= pc.gh) return; + uint i = uint(p.y * pc.gw + p.x); + uvec4 it = intent[i]; + if (it.x == NONE) return; + uint t = it.x & 0x7FFFFFFFu; + if (claims[i] != it.y || claims[t] != it.y) return; // lost arbitration, retry next pass + + uint cs = cells[i], ct = cells[t]; + vec2 et = ext[t]; + if ((it.x >> 31) != 0u) cs ^= DIR_BIT; // gas bounced: flip horizontal direction + cells[t] = cs; + ext[t] = vec2(uintBitsToFloat(it.z), uintBitsToFloat(it.w)); + cells[i] = ct; + ext[i] = et; + const uint bt = birth[t]; birth[t] = birth[i]; birth[i] = bt; // the age moves with the cell +} diff --git a/shaders/areas.glsl b/shaders/areas.glsl new file mode 100644 index 0000000..e00ea56 --- /dev/null +++ b/shaders/areas.glsl @@ -0,0 +1,26 @@ +// The loaded biome areas (host: composeRegion in main.cpp). One record of 16 words per area after the 8 word header, then the tile map (biome-map tile -> area + 1). +// header (8 words): [0] area count, [1] tile map offset, [2] tile map width, [3] tile map height, [4] offset of the per-tile topology record offsets, [5] world seed, [6] x offset of the gradient noise (float) +// record: [0] origin x, [1] origin y (world cell at the centre of wang pixel (0,0)), [2] wang width, [3] wang height, [4] offset in the solidity plane, [5] offset in the material plane, +// [6] scene x0, [7] scene y0, [8] scene width, [9] scene height, [10] offset of the chunk table of the scene planes (materials, visuals, backgrounds; chunks of 64 x 64 cells, absolute index of the chunk or 0xFFFFFFFF), [11] offset of the background tile, +// [12] background width, [13] background height, [14] offset of the biome's list, [15] its component count +layout(std430, set = 0, binding = 43) readonly buffer AreaTab { uint atab[]; }; + +int arec(int a, int k) { return int(atab[8 + 16 * a + k]); } + +// The area of the biome-map tile that holds the world cell, -1 when none is loaded. +int area_of(ivec2 w) { + int tx = (w.x >= 0 ? w.x / 512 : -((-w.x + 511) / 512)) + 35; + int ty = (w.y >= 0 ? w.y / 512 : -((-w.y + 511) / 512)) + 14; + if (tx < 0 || ty < 0 || tx >= int(atab[2]) || ty >= int(atab[3])) return -1; + return int(atab[atab[1] + uint(ty) * atab[2] + uint(tx)]) - 1; +} + +// Index of a world cell in the scene planes of area a, -1 when the cell is outside that area's planes. +int scene_index(int a, ivec2 w) { + ivec2 sc = w - ivec2(arec(a, 6), arec(a, 7)); + if (sc.x < 0 || sc.y < 0 || sc.x >= arec(a, 8) || sc.y >= arec(a, 9)) return -1; + int cw = (arec(a, 8) + 63) >> 6; + uint ct = atab[uint(arec(a, 10) + (sc.y >> 6) * cw + (sc.x >> 6))]; // scene planes are stored in chunks of 64 x 64 cells + if (ct == 0xFFFFFFFFu) return -1; + return int(ct) + ((sc.y & 63) << 6) + (sc.x & 63); +} diff --git a/shaders/biome_mods.glsl b/shaders/biome_mods.glsl new file mode 100644 index 0000000..8b7dbcc --- /dev/null +++ b/shaders/biome_mods.glsl @@ -0,0 +1,18 @@ +// The biome modifiers (data/scripts/biome_modifiers.lua, BiomeModifiers at biome + 0x170; src/world/biome_modifiers.h) of the biome-map tile under a world point, +// and the cold / warm conversions of the materials. Needs the Tables buffer as `tbl` (main.cpp fills both after the fog of war hole images): +// per tile: projectile_drag_coeff, entity_gravity_y_multiplier (float bits), bytes (fog_of_war_delta, fire_extinguish_chance, reaction_freeze_chance, reaction_unfreeze_chance), dust_amount (float bits) +// per material: cold_freezes_to_material + 1 | cold_freezes_chance_rev << 16, warmth_melts_to_material + 1 | warmth_melts_chance_rev << 16 +uint bm_fog_base() { const uint n = mats[mats.length() - 1u].w; return 1024u + 9u * n + 132u + n; } // the biome map's width, height, then the fog type per tile +uint bm_mods_base() { return bm_fog_base() + 2u + 8192u + 1u + 64u * 6u + 1u + 16u * 3u + 8u * 3272u; } +int bm_tile(vec2 w) { // the tile index, -1 outside the map (the world default: drag 1, gravity 1, no chances) + const uint fb = bm_fog_base(); + const int bw = int(tbl[fb]), bh = int(tbl[fb + 1u]); + const ivec2 t = ivec2(floor(w / 512.0)) + ivec2(35, 14); + if (bw <= 0 || t.x < 0 || t.y < 0 || t.x >= bw || t.y >= bh) return -1; + return t.y * bw + t.x; +} +float bm_drag(vec2 w) { const int t = bm_tile(w); return t < 0 ? 1.0 : uintBitsToFloat(tbl[bm_mods_base() + 4u * uint(t)]); } +float bm_gravity(vec2 w) { const int t = bm_tile(w); return t < 0 ? 1.0 : uintBitsToFloat(tbl[bm_mods_base() + 4u * uint(t) + 1u]); } +uint bm_bytes(vec2 w) { const int t = bm_tile(w); return t < 0 ? 0u : tbl[bm_mods_base() + 4u * uint(t) + 2u]; } // fog delta | fire extinguish << 8 | freeze << 16 | unfreeze << 24 +float bm_dust(vec2 w) { const int t = bm_tile(w); return t < 0 ? 0.0 : uintBitsToFloat(tbl[bm_mods_base() + 4u * uint(t) + 3u]); } // the biome modifier dust_amount (post_final.frag: tex_fog .g) +uvec2 bm_conversions(uint m) { const uint cb = bm_mods_base() + 8192u * 4u + 2u * m; return uvec2(tbl[cb], tbl[cb + 1u]); } diff --git a/shaders/burn.comp b/shaders/burn.comp new file mode 100644 index 0000000..38de02f --- /dev/null +++ b/shaders/burn.comp @@ -0,0 +1,192 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 8, local_size_y = 8) in; +#include "common.glsl" +#include "fx_state.glsl" +layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; }; // 1024 noise words, then 4 per material: burning explosion (type + 1, power, probability, damage), then 2 per material: flame and smoke material + 1 (0 = the default) +#include "biome_mods.glsl" + +// Fire, following docs/DISPLACEMENT_AND_FIRE.md (functions 0070f5e0, 0070f9e0, 0070fb10, 0070c6e0, 0070e060, 00709fc0), in a data-parallel form. +// +// * State of a cell: burning (bit 27) and the engine's signed fade byte (cell + 0x11), in the top byte of the birth word. A flame cell (fire material) carries its fade +// (the "intensity" the emitter gave it), a burning cell the fade of the cell that ignited it. +// * Ordinary ignition: a burnable, not burning cell ignites iff r % 101 < 100 - autoignition_temperature, and takes the source's fade. +// * Spread: every source (burning cell, fire source material, flame) first passes the gate r % 101 <= its fade, then picks ONE of its four neighbours; seen from +// the target each neighbour is therefore a source with probability gate x 1/4 (flames spread twice per update), then the ignition test applies. +// * A burning cell emits flames (generates_flames percent per update, intensity fade x (0.75 .. 1.29), into (x-1..x+1, y-1) if that cell is empty) and smoke +// (generates_smoke); a material that requires_oxygen restores its fade from temperature_of_fire when the flame was accepted and otherwise loses fade, burning +// stops when the fade is below 1. +// * Liquids (and powders, gases) burn out with a chance 1/(fire_hp+1) per update into a flame (fire_hp 0: at once); solids age (a counter in ext.x) and are +// removed on every 32nd frame once fire_hp < age (fire_hp -1 never). +// * Water does not put out a neighbour (0070c6e0 / 0070e060): it smothers a covered cell through requires_oxygen. +// * Hot smoke (0070f800 + CGasCell update 0070a670): smoke made by a burning cell carries an intensity trunc(fade x (0.75 .. 1.29)) (birth top byte); on an update that +// reaches it ((gas_speed + 1) / 101) the intensity drops by rand % 3 and, as the signed fade, spreads fire (rand % 101 < fade, then 0070f9e0): fire climbs through its smoke. +// * A burnable gas reached by a spread turns into its flame material at once (0070aa80: whatever the ignition draw; cell type fire excluded). +// The engine's random streams are shared and ordered; here every cell draws its own hash, the probabilities are the engine's. +const ivec2 D4[4] = ivec2[4](ivec2(0, -1), ivec2(-1, 0), ivec2(1, 0), ivec2(0, 1)); + +uint cell_at(ivec2 p) { + if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return NONE; + return cells[p.y * pc.gw + p.x]; +} + +float rnd(uint seed, uint k) { return float(hash3(seed, k, 0x51u) & 0xFFFFu) / 65536.0; } +uint flame_material(uint m, uint dflt) { const uint v = tbl[1024u + 4u * mats[mats.length() - 1u].w + 2u * m]; return v != 0u ? v - 1u : dflt; } // 0070f8c0: the CellData flame material (+0xe0) of the burning cell +uint smoke_material(uint m, uint dflt) { const uint v = tbl[1024u + 4u * mats[mats.length() - 1u].w + 2u * m + 1u]; return v != 0u ? v - 1u : dflt; } // 0070f800: its smoke material (+0xfc) + +// TODO(to-do #30): the flames over burning oil look a little taller / denser than the game's low line +void main() { + ivec2 p = ivec2(gl_GlobalInvocationID.xy); + if (p.x >= pc.gw || p.y >= pc.gh) return; + int i = p.y * pc.gw + p.x; + uint c = cells[i]; + uint m = cell_mat(c); + if (m == 0u) return; + uvec4 sp = mats[mats.length() - 1u]; // special ids: fire, smoke, steam, material count + uint mz = mats[3u * m + 2u].z, mw = mats[3u * m + 2u].w; + uint f = mflags(m); + bool burnable = ((mz >> 16) & 1u) != 0u; + bool burning = (c & BURN_BIT) != 0u; + uint seed = uint(i) * 977u + uint(pc.frame) * 64u + uint(pc.pass_); + int ai = int(mw & 255u); + int tempFire = int((mw >> 8) & 255u); + int smokeP = int((mw >> 16) & 255u), flameP = int((mw >> 24) & 255u); + int fireHp = int(mz << 16) >> 16; // fire_hp as the signed 16 bits + bool noBurnOut = fireHp == -1; // exactly -1 never burns out (0070e060); other negative values satisfy fire_hp < age + + { // the biome's reaction_freeze_chance / reaction_unfreeze_chance (0070e8c0; FREEZING, HOT): r % (cold_freezes_chance_rev + 1) < freeze turns the cell into its + // cold_freezes_to_material when the random neighbour is free; r % (warmth_melts_chance_rev + 1) < unfreeze turns it into its warmth_melts_to_material when that + // neighbour is free or of another material (the cell-type gate +0xa0 and the contact predicate 004ac0f0 of the freeze are not ported) + const uint bb = bm_bytes(vec2(p + ivec2(pc.view_x, pc.view_y))); + const uint fc = (bb >> 16) & 0xFFu, uc = bb >> 24; + if ((fc | uc) != 0u && !burning) { + const uvec2 cv = bm_conversions(m); + const uint nb = cell_at(p + D4[hash3(seed, 41u, 9u) & 3u]); + const uint nm = nb == NONE ? 0u : cell_mat(nb); + if (fc != 0u && (cv.x & 0xFFFFu) != 0u && (hash3(seed, 42u, 9u) & 0x7FFFu) % ((cv.x >> 16) + 1u) < fc && nm == 0u) { cells[i] = ((cv.x & 0xFFFFu) - 1u) | (c & 0xFF0000u); ext[i] = vec2(0.0); birth[i] = birth_frame(); return; } + if (uc != 0u && (cv.y & 0xFFFFu) != 0u && (hash3(seed, 43u, 9u) & 0x7FFFu) % ((cv.y >> 16) + 1u) < uc && nm != m) { cells[i] = ((cv.y & 0xFFFFu) - 1u) | (c & 0xFF0000u); ext[i] = vec2(0.0); birth[i] = birth_frame(); return; } + } + } + + if (!burning && (f & F_GAS) != 0u) { // CGasCell update 0070a670: with probability (gas_speed + 1) / 101 (else the update only drifts the gas), intensity -= rand % 3, + // fade = (char) intensity; a positive one then spreads (rand % 101 < fade, then 0070f9e0) - the targets below count this smoke as a source + const uint raw = (birth[i] >> 24) & 255u; + if (raw != 0u && (mats[3u * m + 1u].w & 255u) >= uint(rnd(seed, 30u) * 101.0)) { + const int inten = max(int(raw) - int(hash3(seed, 31u, 7u) % 3u), 0); + birth[i] = with_fade(birth[i], inten); + } + if (!burnable) return; + } + if (!burning) { + if (!burnable) return; + // ignition from the four neighbours + // 0070f9e0: every source attempts: r % 101 <= its fade, one of its four neighbours is chosen, then the ignition draw; a fire cell attempts twice per update (00709fc0). The target + // sees each neighbour as a source with that probability, and the first one that succeeds gives the fade it carries (not the strongest neighbour's) + int srcFade = 0; + bool ignited = false; + float pIgnite = max(0.0, float(100 - ai)) / 101.0; + for (int d = 0; d < 4 && !ignited; ++d) { + ivec2 np = p + D4[d]; + uint n = cell_at(np); + if (n == NONE) continue; + uint nm = cell_mat(n); + if (nm == 0u) continue; + uint nz = mats[3u * nm + 2u].z, nw = mats[3u * nm + 2u].w; + bool nBurning = (n & BURN_BIT) != 0u; + const bool hotGas = !nBurning && (mflags(nm) & F_GAS) != 0u && ((nz >> 17) & 1u) == 0u; + int nf = fade_of(birth[np.y * pc.gw + np.x]); + bool nSource = nBurning || ((nz >> 17) & 1u) != 0u || (hotGas && nf > 0); + if (!nSource) continue; + if (!nBurning && !hotGas && nf == 0) nf = int((nw >> 8) & 255u); // a fire source material without a stored fade: its temperature_of_fire + bool flame = (mflags(nm) & F_FIRE) != 0u; + float gate = nf < 0 ? 0.0 : min(float(nf) + 1.0, 101.0) / 101.0; + // (a hot gas: its update reaches the spread with (gas_speed + 1) / 101, then rand % 101 < fade, then 0070f9e0's own gate) + if (hotGas) gate *= float(min(mats[3u * nm + 1u].w & 255u, 100u) + 1u) / 101.0 * float(nf) / 101.0; + // 0070aa80: a gas target (not of cell type fire) turns into its flame material whenever a spread reaches it, whatever the ignition draw + float pSrc = gate * 0.25 * ((f & F_GAS) != 0u ? 1.0 : pIgnite); // gate, then this neighbour is the one chosen, then the ignition draw + if (flame) pSrc = 1.0 - (1.0 - pSrc) * (1.0 - pSrc); // two separate attempts + if (rnd(seed, 1u + uint(d)) < pSrc) { ignited = true; srcFade = nf; } + } + if (ignited && (f & F_GAS) != 0u) { const uint fm = flame_material(m, sp.x); cells[i] = fm | (c & 0xFF0000u); birth[i] = with_fade(birth_frame(), int((mats[3u * fm + 2u].w >> 8) & 255u)); ext[i] = vec2(0.0); return; } + if (ignited) { cells[i] = c | BURN_BIT; birth[i] = with_fade(birth[i], srcFade); } + return; + } + + // (no "water next to it puts it out": the cell updates 0070c6e0 / 0070e060 extinguish a burning cell only by the world's gate at +0x451 (r % 100 + 1 < byte); water + // smothers a fire through requires_oxygen - a covered cell cannot put its flame into a free cell above, its fade runs out - while oil floating on water burns on) + // the explosion of a burning cell (0070fb10, tail): p <= (r / 32767) * min(p + 0.5, 100) with p the create_cell_probability (100 by default: only r = 32767, one draw in 32768 per update); + // the radius is min(sqrt(power * (0.75 + 0.5 u)), 150); the blast is a projectile of the material's type, spawned here and exploding on its first step + { + const uint bt = tbl[1024u + 4u * m]; + if (bt != 0u) { + const float pr = uintBitsToFloat(tbl[1024u + 4u * m + 2u]); + const float r1 = float(hash3(seed, 12u, 5u) & 0x7FFFu) / 32767.0; + if (pr <= r1 * min(pr + 0.5, 100.0)) { + const float rad = min(sqrt(uintBitsToFloat(tbl[1024u + 4u * m + 1u]) * (0.75 + 0.5 * float(hash3(seed, 13u, 5u) & 0x7FFFu) / 32767.0)), 150.0); + uint slot = atomicAdd(fcnt[0], 1u) % uint(FP_MAX); + FProj bp; + bp.a = vec4(vec2(p + ivec2(pc.view_x, pc.view_y)) + 0.5, 0.0, 0.0); + bp.b = vec4(0.0, 1.0, 0.0, 0.0); + bp.c = vec4(rad, uintBitsToFloat(tbl[1024u + 4u * m + 3u]), 0.0, 0.0); + bp.d = ivec4(int(bt) - 1, -1, 1 << 8, 1); + bp.e = vec4(0.0); + fprojs[slot] = bp; + cells[i] = 0u; ext[i] = vec2(0.0); // the cell is consumed + return; + } + } + } + int fade = fade_of(birth[i]); + bool liquidClass = (f & (F_LIQUID | F_GAS | F_LIQCELL)) != 0u; // (cell_type liquid, static ones too: wood_static, sand_static... are CLiquidCells) + + // flames and smoke into (x-1..x+1, y-1), the materials of the burning cell's CellData + bool flameAccepted = false; + if (flameP > 0 && int(rnd(seed, 2u) * 101.0) < flameP) { + ivec2 t = p + ivec2(int(rnd(seed, 4u) * 3.0) - 1, -1); + if (t.x >= 0 && t.y >= 0 && t.x < pc.gw && t.y < pc.gh) { + int ti = t.y * pc.gw + t.x; + if (cell_mat(cells[ti]) == 0u) { + const uint fm = flame_material(m, sp.x); + // (0070f8c0: the intensity goes to the new gas cell's +0x2c; its fade byte +0x11, the gate of its spread, is what a new cell of the flame material has: its + // temperature_of_fire, 0070f450 - fire: 100. A flame of burning oil ignites wood as readily as any other) + if (atomicCompSwap(cells[ti], 0u, fm | (hash3(seed, 9u, 1u) & 0xFF0000u)) == 0u) { flameAccepted = true; birth[ti] = with_fade(birth_frame(), int((mats[3u * fm + 2u].w >> 8) & 255u)); } + } + } + } + if (((mz >> 19) & 1u) != 0u) { // requires_oxygen: an accepted flame restores the fade, a failure takes one off the signed byte (wrapping) + fade = flameAccepted ? tempFire : ((fade - 1 + 128) & 255) - 128; + birth[i] = with_fade(birth[i], fade); + if (fade < 1) { cells[i] = c & ~BURN_BIT; return; } + } + if (smokeP > 0 && int(rnd(seed, 6u) * 101.0) < smokeP) { + ivec2 t = p + ivec2(int(rnd(seed, 7u) * 3.0) - 1, -1); + if (t.x >= 0 && t.y >= 0 && t.x < pc.gw && t.y < pc.gh) { + const int ti = t.y * pc.gw + t.x; + // 0070f800: a new cell of type gas gets the intensity trunc(fade x (0.75 .. 1.29)) at +0x2c (here the birth word's top byte, unsigned); its gas update (0070a670) spreads fire with it + const uint smm = smoke_material(m, sp.y); + const int inten = (mflags(smm) & F_GAS) != 0u ? clamp(int(float(fade) * (0.75 + 0.54 * rnd(seed, 8u))), 0, 255) : 0; + if (atomicCompSwap(cells[ti], 0u, smm | (hash3(seed, 9u, 2u) & 0xFF0000u)) == 0u) birth[ti] = with_fade(birth_frame(), inten); + } + } + + { // the biome's fire_extinguish_chance (0070c6e0 / 0070e060, after the burning tick 0070fb10): the cell goes out when rng % 100 + 1 < it (MOIST: 11) + const uint fx = (bm_bytes(vec2(p + ivec2(pc.view_x, pc.view_y))) >> 8) & 0xFFu; + if (fx != 0u && (hash3(seed, 31u, 7u) & 0x7FFFu) % 100u + 1u < fx) { cells[i] = c & ~BURN_BIT; return; } + } + if (liquidClass) { // liquids, powders, gases: burn out into a flame with chance 1/(fire_hp+1) (0: at once) + bool burnOut = !noBurnOut && (fireHp <= 0 || int(hash3(seed, 8u, 3u) % uint(fireHp + 1)) == 0); + if (!burnOut && !noBurnOut && fireHp > 0 && (f & F_STATIC) != 0u && int(hash3(seed, 8u, 4u) % uint(fireHp + 1)) <= 29) { + // a passive (static) liquid (0070c6e0): a second draw <= 29 also burns it out when a random cardinal neighbour is empty (00743e80: one of four by two bits of a + // draw; outside the window the opposite one is looked at) - the surface of burning wood is eaten away + const uint k = hash3(seed, 8u, 5u) & 3u; + ivec2 q = p + D4[k]; + if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) q = p - D4[k]; + if (q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh && cells[q.y * pc.gw + q.x] == 0u) burnOut = true; + } + if (burnOut) { const uint fm = flame_material(m, sp.x); cells[i] = fm | (c & 0xFF0000u); birth[i] = with_fade(birth_frame(), int((mats[3u * fm + 2u].w >> 8) & 255u)); ext[i] = vec2(0.0); return; } // (a new cell of the flame material: its own fade) + } else if ((f & F_STATIC) != 0u) { // solids: a wrapping 16-bit burn age; removed every 32nd frame once fire_hp < age + int age = (int(ext[i].x) + 1) & 0xFFFF; + if ((pc.frame & 31) == 0 && !noBurnOut && fireHp < age) { cells[i] = 0u; ext[i] = vec2(0.0); return; } + ext[i] = vec2(float(age), ext[i].y); + } +} diff --git a/shaders/clip_half.comp b/shaders/clip_half.comp new file mode 100644 index 0000000..3bf7cb0 --- /dev/null +++ b/shaders/clip_half.comp @@ -0,0 +1,23 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 8, local_size_y = 8) in; +#include "common.glsl" + +// The frame for the replay recorder at half size (2 x 2 average) when the window is wider than 1600 px: the copy to the host is a quarter +// (main.cpp; a 2560 x 1360 frame is 14 MB, copied every simulation tick). +layout(std430, set = 0, binding = 58) buffer ClipHalf { uint clipHalf[]; }; +layout(std430, set = 0, binding = 60) readonly buffer ClipRaw { uint clipraw[]; }; // the window picture without the HUD (render.comp) + +void main() { + const ivec2 q = ivec2(gl_GlobalInvocationID.xy); + const int hw = pc.ow / 2, hh = pc.oh / 2; + if (q.x >= hw || q.y >= hh) return; + const int i = 2 * q.y * pc.ow + 2 * q.x; + const uvec4 a = uvec4(clipraw[i], clipraw[i + 1], clipraw[i + pc.ow], clipraw[i + pc.ow + 1]); + uint o = 0u; + for (int c = 0; c < 32; c += 8) { + const uvec4 v = (a >> uint(c)) & 0xFFu; + o |= ((v.x + v.y + v.z + v.w + 2u) >> 2) << uint(c); + } + clipHalf[q.y * hw + q.x] = o; +} diff --git a/shaders/common.glsl b/shaders/common.glsl new file mode 100644 index 0000000..8649ea4 --- /dev/null +++ b/shaders/common.glsl @@ -0,0 +1,84 @@ +// Shared by intent/apply/paint/render compute shaders. +// +// Cell word (u32): bits 0-15 material index, 16-23 colour variation, 26 gas/fire horizontal direction. +// Per-cell extension (vec2): x = vertical velocity accumulator, y = horizontal velocity. +// Material table: 3 uvec4 per material +// [0] colour(ARGB), flags, density(float), liquid_gravity(float) +// [1] liquid_viscosity(int), liquid_damping(float), liquid_flow_speed(float), +// gas speeds packed: byte0 gas_speed, byte1 up, byte2 horizontal, byte3 down +// [2] lifetime(int), durability(float), burn word 1 (fire_hp | burnable << 16 | on_fire << 17 | extinguishes << 18), burn word 2 (autoignition | temperature_of_fire << 8 | smoke << 16) +// The last entry of the table is special: (fire id, smoke id, steam id, material count). +layout(std430, set = 0, binding = 0) buffer Cells { uint cells[]; }; +layout(std430, set = 0, binding = 1) readonly buffer Mats { uvec4 mats[]; }; +layout(std430, set = 0, binding = 2) buffer Pixels { uint pixels[]; }; +layout(std430, set = 0, binding = 3) buffer Ext { vec2 ext[]; }; +layout(std430, set = 0, binding = 42) buffer Birth { uint birth[]; }; // the frame a cell got its material (cell age = frame - birth, 0070ebf0 req_lifetime) +layout(std430, set = 0, binding = 4) buffer Claims { uint claims[]; }; +// per cell: x = target index | (flip direction << 31), y = priority, z/w = new vy/vx bits +layout(std430, set = 0, binding = 5) buffer Intent { uvec4 intent[]; }; + +layout(push_constant) uniform PC { + int gw, gh; // grid size in cells + int ow, oh; // output size in pixels + int scale; // pixels per cell + int frame; + int pass_; // movement pass within the frame + int brush_x, brush_y, brush_r; + int brush_mat; // -1: none, 0: erase + int seed; + int view_x, view_y; // world cell at the top-left of the grid + int area_ox, area_oy; // world cell at the centre of wang pixel (0,0) + int pw, ph; // wang plane size + int m_rock, m_soil, m_sand, m_wet; // fill materials + int shift_x, shift_y, shift_mode; + int scene_x0, scene_y0, scene_w, scene_h; // pixel-scene plane placement (cells) + int bg_w, bg_h; // biome background tile size (0 = none) + int ent_count; // sprite instances for sprites.comp // terrain.comp: mode 1 = scroll by (shift_x, shift_y), keep the overlap +} pc; + +const uint F_STATIC = 1u; // never moves (solid or liquid_static) +const uint F_LIQUID = 2u; // liquid or powder +const uint F_GAS = 4u; +const uint F_SAND = 8u; // powder (liquid_sand) +const uint F_AIR = 16u; +const uint F_FIRE = 32u; +const uint F_LIQCELL = 128u; // cell_type liquid, static or not (the native count / immersion predicate: liquid slot 22 true, powder slot 25 false) +const uint F_POWDER = 256u; // liquid_sand, static or not +const uint F_STAINS = 512u; // liquid_stains: a body in it takes its stain (vine points: the stored material that slows them) +const uint F_NOSTAIN = 2048u; // CellData stainable off (the box2d materials): no explosion burn +const uint F_INDESTRUCT = 1024u; // material tag [indestructible]: a CellEaterComponent with ignored_material_tag leaves it +const uint F_SLIME = 64u; // liquid_slime: moved by 0070b7c0 (docs/IMPLEMENTATION_BLOCKERS.md) +const uint NONE = 0xFFFFFFFFu; +const uint DIR_BIT = 1u << 26; +const uint BURN_BIT = 1u << 27; // the cell is burning +const uint FROZEN_BIT = 1u << 26; // a static cell stained frozen (MagicConvertMaterialComponent stain_frozen; bits 24-25 then hold its damage level, explode_rays.comp) +const uint ATTACH_BIT = 1u << 26; // a powder attached to a wall or ceiling (passive: byte 22 of the original cell); bits 24-25 then hold the direction: 1 right (state 5), 2 above (6), 3 left (7) +// Fire fade (the engine's signed fade byte, in steps of 4): bits 24-25 and 28-31 of the cell word. Burning cells and flames carry it. +uint cell_fade6(uint c) { return ((c >> 28) & 15u) | (((c >> 24) & 3u) << 4); } +uint with_fade6(uint c, uint f) { f = min(f, 63u); return (c & ~0xF3000000u) | ((f & 15u) << 28) | ((f >> 4) << 24); } + +// The age of a cell and its fire fade share the birth word: bits 0-23 the frame it got its material (the age is the difference modulo 2^24), bits 24-31 the engine's signed fade byte (cell + 0x11). +const uint BIRTH_MASK = 0xFFFFFFu; +uint birth_frame() { return uint(pc.frame) & BIRTH_MASK; } +uint birth_age(uint b) { return (uint(pc.frame) - b) & BIRTH_MASK; } +int fade_of(uint b) { return int(b) >> 24; } // signed byte +uint with_fade(uint b, int f) { return (b & BIRTH_MASK) | (uint(f & 255) << 24); } + +uint cell_mat(uint c) { return c & 0xFFFFu; } +uint mflags(uint m) { return mats[3u * m].y; } +float mdensity(uint m) { return uintBitsToFloat(mats[3u * m].z); } +float mgravity(uint m) { return uintBitsToFloat(mats[3u * m].w); } +int mviscosity(uint m) { return int(mats[3u * m + 1u].x & 0xFFFFu); } +uint mceiling(uint m) { return mats[3u * m + 1u].x >> 16; } // liquid_sticks_to_ceiling (powders) +float mdamping(uint m) { return uintBitsToFloat(mats[3u * m + 1u].y); } +float mflow(uint m) { return uintBitsToFloat(mats[3u * m + 1u].z); } +uint mgasbits(uint m) { return mats[3u * m + 1u].w; } +int mlifetime(uint m) { return int(mats[3u * m + 2u].x); } + +uint hash(uint x) { + x ^= x >> 16; x *= 0x7feb352du; + x ^= x >> 15; x *= 0x846ca68bu; + x ^= x >> 16; + return x; +} +uint hash3(uint a, uint b, uint c) { return hash(a * 0x9E3779B1u ^ hash(b + 0x85EBCA6Bu ^ hash(c))); } diff --git a/shaders/displace.comp b/shaders/displace.comp new file mode 100644 index 0000000..978906e --- /dev/null +++ b/shaders/displace.comp @@ -0,0 +1,90 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 64) in; +#include "rigid_common.glsl" + +// Rigid bodies displace liquids: the liquid cells inside their volume are moved to the nearest free cell outside it, away from the centre (preferring upward). +// (One workgroup per entity, which return at once, then one per rigid body.) +layout(std430, set = 0, binding = 23) readonly buffer Ctrl { ivec4 inp; } ctrl; + +// Liquids, and for bodies that do not go through sand (CellData solid_go_through_sand, bit 9 of the body flags) the powders too: a body that sinks into a pile pushes the grains out. +bool liquid_cell(uint c, bool powders) { + uint m = cell_mat(c); + if (m == 0u) return false; + uint f = mflags(m); + if ((f & F_STATIC) != 0u) return false; + return (f & F_LIQUID) != 0u && ((f & F_SAND) == 0u || powders); +} + +// Moves the cell at world position w out of the volume: the volume is a box (bodyIdx < 0) or a rigid body's mask. +void push_out(ivec2 w, vec2 centre, ivec4 box, int bodyIdx) { + const bool powders = bodyIdx >= 0 && (bodies[bodyIdx].i.z & 512) == 0; + ivec2 p = w - ivec2(pc.view_x, pc.view_y); + if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return; + int i = p.y * pc.gw + p.x; + uint c = cells[i]; + if (!liquid_cell(c, powders)) return; + vec2 d = vec2(w) + 0.5 - centre; + d.y -= 0.8; // up is preferred + d = length(d) < 0.01 ? vec2(0.0, -1.0) : normalize(d); + // the target is claimed first (only free cells are claimed), then the source is released; another invocation that already moved the source (bodies that overlap handle the + // same cell) makes this one give its target back. Nothing is lost or doubled: before, the source was emptied first and put back when no free cell was found, and a neighbour + // could fill the briefly empty source in between (a full pool with rubble in it lost its water) + const vec2 e = ext[i]; + for (int r = 1; r <= 12; ++r) { + ivec2 t = w + ivec2(round(d * float(r))); + ivec2 q = t - ivec2(pc.view_x, pc.view_y); + if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) break; + bool inside; + if (bodyIdx < 0) inside = t.x >= box.x && t.x < box.z && t.y >= box.y && t.y < box.w; + else { + Body b = bodies[bodyIdx]; + int s = b.i.x; + vec2 dd = vec2(t) + 0.5 - b.p.xy; + float cs = cos(b.p.z), sn = sin(b.p.z); + vec2 im = vec2(cs * dd.x + sn * dd.y, -sn * dd.x + cs * dd.y) + vec2(shf(s, 5), shf(s, 6)); + inside = shape_solid(s, int(floor(im.x)), int(floor(im.y))); + } + if (inside) continue; + int ti = q.y * pc.gw + q.x; + if (atomicCompSwap(cells[ti], 0u, c) == 0u) { // a free target, now holding the liquid + if (atomicCompSwap(cells[i], c, 0u) == c) { ext[ti] = e; ext[i] = vec2(0.0); } + else atomicCompSwap(cells[ti], c, 0u); // the source went elsewhere meanwhile: give the target back + return; + } + } + // nowhere to go: the liquid stays where it is +} + +void main() { + int g = int(gl_WorkGroupID.x), t = int(gl_LocalInvocationID.x); + int nEnt = ctrl.inp.w; + if (g < nEnt) { + return; // creatures and the player displace through the particle path of entities.comp (docs/DISPLACEMENT_AND_FIRE.md); only rigid bodies are pushed out here (the engine's body rule is not recovered) + Ent e = ents[g]; + int kind = e.st.x; + if ((kind != 1 && kind != 2 && kind != 3) || (e.st.w & 8) != 0) return; + ivec2 ip = ivec2(floor(e.a.xy)); + ivec2 lo = ip + ivec2(int(e.box.x), int(e.box.z)), hi = ip + ivec2(int(e.box.y), int(e.box.w)); + ivec2 sz = hi - lo; + if (sz.x <= 0 || sz.y <= 0 || sz.x * sz.y > 4096) return; + vec2 centre = vec2(lo + hi) * 0.5; + for (int k = t; k < sz.x * sz.y; k += 64) push_out(lo + ivec2(k % sz.x, k / sz.x), centre, ivec4(lo, hi), -1); + } else { + int bi = g - nEnt; + if (bi >= bhdr.x) return; + Body b = bodies[bi]; + if ((b.i.z & 2) != 0) return; + int s = b.i.x; + float rad = shf(s, 9); + int n = int(2.0 * rad) + 2; + ivec2 lo = ivec2(floor(b.p.xy - vec2(rad))); + for (int k = t; k < n * n; k += 64) { + ivec2 w = lo + ivec2(k % n, k / n); + vec2 dd = vec2(w) + 0.5 - b.p.xy; + float cs = cos(b.p.z), sn = sin(b.p.z); + vec2 im = vec2(cs * dd.x + sn * dd.y, -sn * dd.x + cs * dd.y) + vec2(shf(s, 5), shf(s, 6)); + if (shape_solid(s, int(floor(im.x)), int(floor(im.y)))) push_out(w, b.p.xy, ivec4(0), bi); + } + } +} diff --git a/shaders/edits.comp b/shaders/edits.comp new file mode 100644 index 0000000..8e7b9ec --- /dev/null +++ b/shaders/edits.comp @@ -0,0 +1,191 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 8, local_size_y = 8) in; +#include "common.glsl" +#include "far.glsl" +#include "fx_state.glsl" + +// Explosions and spills: circular edits of the cell grid, two vec4 per edit. +// e[2k] = (world x, world y, radius, max durability) +// e[2k+1] = (mode, material, probability, 0) mode 0: dig (cells whose durability is at most the limit become air) +// mode 1: fill air cells with the material, each with the given probability +// mode 4: a flask sucks the liquid: the cell becomes air when it still holds material e[2k+1].y +// mode 3: the same with a velocity (a poured flask): e[2k].w = horizontal velocity (cells per frame), e[2k+1].w = vertical velocity +// mode 5: CellEaterComponent (00a639e0): every cell inside the radius whose material is not e[2k+1].y (ignored_material) and not [indestructible] +// (with ignored_material_tag) is removed with probability e[2k+1].z (eat_probability); e[2k].w = 1: only_stain (nothing removed) +// mode 6: MagicConvertMaterialComponent (00bab8c0 / 00baac80): the rings e[2k].z .. e[2k].w (excluded) of the square (e[2k+1].w = 0) or circle (1) +// around the point: a cell of material e[2k+1].y (-1: any material) becomes e[2k+1].z +// mode 7: a line from (e[2k].x, e[2k].y) to (e[2k].z, e[2k].w): its empty cells become material e[2k+1].y (an ArcComponent of type MATERIAL; the host +// cut the line at the first cell of another material) +// mode 8: MaterialSeaSpawnerComponent (00bbc060), two records: (left x, top y, width, first row) (8, material, end row, 0), then +// (sine_wavelength, sine_amplitude, noise_scale, noise_threshold) (0, 0, 0, 0): the empty cells of the rows +// first .. end (excluded) at y = top + row + int(sin(x / wavelength) x amplitude) where 3D simplex noise (00872560: +// x scale, y scale, 0) x 0.5 + 0.5 >= threshold become the material +// mode 10: every non-air cell not e[2k+1].y / .z becomes e[2k+1].z when static, else .y (ConvertEverythingToGold) +// mode 11: the one cell at (x, y) is set on fire with the fade e[2k+1].y: ordinary ignition (0070f5e0: a burnable cell, not burning, +// r % 101 < 100 - autoignition_temperature) or, with e[2k+1].z = 1, forced (0070f750: no draw) - TorchComponent, hack_is_set_fire +// mode 9: AnimalAI eating (00a232e0): the cells of material e[2k+1].y inside the box (x, y) +- (e[2k].z, e[2k].w) become, each with +// probability e[2k+1].z, air (e[2k+1].w = 0) or material e[2k+1].w - 1 (food_particle_effect_material) +// Up to 256 edits per frame. +layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; }; // after 1024 + 8 words per material: the material tag bits (one word per material, bits assigned by the host) +layout(std430, set = 0, binding = 27) readonly buffer Edits { ivec4 head; vec4 e[512]; } ed; // filled by the host and by the GPU projectiles + +#include "simplex3.glsl" + +// pass 1: the far blocks (far.glsl) - per edit (z) the cells of a 128 x 128 box around its point outside the window: dig (0), fill (1), the cell eater (5), +// magic convert (6: freezing charge's liquids to ice); +// the slot is marked dirty (main.cpp copies it back to the store) +void far_pass() { + const int k = int(gl_GlobalInvocationID.z); + if (k >= ed.head.x || k >= 256) return; + const vec4 q = ed.e[2 * k], r = ed.e[2 * k + 1]; + const int mode = int(r.x + 0.5); + if (mode != 0 && mode != 1 && mode != 5 && mode != 6 && mode != 12) return; + const ivec2 wc = ivec2(floor(q.xy)) - 64 + ivec2(gl_GlobalInvocationID.xy); + const ivec2 lp = wc - ivec2(pc.view_x, pc.view_y); + if (lp.x >= 0 && lp.y >= 0 && lp.x < pc.gw && lp.y < pc.gh) return; // (the window's own pass) + const vec2 w = vec2(wc) + 0.5; + if (mode != 6 && distance(w, q.xy) > q.z) return; + const int s = far_slot(wc); + if (s == 0) return; + const int fi = far_index(s, wc); + const uint c = farcells[fi], m = cell_mat(c); + const uint hi = uint(fi) * 2654435761u; + if (mode == 12) { // a frozen stain (as the window's) + if (m == 0u || (mflags(m) & F_STATIC) == 0u || (mflags(m) & F_FIRE) != 0u || (c & FROZEN_BIT) != 0u) return; + farcells[fi] = c | FROZEN_BIT; + } else if (mode == 6) { // (as the window's: the rings q.z .. q.w of the circle / square, from r.y (-1: any) to r.z) + if (m == 0u) return; + const vec2 d = w - q.xy; + const int ring = r.w > 0.5 ? int(length(d) + 0.5) : int(max(abs(d.x), abs(d.y))); + if (ring < int(q.z) || ring >= int(q.w) || (r.y >= 0.0 && m != uint(r.y)) || m == uint(r.z)) return; + farcells[fi] = uint(r.z) | (((hash3(hi, uint(pc.frame), uint(k) + 41u) >> 16) & 0xFFu) << 16); + } else if (mode == 5) { + if (m == 0u || (r.y >= 0.0 && m == uint(r.y)) || (q.w > 1.5 && (mflags(m) & F_INDESTRUCT) != 0u)) return; + if (r.w > 0.5 && ((tbl[1024u + 8u * mats[mats.length() - 1u].w + m] >> uint(r.w - 1.0)) & 1u) != 0u) return; + if (float(hash3(hi, uint(pc.frame), uint(k) + 37u) % 101u) >= r.z * 100.0 || q.w == 1.0 || q.w == 3.0) return; + farcells[fi] = 0u; + } else if (mode == 0) { + if (m == 0u || uintBitsToFloat(mats[3u * m + 2u].y) > q.w) return; + farcells[fi] = 0u; + } else { + if (m != 0u) return; + const uint h = hash3(hi, uint(pc.frame), uint(k) + 31u); + if (float(h & 0xFFFFu) / 65536.0 >= r.z) return; + farcells[fi] = uint(r.y) | (((h >> 16) & 0xFFu) << 16); + } + farmap[kFarDirty + s - 1] = 1; +} + +void main() { + if (pc.pass_ == 1) { far_pass(); return; } + ivec2 p = ivec2(gl_GlobalInvocationID.xy); + if (p.x >= pc.gw || p.y >= pc.gh) return; + int i = p.y * pc.gw + p.x; + uint m = cell_mat(cells[i]); + vec2 w = vec2(p + ivec2(pc.view_x, pc.view_y)) + 0.5; + for (int k = 0; k < ed.head.x && k < 256; ++k) { + vec4 q = ed.e[2 * k], r = ed.e[2 * k + 1]; + if (r.x > 1.5 && r.x < 2.5) continue; // mode 2 (angular explosion) is explode_rays.comp + if (r.x > 10.5 && r.x < 11.5) { // ignite one cell + if (p + ivec2(pc.view_x, pc.view_y) != ivec2(floor(q.xy)) || m == 0u || ((mats[3u * m + 2u].z >> 16) & 1u) == 0u || (cells[i] & BURN_BIT) != 0u) continue; + if (r.z < 0.5 && int(hash3(uint(i), uint(pc.frame), uint(k) + 71u) % 101u) >= 100 - int(mats[3u * m + 2u].w & 255u)) continue; + cells[i] |= BURN_BIT; birth[i] = with_fade(birth[i], int(r.y)); + continue; + } + if (r.x > 12.5 && r.x < 14.5) { // the player digs out of the ground (main.cpp, 00bfa850): q = the box; 13: its loose cells fly out as real particles (007175c0: not a + // liquid_static material - the powders and liquids), r.y = the speed s; 14: every cell of the box goes (stuck in the static ground for long) + if (m == 0u || w.x < q.x || w.x > q.z + 1.0 || w.y < q.y || w.y > q.w + 1.0) continue; + if (r.x > 13.5) { cells[i] = 0u; ext[i] = vec2(0.0); m = 0u; continue; } + if ((mflags(m) & F_STATIC) != 0u || (mflags(m) & (F_LIQUID | F_SAND)) == 0u) continue; + const uint h1 = hash3(uint(i), uint(pc.frame), uint(k) + 81u), h2 = hash3(uint(i), uint(pc.frame), uint(k) + 83u); + const float s = r.y, c = mix(-1.0, -0.65, float(h1 & 0xFFFFu) / 65536.0) * s, b = mix(-0.5, 0.5, float(h1 >> 16) / 65536.0) * s; + const float a = float(h2 & 0xFFFFu) / 65536.0 - 0.5, mul = 0.65 + 0.4 * float(h2 >> 16) / 65536.0; + const vec2 v = vec2(cos(a) * b - sin(a) * c, sin(a) * b + cos(a) * c) * mul / 60.0; + if (emit_material_particle(w, v, 600.0, PART_GRAV, mats[3u * m].x, int(m))) { cells[i] = 0u; ext[i] = vec2(0.0); m = 0u; } + continue; + } + if (r.x > 11.5 && r.x < 12.5) { // a frozen stain (MagicConvertMaterialComponent stain_frozen: freeze_charge.xml): the static cells inside the radius look frozen (FROZEN_BIT on a static cell) + if (m != 0u && distance(w, q.xy) <= q.z && (mflags(m) & F_STATIC) != 0u && (mflags(m) & F_FIRE) == 0u) cells[i] |= FROZEN_BIT; + continue; + } + if (r.x > 4.5 && r.x < 5.5) { // cell eater + if (m == 0u || distance(w, q.xy) > q.z) continue; + if ((r.y >= 0.0 && m == uint(r.y)) || (q.w > 1.5 && (mflags(m) & F_INDESTRUCT) != 0u)) continue; + if (r.w > 0.5 && ((tbl[1024u + 8u * mats[mats.length() - 1u].w + m] >> uint(r.w - 1.0)) & 1u) != 0u) continue; // ignored_material_tag + if (float(hash3(uint(i), uint(pc.frame), uint(k) + 37u) % 101u) >= r.z * 100.0) continue; + if (q.w == 1.0 || q.w == 3.0) continue; // only_stain: the colour darkens in the engine (not drawn here) + { // 00a639e0: the eaten cell leaves a cosmetic particle of its material (00716ce0 force-created at the cell, velocity uniform(-30, 30) px/s on each axis, + // lifetime arguments 5 .. 10): the debris in the tunnel of a black hole. TODO(to-do #36): the unit of that lifetime - the real game's debris + // (spell_reference/BH_SPEED_PLAIN.gif) is gone after 0.5 .. 1 s, so 30 .. 60 frames here + const uint h = hash3(uint(i), uint(pc.frame), uint(k) + 39u); + const vec2 v = (vec2(float(h & 0xFFFFu), float(h >> 16)) / 65536.0 * 60.0 - 30.0) / 60.0; + emit_particle_g(w, v, 6.0 * (5.0 + 5.0 * float(hash3(uint(i), uint(pc.frame), 41u) & 0xFFFFu) / 65536.0), PART_GRAV, mats[3u * m].x, false, false, true); + } + cells[i] = 0u; ext[i] = vec2(0.0); m = 0u; + continue; + } + if (r.x > 8.5 && r.x < 9.5) { // a creature eats + if (m == 0u || m != uint(r.y) || abs(w.x - q.x) > q.z + 0.5 || abs(w.y - q.y) > q.w + 0.5) continue; + if (float(hash3(uint(i), uint(pc.frame), uint(k) + 53u) & 0xFFFFu) / 65536.0 >= r.z) continue; + if (r.w < 0.5) { cells[i] = 0u; ext[i] = vec2(0.0); m = 0u; continue; } + const uint h = hash3(uint(i), uint(pc.frame), uint(k) + 59u); + m = uint(r.w - 1.0); cells[i] = m | (((h >> 16) & 0xFFu) << 16); ext[i] = vec2(0.0); birth[i] = birth_frame(); + continue; + } + if (r.x > 5.5 && r.x < 6.5) { // magic convert + if (m == 0u) continue; + const vec2 d = w - q.xy; + const int ring = r.w > 0.5 ? int(length(d) + 0.5) : int(max(abs(d.x), abs(d.y))); + if (ring < int(q.z) || ring >= int(q.w)) continue; + if ((r.y >= 0.0 && m != uint(r.y)) || m == uint(r.z)) continue; + const uint h = hash3(uint(i), uint(pc.frame), uint(k) + 41u); + cells[i] = uint(r.z) | (((h >> 16) & 0xFFu) << 16); ext[i] = vec2(0.0); birth[i] = birth_frame(); m = uint(r.z); + continue; + } + if (r.x > 7.5 && r.x < 8.5) { // a material sea (its parameters are the next record) + const vec4 sp = ed.e[2 * k + 2]; + k += 1; + if (m != 0u) continue; + const int xi = int(floor(w.x)), yi = int(floor(w.y)); + if (xi < int(q.x) || xi >= int(q.x) + int(q.z)) continue; + const int row = yi - int(q.y) - int(sin(float(xi) / sp.x) * sp.y); + if (row < int(q.w) || row >= int(r.z)) continue; + if (simplex3(vec3(float(xi) * sp.z, float(yi) * sp.z, 0.0)) * 0.5 + 0.5 < sp.w) continue; + const uint h = hash3(uint(i), uint(pc.frame), uint(k) + 47u); + cells[i] = uint(r.y) | (((h >> 16) & 0xFFu) << 16); ext[i] = vec2(0.0); birth[i] = birth_frame(); m = uint(r.y); + continue; + } + if (r.x > 9.5 && r.x < 10.5) { // everything to gold (00704f70: every material but air is redefined as gold, the static ones as gold_static; here the cells take it) + if (m == 0u || m == uint(r.y) || m == uint(r.z)) continue; + const uint g = (mflags(m) & F_STATIC) != 0u ? uint(r.z) : uint(r.y); + const uint h = hash3(uint(i), uint(pc.frame), uint(k) + 61u); + cells[i] = g | (((h >> 16) & 0xFFu) << 16); ext[i] = vec2(0.0); birth[i] = birth_frame(); m = g; + continue; + } + if (r.x > 6.5 && r.x < 7.5) { // a material line + if (m != 0u) continue; + const vec2 ab = q.zw - q.xy; + const float l2 = dot(ab, ab); + const float t = l2 > 0.0 ? clamp(dot(w - q.xy, ab) / l2, 0.0, 1.0) : 0.0; + if (distance(w, q.xy + ab * t) > 0.5) continue; + const uint h = hash3(uint(i), uint(pc.frame), uint(k) + 43u); + cells[i] = uint(r.y) | (((h >> 16) & 0xFFu) << 16); ext[i] = vec2(0.0); birth[i] = birth_frame(); m = uint(r.y); + continue; + } + if (distance(w, q.xy) > q.z) continue; + const bool moving = r.x > 2.5 && r.x < 3.5; + if (r.x > 3.5) { if (m == uint(r.y)) { cells[i] = 0u; ext[i] = vec2(0.0); m = 0u; } continue; } + if (r.x < 0.5) { + if (m != 0u && uintBitsToFloat(mats[3u * m + 2u].y) <= q.w) { cells[i] = 0u; ext[i] = vec2(0.0); m = 0u; } + } else if (m == 0u) { + uint h = hash3(uint(i), uint(pc.frame), uint(k) + 31u); + if (float(h & 0xFFFFu) / 65536.0 < r.z) { + cells[i] = uint(r.y) | (((h >> 16) & 0xFFu) << 16); + ext[i] = moving ? vec2(r.w, q.w) : vec2(0.0); // x = vertical, y = horizontal velocity + birth[i] = birth_frame(); + m = uint(r.y); + } + } + } +} diff --git a/shaders/entities.comp b/shaders/entities.comp new file mode 100644 index 0000000..5caa76d --- /dev/null +++ b/shaders/entities.comp @@ -0,0 +1,722 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 64) in; +#include "common.glsl" +#include "fx_state.glsl" +#include "fx_host.glsl" + +// Entity simulation: gravity, walking, jumping, flying and a basic AI, one invocation per entity, once per frame. +// Entities collide with the live cell grid (solid = static or powder cells). Entities that are not fully inside the +// simulated window are frozen. Kinds: 0 static, 1 walker, 2 flyer, 3 player (index 0, controlled by the host). +struct Ent { + vec4 a; // position xy, velocity xy (cells, cells per frame) + vec4 box; // min_x, max_x, min_y, max_y + vec4 mv; // walk speed, max speed x, jump velocity (negative = up), gravity + vec4 ai; // detection range x, y, melee distance, melee damage + ivec4 st; // kind, facing, timer, flags (1 grounded, 2 can jump, 4 can fly, 8 dead, 16 in liquid) + vec4 hp; // hp, attack cooldown, max fall speed, climb-over height + ivec4 x; // damage taken this frame (milli-hp), jetpack energy / idle flag, spawn x (player) / ranged cooldown, spawn y + ivec4 rg; // ranged attack: projectile type (-1 none), frames between shots, min distance, max distance + ivec4 rg2; // projectiles per shot (min, max), muzzle offset y, the attack of the last attacking tick (1 melee, 2 ranged | tick << 4) + ivec4 aux; // player: maximum hp (milli-hp); creatures: velocity of the last hit (milli-cells per frame) + vec4 ex; // detection cone (cos of half angle), eye offset y, melee enabled, 0 + vec4 ex2; // ranged attack: projectile speed, gravity + ivec4 ex3; // blood material + 1, blood colour, damage types taken (1 projectile, 2 explosion, 4 fire, 8 melee), 0 + vec4 ex4; // air in the lungs (frames), air maximum (0 = no air needed), damage per frame without air, frames left on fire + vec4 ex5; // swimming: swim_idle_buoyancy_coeff, swim_up_buoyancy_coeff, swim_drag, swim_extra_horizontal_drag + vec4 ex6; // swim_down_buoyancy_coeff, frames not swimming, pixel of the last liquid count (x, y) + vec4 ex7; // blood_multiplier (creatures); liquid inventory of a body: reserve, material + 1, frames of emission left + ivec4 ex8; vec4 ex9; vec4 ex10; vec4 ex11; vec4 ex12, ex13, ex14, ex15, ex16, ex17, ex18, ex19; // damage event of a body: hit point relative to the body x 1000, damage x 1000 pending +}; + +struct Proj { vec4 a; vec4 b; vec4 c; }; // pos, vel / damage, life, gravity, colour bits / owner (0 creature, 1 player) +struct PType { vec4 a; vec4 b; }; // speed min, max, damage, lifetime / gravity, colour bits, 0, 0 +layout(std430, set = 0, binding = 24) readonly buffer PTypes { PType ptypes[]; }; +layout(std430, set = 0, binding = 25) buffer Projs { Proj projs[]; }; +layout(std430, set = 0, binding = 22) buffer Ents { Ent ents[]; }; +layout(std430, set = 0, binding = 23) buffer Ctrl { + ivec4 inp; // input x (-1/0/1), jump held, attack pressed this frame, entity count + ivec4 inp2; // frame, has player, attack hits (debug), projectile counter + ivec4 inp3; // aim x, aim y (window cells), fire, player projectile type + vec4 outp; // player: x, y, hp, facing (written by the shader, read by the host) + vec4 outp2; // player: levitation energy 0..1, fire cooldown, 0, 0 + vec4 view; // host: camera shift of the last tick (cells), interpolation alpha, gravity scale + vec4 hand0; // host: held wand: aim point (world cells), enabled, mirror + ivec4 hand1; // host: held wand: texel offset, w, h + vec4 hand2; // host: held wand: pivot + ivec4 burnN; // rigid_burn.comp: x the burnt pixels; y: the player's respawns, z: saved by SAVING_GRACE, w: an extra life used (the host reads and clears them) +} ctrl; + +const int ST_HOVER = 128; // an item that hovers (a wand in the world): no physics, a small bob (play_hover_animation) +const int ST_SWIMMER = 256; // the immersion probe at ex.w (the integer buoyancy_check_offset_y) is tested +const int ST_GROUNDED = 1, ST_CANJUMP = 2, ST_CANFLY = 4, ST_DEAD = 8, ST_LIQUID = 16, ST_RIGID = 32; // ST_RIGID: moved by the rigid-body engine (rigid_*.comp), only damage is handled here + +// rigid bodies are solid for creatures and the player: rigid_raster.comp marks the cells their masks cover (read here with a one-tick delay) +layout(std430, set = 0, binding = 40) readonly buffer BodyGrid { uint bodyGrid[]; }; +layout(std430, set = 0, binding = 46) readonly buffer GlowTab { uint gtab[]; }; // per material: gfx_glow in the low 16 bits, the stain statuses it gives in bits 16-21 (WET, OILED, BLOODY, ALCOHOLIC, SLIMY, RADIOACTIVE) +bool body_solid(ivec2 w) { + ivec2 p = w - ivec2(pc.view_x, pc.view_y); + if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false; + const uint b = bodyGrid[p.y * pc.gw + p.x]; + return b != 0u && (b & 0x80000000u) == 0u; // (bit 31: a body that is no platform - the lanterns and their chains do not block, main.cpp isPlatform) +} +// 00bb6820: the leak emitter's offset is the local hit point, clamped to the HitboxComponent (a corpse part: +-12 here); without one (ex7.w, a prop) as it is, zero beyond 256 px +vec2 leak_offset(Ent e, vec2 rel) { return e.ex7.w > 0.5 ? (dot(rel, rel) > 65536.0 ? vec2(0.0) : rel) : clamp(rel, vec2(-12.0), vec2(12.0)); } +bool solid_cell_at(ivec2 w) { + ivec2 p = w - ivec2(pc.view_x, pc.view_y); + if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false; // outside the window there is no ground (such entities are frozen) + uint m = cell_mat(cells[p.y * pc.gw + p.x]); + if (m != 0u && (mflags(m) & (F_STATIC | F_SAND)) != 0u) return true; + return body_solid(w); +} +// The character's cell test 0063ef70: a platform cell counts only when one of its four neighbours (below, left, right, above) is one too - a lone pixel (a crumb of +// rock, a single body pixel) does not stop the player. (Its other exception, the bodies held by telekinesis - 00c2dfc0 stamps their b2Body user data +0x48 with +// the frame - does not apply: TelekinesisComponent is not done.) +bool solid_at(ivec2 w) { + if (!solid_cell_at(w)) return false; + return solid_cell_at(w + ivec2(0, 1)) || solid_cell_at(w + ivec2(-1, 0)) || solid_cell_at(w + ivec2(1, 0)) || solid_cell_at(w + ivec2(0, -1)); +} +// What a creature or the player takes by damage type (bit 1 projectile, 2 explosion, 4 fire, 8 melee): the multipliers of the game effects, ex9 = projectile, explosion, fire, melee +float taken_mult(vec4 m, int types) { + if ((types & 2) != 0) return m.y; + if ((types & 8) != 0) return m.w; + if ((types & 4) != 0) return m.z; + return m.x; +} +// The hit flash (00ab5b00, called by TakeDamage 00ab9a90 for damage > 0 that is not fire alone): every sprite of the entity is drawn with +// sprite_damage_highlight (shader 6: white, the sprite's alpha) or, for a critical hit, sprite_damage_critical_hit_highlight (7: red) in its third shader slot; +// at most once per 5 frames (frame - DamageModel +0x238 > 4), and the DamageModelSystem update (00ab6d00) clears the slot at +0x238 + 3: 3 frames of flash. +// ex19.w counts it down: 5 at the hit, drawn while >= 3 (sprites.comp), a new flash at 0; + 8 for a critical hit. (Worm segments keep their root slot there.) +void hit_flash(uint id, bool hit, bool crit) { + const int c = int(ents[id].ex19.w + 0.5); + int n = max((c & 7) - 1, 0); + bool red = (c & 8) != 0; + if (hit && n == 0) { n = 5; red = crit; } + ents[id].ex19.w = n == 0 ? 0.0 : float(n + (red ? 8 : 0)); +} +// What an entity deals: the DAMAGE_MULTIPLIER effect (the perk glass cannon) etc.; the host stores the factor in ex7.y (0 = none) +float dealt_mult(Ent s) { return s.ex7.y > 0.0 ? s.ex7.y : 1.0; } + +bool liquid_at(ivec2 w) { + ivec2 p = w - ivec2(pc.view_x, pc.view_y); + if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false; + uint m = cell_mat(cells[p.y * pc.gw + p.x]); + if (m == 0u) return false; + uint f = mflags(m); + return (f & F_LIQCELL) != 0u && (f & F_POWDER) == 0u; // liquid and not powder (a static liquid counts too) +} +bool line_clear(vec2 a, vec2 b) { // no solid cell between two points (3-cell steps) + vec2 d = b - a; + int n = int(length(d) / 3.0); + for (int i = 1; i < n; ++i) if (solid_cell_at(ivec2(floor(a + d * (float(i) / float(n)))))) return false; + return true; +} +bool box_hits(ivec2 o, vec4 box) { + ivec2 lo = o + ivec2(int(box.x), int(box.z)), hi = o + ivec2(int(box.y), int(box.w)); // hi exclusive + for (int y = lo.y; y < hi.y; ++y) + for (int x = lo.x; x < hi.x; ++x) + if (solid_at(ivec2(x, y))) return true; + return false; +} + +// Blood: real particles of the creature's blood material that fly out along the hit and become cells where they land. +void blood_burst(uint seed, vec2 pos, vec2 dirv, int count, float speed, int matp1, uint colour) { + if (matp1 <= 0) return; + for (int k = 0; k < count && k < 64; ++k) { + float r1 = float(hash3(seed, uint(k), 1u) & 0xFFFFu) / 65536.0, r2 = float(hash3(seed, uint(k), 2u) & 0xFFFFu) / 65536.0; + vec2 v = dirv * (0.4 + r1 * 1.2) * speed + vec2((r2 - 0.5) * 1.4 * speed, -0.3 - r1 * 0.9 * speed); + emit_material_particle(pos + vec2(0.0, -3.0), v, 70.0 + r2 * 40.0, PART_GRAV, colour, matp1 - 1); + } +} + +// Liquid of a living hit (00ab9a90): the count does not depend on the damage, N = trunc((u * (40 - 20) + 20) * blood_multiplier + 0.5) (the incoming multiplier and the world +// multiplier are 1); the first droplet leaves along the hit, each next one is turned by a random angle (-0.6..0.6 rad) from the previous one, a speed of 0.5..1.25 times +// the base, at the hit point +-1.5. The velocity is 20 x s x the packet impulse (px/s): a projectile's velocity x ragdoll_force_multiplier (00c091f0), an explosion's radial force (00657960). +void hit_blood(uint seed, vec2 hit, vec2 dirv, float bloodMult, int matp1, uint colour, float speedMul) { + if (matp1 <= 0 || bloodMult <= 0.0) return; + const float u0 = float(hash3(seed, 0u, 11u) & 0x7FFFu) / 32767.0; + int n = int((u0 * 20.0 + 20.0) * bloodMult + 0.5); + vec2 v = dirv; + for (int k = 0; k < n && k < 64; ++k) { + float u1 = float(hash3(seed, uint(k), 12u) & 0x7FFFu) / 32767.0, u2 = float(hash3(seed, uint(k), 13u) & 0x7FFFu) / 32767.0; + float u3 = float(hash3(seed, uint(k), 14u) & 0x7FFFu) / 32767.0, u4 = float(hash3(seed, uint(k), 15u) & 0x7FFFu) / 32767.0; + float sp = 0.5 + 0.75 * u1, a = -0.6 + 1.2 * u2; + v = vec2(v.x * cos(a) - v.y * sin(a), v.x * sin(a) + v.y * cos(a)); + emit_material_particle(vec2(hit.x - 1.5 + 3.0 * u4, hit.y - 1.5 + 3.0 * u3), v * (20.0 * sp / 60.0) * speedMul, 70.0 + u2 * 40.0, PART_GRAV, colour, matp1 - 1); // (20 s v px/s, v the packet impulse) + } +} + +layout(std430, set = 0, binding = 27) buffer Edits { ivec4 ehead; vec4 ee[512]; }; // cell edits (see edits.comp): flames falling from a burning creature +bool extinguishes_at(ivec2 w) { // water and the like put a burning creature out + ivec2 p = w - ivec2(pc.view_x, pc.view_y); + if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false; + uint m = cell_mat(cells[p.y * pc.gw + p.x]); + return m != 0u && ((mats[3u * m + 2u].z >> 18) & 1u) != 0u; +} + +// script_damage_about_to_be_received (the host sets 65536 in ex8.x when the entity or a child has one): the hp a hit takes goes to a running total instead +// (ex14.w, milli-hp, written only by the entity's own thread); the host runs the scripts on what was added and changes the hp by their result +float take_damage(uint id, float v) { + if ((ents[id].ex8.x & 65536) != 0) { ents[id].ex14.w += v * 1000.0; return 0.0; } + return v; +} + +void main() { + uint id = gl_GlobalInvocationID.x; + if (id >= uint(ctrl.inp.w)) return; + Ent e = ents[id]; + int kind = e.st.x; + if (kind == 0 || (e.st.w & ST_DEAD) != 0) return; + const bool is_prop = kind == 4; + if (is_prop && e.ex7.y > 0.0) { // a corpse part with a liquid inventory: the particle emitter of the ragdoll piece (docs/BLOOD_EMITTER_LIFECYCLE.md) + // ex = (x_vel_min, x_vel_max, y_vel_min, y_vel_max) in px/s (local), ex2 = (next emit frame, mode 0 off / 1 death leakage (1-3 per emission) / 2 damage leakage (1), 0, mass), + // ex7 = (-, reserve, material + 1, -). A damage event (min_damage_to_leak 0.09, leak_on_damage_percent 1 compared with u in [0,1)) rewrites the velocity ranges from the clamped + // local hit offset (00bb6820) and switches to one droplet per emission; the interval is 1-2 frames either way, the schedule advances before the spawn, a request larger than the + // reserve emits nothing, the reserve only falls by the droplets that were created, and a blocked spawn cell ends the burst (emit_only_if_there_is_space). + float reserve = e.ex7.y; + vec4 vr = e.ex; + float nextf = e.ex2.x, mode = e.ex2.y; + const uint fr = uint(ctrl.inp2.x); + const int dmg = atomicExchange(ents[id].ex8.z, 0); + if (dmg > 90) { // 00bb6820: strictly more than min_damage_to_leak (0.09) + const vec2 rel = vec2(float(ents[id].ex8.x), float(ents[id].ex8.y)) * 0.001; + const uint sd = hash3(id, fr, 53u); + float u[8]; + for (int k = 0; k < 8; ++k) u[k] = float(hash3(sd, uint(k), 54u) & 0x7FFFu) / 32767.0; + if (u[7] < 1.0) { // u < leak_on_damage_percent (1): fails only for the draw 32767 / 32767 + vec2 d = leak_offset(e, rel); + const float dl = length(d); + d = dl > 1e-4 ? d / dl : vec2(0.0); + const float dx = d.x * (0.85 + 0.29999995 * u[0]), dy = d.y * (0.85 + 0.29999995 * u[1]); + const float speed = 100.0 * (0.7 + (1.1 - 0.7) * u[2]); + const float vx = speed * dx, vy = speed * dy; + vr = vec4(vx * (0.6 + 0.39999998 * u[3]), vx * (1.0 + 0.39999998 * u[4]), vy * (0.6 + 0.39999998 * u[5]), vy * (1.0 + 0.39999998 * u[6])); + mode = 2.0; + } + } + if (mode > 0.5 && float(fr) >= nextf && e.ex7.z > 0.0) { + const uint seed = hash3(id, fr, 51u); + nextf = float(fr) + ((e.ex2.z > 0.5 && mode < 1.5) ? 3.0 + float(hash3(seed, 3u, 58u) % 3u) : 1.0 + float(seed & 1u)); // the next emission is scheduled before anything is spawned (3-5 frames when the cosmetic spray exists and no damage has rewritten the emitter) + const int cnt = mode > 1.5 ? 1 : 1 + int(hash3(seed, 7u, 56u) % 3u); + if (reserve >= float(cnt)) { + const float ca = cos(e.a.z), sa = sin(e.a.z); + int made = 0; + for (int k = 0; k < cnt; ++k) { + const uint r = hash3(seed, uint(k), 52u); + const float ux = float(r & 0x7FFFu) / 32767.0, uy = float((r >> 15) & 0x7FFFu) / 32767.0, px = float(hash3(r, 1u, 57u) & 0xFFFFu) / 65536.0, py = float(hash3(r, 2u, 57u) & 0xFFFFu) / 65536.0; + const vec2 lo = leak_offset(e, vec2(float(ents[id].ex8.x), float(ents[id].ex8.y)) * 0.001); + const vec2 off = mode > 1.5 ? vec2(ca * lo.x - sa * lo.y, sa * lo.x + ca * lo.y) : vec2(0.0); // the damage event's offset (the hit point), turned with the body + const vec2 at = e.a.xy + off + vec2(-1.0 + 2.0 * px, -1.0 + 2.0 * py); // rect (-1,-1) .. (1,1) + if (solid_cell_at(ivec2(floor(at)))) break; + vec2 v = vec2(mix(vr.x, vr.y, ux), mix(vr.z, vr.w, uy)) * (1.0 / 60.0); // px/s -> cells/frame + v = vec2(ca * v.x - sa * v.y, sa * v.x + ca * v.y); // the entity's rotation + if (emit_material_particle(at, v, 70.0 + float((r >> 24) & 0x3Fu), PART_GRAV, uint(e.ex3.y), int(e.ex7.z) - 1)) ++made; // only the particles that were created cost reserve (00bc75e0) + } + reserve -= float(made); + } + } + if (e.ex2.z > 0.5 && e.ex7.z > 0.0 && float(fr) >= e.ex6.z) { // blood_spray_create_some_cosmetic: a second emitter on the same reserve, 1-3 cosmetic droplets (no cells) every 1-2 frames + const uint seed2 = hash3(id, fr, 59u); + ents[id].ex6.z = float(fr) + 1.0 + float(seed2 & 1u); + const int cnt2 = 1 + int(hash3(seed2, 7u, 56u) % 3u); + if (reserve >= float(cnt2)) { + float u2[8]; + for (int k = 0; k < 8; ++k) u2[k] = float(hash3(seed2, uint(k), 60u) & 0x7FFFu) / 32767.0; + const vec2 d = e.ex6.xy * vec2(0.85 + 0.29999995 * u2[0], 0.85 + 0.29999995 * u2[1]); // the same formula as the death emitter, other draws + const float base = (360.0 + 0.7 * 1200.0 * u2[2]) * e.ex2.w, speed = 0.7 * base + (1.1 * base - 0.7 * base) * u2[3]; + const vec2 v0 = speed * d; + const vec4 cr = vec4(v0.x * (0.8 + 0.19999999 * u2[4]), v0.x * (1.0 + 0.20000005 * u2[5]), v0.y * (0.8 + 0.19999999 * u2[6]), v0.y * (1.0 + 0.20000005 * u2[7])); + const float ca = cos(e.a.z), sa = sin(e.a.z); + int made = 0; + for (int k = 0; k < cnt2; ++k) { + const uint r = hash3(seed2, uint(k), 61u); + const float ux = float(r & 0x7FFFu) / 32767.0, uy = float((r >> 15) & 0x7FFFu) / 32767.0, px = float(hash3(r, 1u, 62u) & 0xFFFFu) / 65536.0, py = float(hash3(r, 2u, 62u) & 0xFFFFu) / 65536.0; + const vec2 at = e.a.xy + vec2(-1.0 + 2.0 * px, -1.0 + 2.0 * py); + if (solid_cell_at(ivec2(floor(at)))) break; + vec2 v = vec2(mix(cr.x, cr.y, ux), mix(cr.z, cr.w, uy)) * (1.0 / 60.0); + v = vec2(ca * v.x - sa * v.y, sa * v.x + ca * v.y); + if (emit_particle_hit(at, v, 25.0 + float((r >> 24) & 0x1Fu), 0.1, uint(e.ex3.y), int(e.ex7.z) - 1) != 0xFFFFFFFFu) ++made; // a droplet without a cell: it fades in the air and stains where it lands (it owns no cell: 00716ce0) + } + reserve -= float(made); + } + } + ents[id].ex = vr; ents[id].ex2.x = nextf; ents[id].ex2.y = mode; ents[id].ex7.y = max(reserve, 0.0); + } + + vec2 pos = e.a.xy, vel = e.a.zw; + ivec2 ip = ivec2(floor(pos)); + // frozen unless the collision box (plus margin) is inside the simulated window + ivec2 wp = ip - ivec2(pc.view_x, pc.view_y); + if (wp.x < 3 || wp.y < 3 || wp.x > pc.gw - 3 || wp.y > pc.gh - 3) return; + + uint rnd = hash3(id, uint(ctrl.inp2.x), 911u); + int flags = e.st.w; + int types = 0; // damage types taken this tick (creatures) + vec2 hitv = vec2(0.0); // direction of the hit + float dmgTick = 0.0; // damage taken this tick + int dir = e.st.y; + int timer = e.st.z; + float hp = e.hp.x, cooldown = max(e.hp.y - 1.0, 0.0); + int energy = e.x.y; + // pushed out of solid cells (sand piles landing on it, an explosion refilling, a wall appearing): find the nearest free spot above or beside + const bool rigidBody = is_prop && (flags & ST_RIGID) != 0; // moved by the rigid-body engine: its own mask must not count as an obstacle + if (kind != 0 && !rigidBody && box_hits(ip, e.box)) { + bool freed = false; + for (int k = 1; k <= 12 && !freed; ++k) { + const ivec2 cand[5] = ivec2[5](ivec2(0, -k), ivec2(-k, -k), ivec2(k, -k), ivec2(-k, 0), ivec2(k, 0)); + for (int c = 0; c < 5 && !freed; ++c) + if (!box_hits(ip + cand[c], e.box)) { ip += cand[c]; pos = vec2(float(ip.x) + fract(pos.x), float(ip.y)); vel.y = min(vel.y, 0.0); freed = true; } + } + } + bool grounded = !rigidBody && box_hits(ip + ivec2(0, 1), e.box); + const bool wasGrounded = grounded; + bool liquid = liquid_at(ip); + if (kind == 1 || kind == 2 || kind == 3) { // stains: standing in water, oil, blood... leaves its status on the creature for a while (ex8.z: bits | frame << 8) + uint sm = 0u; + for (int sy = 0; sy < 2; ++sy) { + const ivec2 sp = ip + ivec2(0, -4 * sy) - ivec2(pc.view_x, pc.view_y); + if (sp.x >= 0 && sp.y >= 0 && sp.x < pc.gw && sp.y < pc.gh) sm |= (gtab[cell_mat(cells[sp.y * pc.gw + sp.x])] >> 16) & 63u; + } + if (sm != 0u) { + const uint old = ((uint(ents[id].ex8.z) >> 8) + 900u > uint(pc.frame & 0xFFFFFF)) ? (uint(ents[id].ex8.z) & 63u) : 0u; + ents[id].ex8.z = int((sm | old) | (uint(pc.frame & 0xFFFFFF) << 8)); + } + } + // swimming (CharacterPlatformingComponent swim_*): with the buoyancy check point (CharacterDataComponent buoyancy_check_offset_y) under liquid a walker or + // the player is nearly weightless (swim_idle_buoyancy_coeff), every velocity is damped (swim_drag, swim_extra_horizontal_drag) and the up key swims up + const bool swimming = (kind == 1 || kind == 3) && (e.st.w & ST_SWIMMER) != 0 && liquid_at(ivec2(pos) + ivec2(0, int(e.ex.w))); // float coordinates truncate toward zero (00a7ea90) + float walk = e.mv.x, vmax = e.mv.y; + if (e.ex15.z > 0.5 && (int(e.ex15.w) & 32) != 0) walk *= 2.0; // the run button of a creature the host runs (00a25d50: it has a prey or a threat; 00a7ea90 doubles the speed) + float target = 0.0; + bool want_jump = false, thrust = false; + // a flyer's ControlsComponent: mButtonDownFly and mFlyingTargetY (ex18.z), set by the path steer (00a29b20 -> 00a8d340) and kept between them + bool flyOn = kind == 2; + float flyTY = e.ex18.w < -1.0e8 ? pos.y : e.ex18.z; + + vec4 pl = ents[0].a; // the player (if there is one) + bool has_player = ctrl.inp2.y != 0; + bool player_alive = has_player && (ents[0].st.w & ST_DEAD) == 0; + const bool frozen = (e.ex8.x & 1) != 0; // a game effect with disable_movement (FROZEN, PARALYSIS...): no walking, jumping, attacking + + if (kind == 3) { // ---- the player ---- + target = frozen ? 0.0 : float(ctrl.inp.x) * walk; + vec2 aim_w = vec2(ctrl.inp3.xy) + vec2(float(pc.view_x), float(pc.view_y)); // aim point in world cells + if (ctrl.inp3.x >= 0) dir = aim_w.x >= pos.x ? 1 : -1; else if (ctrl.inp.x != 0) dir = ctrl.inp.x; // face the mouse + cooldown = max(cooldown - 0.0, 0.0); + if (ctrl.inp3.z != 0 && !frozen && cooldown <= 0.0 && ctrl.inp3.w >= 0) { // fire the spark bolt toward the mouse + PType t = ptypes[ctrl.inp3.w]; + vec2 from = pos + vec2(0.0, -3.0); + vec2 dv = aim_w - from; + vec2 dirv = length(dv) > 0.5 ? normalize(dv) : vec2(float(dir), 0.0); + uint r2 = hash3(id, uint(ctrl.inp2.x), 77u); + float ang = (float(r2 & 255u) / 255.0 - 0.5) * 0.04; + dirv = vec2(dirv.x * cos(ang) - dirv.y * sin(ang), dirv.x * sin(ang) + dirv.y * cos(ang)); + uint slot = uint(atomicAdd(ctrl.inp2.w, 1)) % 512u; + projs[slot].a = vec4(from, dirv * t.a.y); + projs[slot].b = vec4(t.a.z, t.a.w, t.b.x, t.b.y); + projs[slot].c = vec4(1.0, 0.0, 0.0, 0.0); + cooldown = 9.0; + } + want_jump = ctrl.inp.y != 0 && !frozen; + { // the jetpack (CharacterDataComponent fly_time_max, fly_recharge_spd, fly_recharge_spd_ground): flying burns one frame of fuel per frame, on the ground it refills at 6 seconds per second, + // in the air the fuel trickles back at fly_recharge_spd as soon as the key is up (units of 1/64 frame) + const float emax = (e.ex2.x > 0.0 ? e.ex2.x : 180.0) * 64.0; + float en = float(energy); + thrust = want_jump && !grounded && en >= 64.0; // the jetpack works in liquids too (the swim correction comes after it) + if (grounded) en = min(emax, en + 6.0 * 64.0); + else if (thrust) en -= 64.0; + else en = min(emax, en + (e.ex2.y > 0.0 ? e.ex2.y : 0.4) * 64.0); + energy = int(en); + } + // damage taken from enemies this frame + int dmg = atomicExchange(ents[id].x.x, 0); + const float hpBeforeHit = hp; + { const int pty = atomicExchange(ents[id].ex3.z, 0); hp -= take_damage(id, float(dmg) * 0.001); if (pty != 0) ents[id].ex3.w = (pty & 0xFF) | ((ctrl.inp2.x & 0xFFFFF) << 8); hit_flash(id, dmg > 0, (pty & 0x10000) != 0); } // (the accumulator already holds the multiplied damage: every source applies the victim's multiplier of its type) (ex3.w: the types of this tick | tick << 8, for the damage sounds) + // TakeDamage (00ab9a90) on a lethal hit: SAVING_GRACE (effect 0x2a) leaves 1 / 25 hp (1 HP shown) when there was more, else an unspent RESPAWN (0x2d, extra life: ex8.x + // bit 131072, the host clears it) sets 100 / 25 hp, at most the maximum; the host is told (ctrl burnN.z / w) for the message and the PROTECTION_ALL of 60 / 30 frames + if (hp <= 0.0 && (e.ex8.x & 16) != 0 && hpBeforeHit > 0.04) { hp = 0.04; atomicAdd(ctrl.burnN.z, 1); } + else if (hp <= 0.0 && (ents[id].ex8.x & 131072) != 0) { hp = min(4.0, e.aux.x > 0 ? float(e.aux.x) * 0.001 : 4.0); atomicAnd(ents[id].ex8.x, ~131072); atomicAdd(ctrl.burnN.w, 1); } + if (hp <= 0.0 && (e.ex8.x & 32768) == 0) { // respawn at the spawn point (a polymorphed body is not: the host ends the polymorph, 32768) + pos = vec2(float(e.x.z), float(e.x.w)); vel = vec2(0.0); ip = ivec2(floor(pos)); + hp = e.aux.x > 0 ? float(e.aux.x) * 0.001 : 4.0; + atomicAdd(ctrl.burnN.y, 1); + } + } else if (kind == 1 || kind == 2) { // ---- creatures ---- + // the target of the creature: the player; a charmed creature (game effect CHARM) fights the nearest creature of another herd instead + vec4 tp = pl; bool tAlive = player_alive; int tgt = 0; + const bool berserk = (e.ex8.x & 4) != 0 && (e.ex8.x & 2) == 0; // BERSERK (00a28e10): it attacks every mortal, the player and its own herd included, unless berserk_dont_attack_friends (bit 3) + if ((e.ex8.x & 2) != 0 || berserk) { + float best = 1.0e9; + tgt = -1; + for (int k = berserk ? 0 : 1; k < ctrl.inp.w; ++k) { + if (k == int(id) || (ents[k].st.w & ST_DEAD) != 0 || (ents[k].ex8.x & 8192) != 0) continue; // (an invisible one is not seen) + if (k == 0 ? (!berserk || ents[0].st.x != 3) : (ents[k].st.x != 1 && ents[k].st.x != 2)) continue; + if (berserk ? ((e.ex8.x & 8) != 0 && ents[k].ex8.y == e.ex8.y) : ents[k].ex8.y == e.ex8.y) continue; + const float dk = distance(ents[k].a.xy, pos); + if (dk < best && abs(ents[k].a.x - pos.x) < e.ai.x && abs(ents[k].a.y - pos.y) < e.ai.y) { best = dk; tgt = k; } + } + tAlive = tgt >= 0; + if (tgt >= 0) tp = ents[tgt].a; else tgt = 0; + } + float dx = tp.x - pos.x, dy = tp.y - pos.y; + // sees the player: within the detection range and, for creatures with a limited field of view (creature_detection_angular_range_deg), inside the cone in front of them + vec2 toP = vec2(tp.x, tp.y - 4.0) - vec2(pos.x, pos.y + e.ex.y); + bool inCone = e.ex.x <= -0.999 || (length(toP) > 1.0 && dot(normalize(toP), vec2(float(dir), 0.0)) >= e.ex.x); + bool sees = tAlive && !frozen && (ents[tgt].ex8.x & 8192) == 0 && abs(dx) < e.ai.x && abs(dy) < e.ai.y && inCone; + types = atomicExchange(ents[id].ex3.z, 0); // damage types taken since the last tick + timer -= 1; + if (timer <= 0) { // pick a new idle/wander action + uint r = rnd; + if ((r % 3u) == 0u) timer = 20 + int((r >> 4) % 50u); + else { dir = ((r >> 8) & 1u) == 0u ? -1 : 1; timer = 60 + int((r >> 4) % 120u); } + e.x.y = (r % 3u) == 0u ? 1 : 0; // 1 = idling for this action + } + bool idle = e.x.y == 1; + if (sees) { dir = dx >= 0.0 ? 1 : -1; idle = false; } + if (e.ex15.z > 0.5) { + // the host's AnimalAI (src/ai, AnimalAISystem 00a22540 + PathFindingSystem 00bca030): mode 1 heads for the next node of its path (a flyer in both axes), mode 2 stands; + // the attacks below stay the GPU's + target = 0.0; + if ((int(e.ex15.w) & 2) != 0 || (int(e.ex15.w) & 8) != 0) target = vel.x; // (from the launch on, also on the ground: a lob down off a ledge starts into the floor and slides off the edge at its speed) // a jump of the path: no control in the air (a dash: for its whole flight time) + else if (e.ex15.z < 1.5) { + const float sdx = e.ex15.x - pos.x; + if (abs(sdx) > 1.0) { dir = sdx > 0.0 ? 1 : -1; target = float(dir) * walk; } + // a flyer (00a29b20): the fly key with the node's y as mFlyingTargetY; a can_walk flyer with ground within 10 px below and the node not above it lets go + // of the key and walks (the host's flag 16) + if (kind == 2) { flyOn = (int(e.ex15.w) & 16) == 0; if (flyOn) flyTY = e.ex15.y; } + // (a fish swims up and down by its buoyancy: the swim correction below) + } + } else { + if (!idle) target = float(dir) * walk; + if (kind == 1) { + // do not walk off ledges while wandering; turn around at walls (handled below), jump after the player + if (!sees && grounded && !idle) { + ivec2 ahead = ip + ivec2(dir * 5, 0); + if (!box_hits(ahead + ivec2(0, 4), e.box) && !box_hits(ahead + ivec2(0, 8), e.box)) { dir = -dir; target = float(dir) * walk; } + } + } else if (sees) flyTY = tp.y - 8.0; // flyer without the host's AI: the fly key, toward the player's height when it sees one (else it holds the last height) + } + { // hits: projectiles and explosions (damage accumulator), the blood sprays out along the hit + int dmg = atomicExchange(ents[id].x.x, 0); + hp -= take_damage(id, float(dmg) * 0.001); // (already multiplied at the source by the victim's multiplier of the damage type) + dmgTick += float(dmg) * 0.001; + hit_flash(id, dmg > 0, (types & 0x10000) != 0); + if (dmg > 0) { + hitv = vec2(float(atomicExchange(ents[id].aux.x, 0)), float(atomicExchange(ents[id].aux.y, 0))) * 0.001; + hit_blood(hash3(id, uint(ctrl.inp2.x), 31u), pos + vec2(0.0, 0.5 * (e.box.z + e.box.w)), hitv, e.ex7.x, e.ex3.x, uint(e.ex3.y), 1.0); // (hitv: the packet impulse, px/s) + } + } + // melee attack + if (tAlive && !frozen && (ents[tgt].ex8.x & 8192) == 0 && e.ex.z > 0.5 && abs(dx) < e.ai.z && abs(dy) < e.ai.z + 4.0 && cooldown <= 0.0) { + atomicAdd(ents[tgt].x.x, int(e.ai.w * dealt_mult(e) * ents[tgt].ex9.w * 1000.0)); // the target's melee multiplier + if (e.ai.w > 0.0 && e.ai.w * dealt_mult(e) * ents[tgt].ex9.w <= 0.0 && ents[tgt].st.x >= 1 && ents[tgt].st.x <= 3) ents[tgt].ex11.w = float(pc.frame); // (a blocked hit: BLOCK on the host) + ents[id].rg2.w = 1 | ((ctrl.inp2.x & 0xFFFFFF) << 4); + atomicOr(ents[tgt].ex3.z, 8); + ents[tgt].ex8.w = int(id + 1) | ((pc.frame & 0xFFFFF) << 12); // the target was hit by this creature + cooldown = 50.0; + } + // ranged attack: cast the creature's projectile (a node of the GPU projectile system, team 2) at the player when in range with a clear line + int rcd = max(e.x.z - 1, 0); + if (e.rg.x >= 0 && tAlive && !frozen) { + vec2 from = pos + vec2(0.0, float(e.rg2.z)), to = tp.xy + vec2(0.0, -4.0); + float dist = length(to - from); + if (sees && rcd == 0 && dist >= float(e.rg.z) && dist <= float(e.rg.w) && line_clear(from, to)) { + int count = e.rg2.x + int(rnd % uint(e.rg2.y - e.rg2.x + 1)); + float speed = max(e.ex2.x, 0.1), tt = dist / speed; + vec2 aimv = (to - from) / tt + vec2(0.0, -0.5 * e.ex2.y * tt); // launch velocity that compensates the gravity over the flight time + for (int k = 0; k < count; ++k) { + uint r2 = hash3(id, uint(ctrl.inp2.x), uint(k) + 5u); + float ang = (float((r2 >> 8) & 255u) / 255.0 - 0.5) * (0.08 + 0.05 * float(count - 1)); + vec2 d0 = normalize(aimv); + vec2 dirv = vec2(d0.x * cos(ang) - d0.y * sin(ang), d0.x * sin(ang) + d0.y * cos(ang)); + int list = pc.frame & 1; + int slot = list == 0 ? atomicAdd(hhead.x, 1) : atomicAdd(hhead.y, 1); + if (slot < 256) { + hcast[(list * 256 + slot) * 2] = vec4(from, dirv); + hcast[(list * 256 + slot) * 2 + 1] = vec4(float(e.rg.x), float(2 + (e.ex8.y << 3)), float(r2 & 0xFFFFFFu), float(1 + (id + 1) * 2)); // r1.w: muzzle flash flag, the shooter's slot + 1 in the higher bits + } + } + rcd = e.rg.y; + ents[id].rg2.w = 2 | ((ctrl.inp2.x & 0xFFFFFF) << 4); // the attack of the tick (1 melee, 2 ranged | tick << 4): the host plays its sprite events (attack sounds) + } + } + e.x.z = rcd; + // (the player's kick on creatures is the host's: KickSystem 00bf2040, 18 frames after the press - its damage goes through the damage accumulator) + if (hp <= 0.0) flags |= ST_DEAD; + } + + if (frozen) { target = 0.0; want_jump = false; thrust = false; } + // soft separation: creatures and the player do not stack on top of each other + // (characters do not push each other: the engine has no creature-creature collision - CharacterCollisionComponent only collides with the world) + // ---- damage: projectiles and explosions (the atomic accumulator), contact with harmful materials, fire ---- + if (is_prop && e.hp.x < 1.0e8) { + int dmg = atomicExchange(ents[id].x.x, 0); + hp -= float(dmg) * 0.001; // (the material damage of 00ab6010 / 00ab7410 comes through this accumulator from the host) + hit_flash(id, dmg > 0, (atomicExchange(ents[id].ex3.z, 0) & 0x10000) != 0); + if (hp <= 0.0) flags |= ST_DEAD; + } + if (kind == 1 || kind == 3 || kind == 2) { // fire hurts: cells of fire sources or burning cells overlapping the box + int burn = 0; + ivec2 lo = ip + ivec2(int(e.box.x), int(e.box.z)), hi = ip + ivec2(int(e.box.y), int(e.box.w)); + for (int y = lo.y; y < hi.y; ++y) + for (int x = lo.x; x < hi.x; ++x) { + ivec2 q = ivec2(x, y) - ivec2(pc.view_x, pc.view_y); + if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue; + uint cw = cells[q.y * pc.gw + q.x]; + uint cm = cell_mat(cw); + if (cm != 0u && (((mats[3u * cm + 2u].z >> 17) & 1u) != 0u || (cw & BURN_BIT) != 0u)) ++burn; + } + if (burn > 0) types |= 4; // (touching fire does no damage of its own: only the materials of materials_that_damage and the burning do) + // ---- burning (DamageModelSystem): ignition in the cell scan 00ab6010, the burning update 00ab7a70 ---- + // ignition (fire_probability_of_ignition != 0): the cells under the body that burn or are of an on_fire material (on_fire counts 2, not the creature's own sparks) + // add up; above 4, rand(0, 100) < the strength (fade) of such a cell sets mFireFramesLeft to max(left, seconds x 60): 12 s, 0 with a status that protects from + // fire (WET, OILED, BLOODY, SLIMY, RADIOACTIVE), 20 s ALCOHOLIC, 30 s OILED (in that order, the last one wins) + const uint stains = ((uint(e.ex8.z) >> 8) + 900u > uint(pc.frame & 0xFFFFFF)) ? (uint(e.ex8.z) & 63u) : 0u; + const bool fireProt = (stains & (1u | 2u | 4u | 16u | 32u)) != 0u; + float fireLeft = e.ex4.w, fireProb = e.ex16.z; + if (e.ex2.z > 0.0) { + int cnt = 0, fade = 0; + ivec2 lo2 = ip + ivec2(int(e.box.x), int(e.box.z)), hi2 = ip + ivec2(int(e.box.y), int(e.box.w)); + for (int y = lo2.y; y < hi2.y; ++y) + for (int x = lo2.x; x < hi2.x; ++x) { + ivec2 q = ivec2(x, y) - ivec2(pc.view_x, pc.view_y); + if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue; + uint cw = cells[q.y * pc.gw + q.x]; + uint cm = cell_mat(cw); + if (cm == 0u) continue; + const bool onFireMat = ((mats[3u * cm + 2u].z >> 17) & 1u) != 0u; + if (!onFireMat && (cw & BURN_BIT) == 0u) continue; + cnt += onFireMat ? 2 : 1; + int f = int(birth[q.y * pc.gw + q.x]) >> 24; // (the cell's fade, its strength: a fire cell without one has its temperature_of_fire) + if (f == 0 && (cw & BURN_BIT) == 0u) f = int((mats[3u * cm + 2u].w >> 8) & 255u); + fade = f; + } + if (cnt > 4 && int(hash3(id, uint(ctrl.inp2.x), 61u) % 101u) < fade) { + float secs = 12.0; + if (fireProt) secs = 0.0; + if ((stains & 8u) != 0u) secs = 20.0; + if ((stains & 2u) != 0u) secs = 30.0; + fireLeft = max(fireLeft, floor(secs * 60.0 + 0.5)); + if (e.ex4.w <= 0.0 && fireLeft > 0.0) fireProb = 50.0; // (mIsOnFire changes: mFireProbability 50) + } + } + if (fireLeft > 0.0) { + // 00ab7a70: fire_how_much_fire_generates times a random point of the hitbox: an empty cell gets a spark (the creature's own: it does not ignite it), a burning cell + // keeps the fire going (mFireProbability + 1, at most 100), each liquid cell met counts 3 against it when more than half of the tries met liquid; out when + // mFireProbability or mFireFramesLeft (-1 a frame, -2 with a fire-protecting status, +1 OILED) reaches 0. The damage while it burns: max(0.8, max hp) x 0.02 + // per second, twice that for anything but the player (fire_damage_amount only switches it on; the engine delivers the sum every 9 - 12 frames, here each frame) + const int nTry = max(int(e.ex2.z) - 1, 0); + int lit = 0, wet = 0; + const uint spark = mats[mats.length() - 2u].x; + for (int k = 0; k < nTry && k < 10; ++k) { + const float rx = float(hash3(id, uint(ctrl.inp2.x), 70u + uint(k)) & 0xFFFFu) / 65536.0, ry = float(hash3(id, uint(ctrl.inp2.x), 90u + uint(k)) & 0xFFFFu) / 65536.0; + const ivec2 w = ivec2(floor(pos + vec2(e.box.x + (e.box.y - e.box.x) * rx, e.box.z + (e.box.w - e.box.z) * ry))); + const ivec2 q = w - ivec2(pc.view_x, pc.view_y); + if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue; + const int qi = q.y * pc.gw + q.x; + uint cw = cells[qi]; + if (cell_mat(cw) == 0u && spark != 0u && atomicCompSwap(cells[qi], 0u, spark | (hash3(id, uint(k), 65u) & 0xFF0000u)) == 0u) { birth[qi] = birth_frame(); lit = 1; continue; } + cw = cells[qi]; + const uint cm = cell_mat(cw); + if (cm != 0u && ((cw & BURN_BIT) != 0u || (mflags(cm) & F_FIRE) != 0u)) lit = 1; + else if (cm != 0u && (mflags(cm) & F_LIQUID) != 0u) wet += 3; + } + fireProb = min(fireProb + float(lit), 100.0); + if (nTry / 2 < wet) fireProb -= float(wet); + fireLeft -= 1.0; // (the extra -1 of a fire-protecting status and the +1 of OILED: the host, from the status system's stain amounts) + if (lit == 1 && e.ex2.w != 0.0) { + const float maxhp = kind == 3 ? float(e.aux.x) * 0.001 : float(e.x.w) * 0.001; + hp -= take_damage(id, max(0.8, maxhp) * 0.02 / 60.0 * (kind == 3 ? 1.0 : 2.0) * e.ex9.z); + } + types |= 4; + if (fireProb < 1.0 || fireLeft < 1.0) { fireLeft = 0.0; fireProb = 0.0; } + } + ents[id].ex16.z = fireProb; + ents[id].ex4.w = fireLeft; + // air (00ab81b0, frames here): with the head in liquid air_in_lungs runs down 1 per frame; out of it (ex16.w counts the frames) it refills 1 per frame, 5 after + // 20 frames, up to air_in_lungs_max; at 0.2 s or less of air (also while it refills) air_lack_of_damage x dt is lost each frame + if (e.ex4.y > 0.0) { + float air = e.ex4.x, outF = e.ex16.w; + if (liquid_at(ip + ivec2(0, int(e.box.z)))) { outF = 0.0; air -= 1.0; } + else { outF = min(outF + 1.0, 1.0e6); if (air < e.ex4.y) air = min(e.ex4.y, air + (outF > 20.0 ? 5.0 : 1.0)); } + if (air <= 12.0) { air = max(air, 0.0); hp -= take_damage(id, e.ex4.z); types |= 32; } + ents[id].ex4.x = air; ents[id].ex16.w = outF; + } + if (kind != 3 && hp <= 0.0) flags |= ST_DEAD; + if ((types & (1 | 2 | 8)) != 0) ents[id].ex3.w = (types & 0xFF) | ((ctrl.inp2.x & 0xFFFFF) << 8); // the damage types of this tick | tick << 8 (the host's damage sounds, 00ab9a90) + } + if ((kind == 1 || kind == 2) && (flags & ST_DEAD) != 0 && (e.st.w & ST_DEAD) == 0) { // it dies now: blood bursts, the host spawns the corpse and runs the death scripts + float maxhp = float(e.x.w) * 0.001; + // KillMe (00abb7d0): the RAGDOLL_FX of the killing hit (bits 12-15; the packet's default NORMAL), a NORMAL projectile or explosion kill turns into BLOOD_SPRAY when + // rand % 100 + 1 <= 20 (01153024); BLOOD_EXPLOSION (2) makes the parts without joints: the gib (ragdoll_fx_forced is the host's) + int rfx = (types >> 12) & 15; + if (rfx == 0 && (types & 3) != 0) rfx = 1; + if (rfx == 1 && (types & 3) != 0 && int(hash3(id, uint(ctrl.inp2.x), 77u) % 100u) + 1 <= 20) rfx = 3; + bool gib = rfx == 2; + float hl = length(hitv); + vec2 hd = hl > 0.01 ? hitv / hl : vec2(0.0, -1.0); + int n = 14 + min(int(maxhp * 30.0), 24); + // no burst of its own: the lethal hit already made its liquid, the corpse parts carry the rest of the blood (finite reserves, see the part branch above) + ents[id].ex3.w = (types & 0xEF) | (gib ? 16 : 0) | ((ctrl.inp2.x & 0xFFFFF) << 8) | (rfx << 28); // (bits 28-31: the RAGDOLL_FX of the death) + ents[id].aux.z = int(hitv.x * 1000.0); ents[id].aux.w = int(hitv.y * 1000.0); + } + if (is_prop && (flags & ST_HOVER) != 0) { + const vec2 home = e.ex6.zw; + ents[id].a = vec4(home.x, home.y + 2.0 * sin(3.0 * float(pc.frame) / 60.0), 0.0, 0.0); // 00b83980 (ItemSystem, play_hover_animation): home y + 2 sin(3 t), t in seconds; the sprite turned by -pi/2 + return; + } + if (is_prop && (flags & ST_RIGID) != 0) { + ents[id].st = ivec4(kind, dir, timer, flags); + ents[id].hp = vec4(hp, cooldown, e.hp.z, e.hp.w); + return; + } + // ---- physics ---- + bool fly = kind == 2 && flyOn; + if (kind == 2) ents[id].ex18.zw = vec2(flyTY, fly ? (e.ex18.w < -1.0e8 ? flyTY : (e.ex18.y > 0.5 ? e.ex18.w + (flyTY - e.ex18.w) * 0.05 : flyTY)) : -1.0e9); + if (fly && (int(e.ex15.w) & 8) == 0) { // (a flyer's dash: the host's velocity for its flight) + // 00a7ea90, fly_model_player 0: mSmoothedFlyingTargetY eases to mFlyingTargetY by 0.05 a frame (fly_smooth_y; set at once when the key goes down); the vertical speed + // goes toward clamp(fly_speed_mult x (smoothed - y) - g dt, -fly_speed_max_up, fly_speed_max_down) by fly_speed_change_spd (snapped at the target), then the gravity + const float sY = ents[id].ex18.w; + const float tv = clamp(e.ex17.x * (sY - pos.y) / 60.0 - e.mv.w, -e.ex17.z, e.ex17.w); + const float nv = vel.y + e.ex17.y * (tv - vel.y); + vel.y = (sign(tv - nv) != sign(tv - vel.y)) ? tv : nv; + vel.y += e.mv.w; + } else if (!fly) { + vel.y += e.mv.w * ctrl.view.w; // gravity; the swim correction (docs/WATER_PHYSICS.md) follows the input staging below + if (kind == 4) vel *= 1.0 - e.ex5.x; // 00c370c0: air friction of the item's VelocityComponent (per frame) after gravity, then the liquid drag, then the terminal speed (vector) + else vel.y = min(vel.y, e.hp.z); + if (grounded && vel.y > 0.0) vel.y = 0.0; // resting on the ground: no gravity build-up (it made the sub-cell height creep 0.97 and snap back every 5 ticks) + } + // The knockback state of the platforming update (00a7ea90 lines 384-433): after a hit with knockback the horizontal velocity stays the kick's x (+0x148) and the controls are off; it ends + // after at least 3 frames when the body is on the ground (is_on_ground), stood in liquid at the buoyancy probe (x, y + buoyancy_check_offset_y) or collided horizontally + // the frame before (mCollidedHorizontally), and for the player after 60 frames (+0x14c, set to 60 by the kick 00ab9910; precision_jumping_max_duration_frames is + // -1 = no limit in every file) + vec4 kst = e.ex10; // the knockback state: x = the kick's horizontal velocity, y = frames since, z = 1 in the state (2 when it also hit a wall the frame before) + if ((kind == 1 || kind == 3) && (e.ex11.y != 0.0 || e.ex11.z != 0.0)) { // a queued kick (00a7ea90 lines 384-395): added to the staged velocity, CONFUSION inverts its x, it becomes the state's horizontal velocity + vec2 kk = e.ex11.yz; + if ((e.ex8.x & 128) != 0) kk.x = -kk.x; + vel += kk; + kst = vec4(kk.x, 0.0, 1.0, e.ex10.w); + ents[id].ex11.yz = vec2(0.0); + } + bool knocked = (kind == 1 || kind == 3) && kst.z > 0.5; + if (knocked) { + const float cnt = kst.y + 1.0; + if ((cnt >= 3.0 && (grounded || kst.z > 1.5 || liquid_at(ivec2(pos) + ivec2(0, int(e.ex.w))))) || (kind == 3 && cnt >= 60.0)) { ents[id].ex10.xyz = vec3(0.0); knocked = false; } + else { ents[id].ex10.xyz = vec3(kst.x, cnt, 1.0); vel.x = kst.x; } + } + if (!knocked) { + if (kind == 3 && thrust) { const float lf = (e.ex8.x & 16384) != 0 ? 1.75 : 1.0; vel.y = max(vel.y - 0.3 * lf, -1.45 * lf); } // levitation (FASTER_LEVITATION 0x4b: the two flying speeds of CharacterPlatformingComponent x 1.75) + else if (want_jump && grounded && kind != 2 && (kind == 3 || (flags & ST_CANJUMP) != 0)) vel.y = e.mv.z; // jump + if (kind == 2 && fly) target = sign(target) * e.ex18.x; // (flying: fly_velocity_x instead of run_velocity, 00a7ea90) + if (kind == 1 || kind == 2 || kind == 3) { // 00a7ea90: v += accel_x x (target - v) each frame (accel_x_air while flying in the air), the target when it would pass it + const float f = ((kind == 3 && thrust) || fly) && !grounded ? e.ex16.y : e.ex16.x; + const float nv = vel.x + f * (target - vel.x); + vel.x = (sign(target - nv) != sign(target - vel.x)) ? target : nv; + } else vel.x += clamp(target - vel.x, -0.12, 0.12); + // (the lob of a path jump keeps its speed from the launch until the host ends the jump: on the launch tick the creature still stands on the ground - capped + // there, a hop flew at walking speed and fell back where it started, and a lob down off a ledge never left it: zombies jumping in place) + if (!(((kind == 1) && (int(e.ex15.w) & 2) != 0) || ((kind == 1 || kind == 2) && (int(e.ex15.w) & 8) != 0))) vel.x = clamp(vel.x, -vmax, vmax); // (a dash keeps its speed for its flight, a flyer's too) // (the lob of a path jump keeps its speed) + } + // player splash (00a7ea90 / 00a809f0), queued BEFORE the swim correction below (the speed it tests and throws is the one before buoyancy and drag): the first swimming frame after more than 15 frames out of the liquid, faster than 40 px/s, falling, not on the ground: the 11x11 square + // around the position throws its liquid forward, v = s Rot(a) v_entity, s = 0.1 + 0.7 u1, a = 0.4 u2 - 0.2 (the original draws two numbers for every one of the 121 places) + if (kind == 3) { + float fns = e.ex6.y; + if (swimming) { + if (fns > 15.0 && length(vel) * 60.0 > 40.0 && vel.y > 0.0 && !grounded) + for (int dy = -5; dy <= 5; ++dy) for (int dx = -5; dx <= 5; ++dx) { + const uint rs = hash3(uint(id) * 131u + uint(dx + 5 + 11 * (dy + 5)), uint(pc.frame), 73u); + const float s1 = 0.1 + 0.7 * float(rs & 0x7FFFu) / 32767.0, a = 0.4 * float((rs >> 15) & 0x7FFFu) / 32767.0 - 0.2; + displace_cell(ivec2(pos) + ivec2(dx, dy), s1 * (mat2(cos(a), sin(a), -sin(a), cos(a)) * vel)); + } + fns = 0.0; + } else fns += 1.0; + ents[id].ex6.y = fns; + } + if (swimming && !fly) { + // 00a7ea90: after gravity, input, jetpack: b = down ? swim_down : fly key ? swim_up : swim_idle; vx = extra * (drag * vx); vy = drag * (vy - b * G), G = pixel_gravity * dt + const float G = e.mv.w * ctrl.view.w; + // Down has priority over Fly (the down key is ControlsComponent.mButtonDownDown, the up one mButtonDownFly): down gives swim_down_buoyancy_coeff, fly swim_up_, nothing swim_idle_ + const bool downKey = kind == 3 && ctrl.inp3.z != 0 && !frozen; + const float fishDy = (int(e.ex15.w) & 4) != 0 && e.ex15.z > 0.5 && e.ex15.z < 1.5 ? e.ex15.y - pos.y : 0.0; // a fish's up / down buttons (AdvancedFishAI's ControlsComponent) + const float b = kind == 3 ? (downKey ? e.ex6.x : (want_jump ? e.ex5.y : e.ex5.x)) : (fishDy > 2.0 ? e.ex6.x : (fishDy < -2.0 ? e.ex5.y : (want_jump && !grounded ? e.ex5.y : e.ex5.x))); + vel.x = e.ex5.w * (e.ex5.z * vel.x); + vel.y = e.ex5.z * (vel.y - b * G); + } + // 00c370c0 counters: only when the integer position changed, 3x3 count of liquid that is not powder; displacement 75 % of the time when on the ground + { + ivec2 last = ivec2(e.ex6.zw); + const ivec2 tp = ivec2(pos); // truncation toward zero (00c370c0) + if (tp != last) { + int n = 0; + for (int dy = -1; dy <= 1; ++dy) for (int dx = -1; dx <= 1; ++dx) if (liquid_nonsand(tp + ivec2(dx, dy))) ++n; + ents[id].ex6.z = float(tp.x); ents[id].ex6.w = float(tp.y); + if (n > 0 && kind == 4) vel *= 1.0 - e.ex5.y * (1.0 / 60.0) * float(n); // 00c370c0: an item with a VelocityComponent loses liquid_drag * dt * N of its velocity (unclamped) + if (n > 0 && (kind == 1 || kind == 2) && !swimming && e.ex10.w > 0.0) vel *= 1.0 - e.ex10.w * (1.0 / 60.0) * float(n); // 00c370c0 liquid drag of creatures that do not swim (liquid_drag of VelocityComponent, default 1) + if (n > 0 && grounded) { // every qualifying cell: r % 101 < 75, then it leaves as a particle with -Rot(a) * 0.1 v, a = 0.6 u - 0.3 (the velocity of a cell per frame is the velocity per second / 60 for both) + for (int dy = 1; dy >= -1; --dy) for (int dx = -1; dx <= 1; ++dx) { + const uint rs = hash3(uint(id) * 9u + uint(dx + 1 + 3 * (dy + 1)), uint(pc.frame), 71u); + if (rs % 101u >= 75u) continue; + const float ang = 0.6 * float((rs >> 8) & 0x7FFFu) / 32767.0 - 0.3; + displace_cell(tp + ivec2(dx, dy), -(mat2(cos(ang), sin(ang), -sin(ang), cos(ang)) * (0.1 * vel))); + } + } + } + } + if (kind == 4) { const float sp = length(vel); if (sp > e.hp.z) vel *= e.hp.z / sp; } // 00c370c0: the terminal speed limits the vector length and rescales both axes + + // horizontal move in one-cell steps, climbing small steps when standing + float nx = pos.x + vel.x; + int steps = int(floor(nx)) - ip.x; + int sx = steps > 0 ? 1 : -1; + bool blocked = false; + for (int s = 0; s < abs(steps); ++s) { + ivec2 np = ip + ivec2(sx, 0); + if (!box_hits(np, e.box)) { ip = np; continue; } + bool ok = false; + if (grounded) { // (a flyer skimming the floor climbs its small steps too: it stopped dead at a plank's edge) + for (int k = 1; k <= int(e.hp.w) && !ok; ++k) { + ivec2 cp = np - ivec2(0, k); + if (!box_hits(cp, e.box)) { ip = cp; ok = true; } + } + } + if (!ok) { blocked = true; break; } + } + pos.x = blocked ? float(ip.x) + fract(pos.x) : nx; + if (blocked && !(kind == 1 && (int(e.ex15.w) & 2) != 0)) vel.x = 0.0; + if (knocked) ents[id].ex10.z = blocked ? 2.0 : 1.0; // (mCollidedHorizontally for the knockback end) // (the lob of a path jump keeps its speed against a wall: it rises past a steep slope's edge and goes on, not straight up and back down where it started) + // vertical move + pos.y = float(ip.y) + fract(pos.y); + float ny = pos.y + vel.y; + int vsteps = int(floor(ny)) - ip.y; + int sy = vsteps > 0 ? 1 : -1; + bool hit = false; + for (int s = 0; s < abs(vsteps); ++s) { + ivec2 np = ip + ivec2(0, sy); + if (!box_hits(np, e.box)) { ip = np; continue; } + hit = true; + break; + } + if (hit) { pos.y = float(ip.y); vel.y = 0.0; } else pos.y = ny; + grounded = box_hits(ip + ivec2(0, 1), e.box); + if (wasGrounded && !grounded && (kind == 1 || kind == 3) && vel.y > -0.05 && vel.y < 0.6 && !swimming) { // walking down a slope: the body follows the ground (up to the climb height) instead of hopping from step to step + for (int k = 1; k <= int(max(e.hp.w, 4.0)); ++k) + if (!box_hits(ip + ivec2(0, k), e.box) && box_hits(ip + ivec2(0, k + 1), e.box)) { ip.y += k; pos.y = float(ip.y); vel.y = 0.0; grounded = true; break; } + } + + // creatures: turn around at walls, hop over obstacles when they can jump - only the GPU's own fallback walkers: a creature the host runs (ex15.z) moves by its + // path alone (its jumps are the AnimalAI's prepared lobs; this random 3-in-4 hop at -jump_velocity_y was a second controller, docs/WEAK_ZOMBIE_AI_DIFFERENCES.md AI-04) + if (kind == 1 && blocked && e.ex15.z <= 0.5) { + if (grounded && (flags & ST_CANJUMP) != 0 && (e.x.y == 0) && (rnd & 3u) != 0u) vel.y = e.mv.z; + else dir = -dir; + } + flags = (flags & ~(ST_GROUNDED | ST_LIQUID)) | (grounded ? ST_GROUNDED : 0) | (liquid ? ST_LIQUID : 0); + + if ((e.st.w & 1024) != 0) { pos = e.a.xy; vel = e.a.zw; } // moved by the host (a worm: WormSystem): only its damage and death are handled here + ents[id].a = vec4(pos, vel); + ents[id].st = ivec4(kind, dir, timer, flags); + ents[id].hp = vec4(min(hp, kind == 3 && e.aux.x > 0 ? float(e.aux.x) * 0.001 : 1e9), cooldown, e.hp.z, e.hp.w); + ents[id].x.y = kind == 3 ? energy : e.x.y; + if (kind != 3) ents[id].x.z = e.x.z; + if (kind == 3) { ctrl.outp = vec4(pos, hp, float(dir)); ctrl.outp2 = vec4(float(energy) / ((e.ex2.x > 0.0 ? e.ex2.x : 180.0) * 64.0), cooldown, e.ex4.y > 0.0 ? ents[id].ex4.x / e.ex4.y : 1.0, ents[id].ex4.w > 0.0 ? 1.0 : 0.0); } +} diff --git a/shaders/ents_prev.comp b/shaders/ents_prev.comp new file mode 100644 index 0000000..5bc8908 --- /dev/null +++ b/shaders/ents_prev.comp @@ -0,0 +1,16 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 64) in; +#include "common.glsl" + +// Remembers the entity positions before a tick: sprites, projectiles and particles are drawn between the last two ticks. +struct Ent { vec4 a, box, mv, ai; ivec4 st; vec4 hp; ivec4 x; ivec4 rg; ivec4 rg2; ivec4 aux; vec4 ex; vec4 ex2; ivec4 ex3; vec4 ex4; vec4 ex5; vec4 ex6; vec4 ex7; ivec4 ex8; vec4 ex9; vec4 ex10; vec4 ex11; vec4 ex12, ex13, ex14, ex15, ex16, ex17, ex18, ex19; }; +layout(std430, set = 0, binding = 22) readonly buffer Ents { Ent ents[]; }; +layout(std430, set = 0, binding = 23) readonly buffer Ctrl { ivec4 inp; } ctrl; +layout(std430, set = 0, binding = 39) buffer EntPrevW { vec4 prevw[]; }; + +void main() { + uint i = gl_GlobalInvocationID.x; + if (i >= uint(ctrl.inp.w)) return; + prevw[i] = vec4(ents[i].a.xy, 0.0, 0.0); +} diff --git a/shaders/explode_rays.comp b/shaders/explode_rays.comp new file mode 100644 index 0000000..1f89947 --- /dev/null +++ b/shaders/explode_rays.comp @@ -0,0 +1,124 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 256) in; +#include "common.glsl" + +// Terrain part of an explosion with angular attenuation (docs/DAMAGE_CONTACTS_AND_EXPLOSIONS.md, 00657960 / 00656b10): 360 bins; each bin is a ray from the centre that +// walks the cells, spends min(remaining energy, cell hp left) on every solid / powder cell, and ends at a cell whose durability is above the maximum or when the energy +// is gone. A cell within the reach of its bin loses the bin's damage from its hp and goes below 1 (the damage stays on static cells: two weak blasts break what +// one cannot, as the engine's per-cell hp). One workgroup per edit +// of mode 2 (e[2k] = (x, y, radius <= 64, max durability), e[2k+1] = (2, ray energy, 0, 0)); the rays read the grid before the workgroup destroys anything. +layout(std430, set = 0, binding = 27) readonly buffer Edits { ivec4 head; vec4 e[512]; } ed; + +shared float reach2[360]; // the squared reach of each bin +shared float binDmg[360]; // the largest hp a ray spent on one cell (00656b10 *param_5) + +float rnd01(uint h) { return float(h & 0xFFFFu) / 65536.0; } +uint cell_hp(uint m) { return 50u * max(1u, (mats[3u * m + 2u].z >> 20) & 0xFFFu); } +// the rays and the hole spend the hp of solid and powder cells only (cell vtable slot 23: solid, slot 25: CellData liquid_sand +0x160); liquids and gases are free +bool ray_cell(uint m) { const uint f = mflags(m); return (f & F_POWDER) != 0u || ((f & F_STATIC) != 0u && (f & F_LIQCELL) == 0u); } +// the hp a cell has left: the material hp (007048c0: cell+4 = CellData hp +0x158) minus the explosion damage it took. A static cell keeps that damage in bits 24-26 of its word +// (free there: the movement passes skip static cells), in eighths of its hp, so it is saved with the cell when the window moves +float cell_hp_left(int i, uint m) { return float(cell_hp(m)) * ((mflags(m) & F_STATIC) != 0u ? 1.0 - float((cells[i] >> 24) & 3u) / 4.0 : 1.0); } + +void main() { + int k = int(gl_WorkGroupID.x); + if (k >= ed.head.x || k >= 256) return; + vec4 q = ed.e[2 * k], r = ed.e[2 * k + 1]; + if (r.x < 1.5 || r.x > 2.5) return; + const vec2 centre = q.xy; + const float R = min(q.z, 64.0); + if (!(R > 0.0)) return; // (a radius that is not a positive number - NaN, -inf: no rays; the rays' stop tests never hold for them) + const float maxDur = mod(q.w, 4096.0); + const float stainCode = floor(q.w / 4096.0); // 0: stains_enabled off, else 1 + stains_radius (gpu_fx.cpp) + // the burnt ring (00657960 + 0070e760): out to the radius + stains_radius (0: + 0.4 x the radius) + const float Rs = stainCode > 0.0 ? R + (stainCode > 1.0 ? stainCode - 1.0 : 0.4 * R) : R; + const uint rot = hash3(uint(k), uint(pc.frame), 41u) % 181u; // the random rotation of the roughness table + // 00656b10: a ray from the centre to the edge, unit steps, a cell sampled when the integer coordinates change; on a solid / powder cell spend = min(energy, its hp + // left), damage = max(damage, spend); it stops at a cell whose durability is above the maximum or when the energy is out: the reach is then the previous sample's + // squared distance (+1 on an even LCG draw), else the full radius + for (int bin = int(gl_LocalInvocationID.x); bin < 360; bin += 256) { + const float a = float(bin) * 6.2831853 / 360.0; + const vec2 dir = vec2(cos(a), sin(a)); + float energy = r.y >= 0.0 ? r.y : 20000.0; // ray_energy of the data, else the constructor default 20000 (004b2780) + ivec2 last = ivec2(floor(centre)); + float rr = R * R, dmg = 0.0, prevD2 = 0.0; + for (int s = 1; s <= 2048; ++s) { + const float d = float(s); + if (d * d >= R * R) break; + const ivec2 w = ivec2(floor(centre + dir * d)); + if (w == last) continue; + last = w; + const ivec2 p = w - ivec2(pc.view_x, pc.view_y); + if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) { rr = prevD2; break; } + const int i = p.y * pc.gw + p.x; + const uint m = cell_mat(cells[i]); + if (m != 0u && ray_cell(m)) { + const float spend = min(energy, max(cell_hp_left(i, m), 0.0)); + energy -= spend; + dmg = max(dmg, spend); + if (uintBitsToFloat(mats[3u * m + 2u].y) > maxDur || energy <= 0.0) { + rr = prevD2 + ((hash3(uint(bin), uint(k), uint(pc.frame) * 13u) & 1u) == 0u ? 1.0 : 0.0); + break; + } + } + prevD2 = d * d; + } + reach2[bin] = rr; + binDmg[bin] = dmg; + } + barrier(); + // the hole (00657960, hole_enabled): every cell at integer offsets inside the radius and within the reach of its bin; a solid / powder cell with a durability up to + // the maximum loses the bin's damage and goes below 1 hp (static cells keep the damage they took); other cells (liquids) go as before + const int hr = int(ceil(Rs)), n = 2 * hr + 1; + const ivec2 c0 = ivec2(floor(centre)); + for (int j = int(gl_LocalInvocationID.x); j < n * n; j += 256) { + const ivec2 o = ivec2(j % n, j / n) - ivec2(hr); + const ivec2 w = c0 + o; + const float dist2 = float(o.x * o.x + o.y * o.y); + if (dist2 > Rs * Rs) continue; + const int bin = (o.x == 0 && o.y == 0) ? 0 : int(mod(atan(float(o.y), float(o.x)) + 6.2831853, 6.2831853) * 360.0 / 6.2831853) % 360; + const ivec2 p = w - ivec2(pc.view_x, pc.view_y); + if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) continue; + const int i = p.y * pc.gw + p.x; + if (dist2 >= R * R || dist2 > reach2[bin]) { + // not reached: a stainable static solid / powder cell is burnt by s = (uniform(0.5, 0.7)) x roughness[bin + rotation] x falloff^2, the falloff 1 - (d^2 - reach^2) / + // (Rs^2 - R^2) clamped to 0..1; the roughness of a bin: uniform(0, 0.5) + (sin(bin / 90) + 1) x 0.25. Each channel x (1 - s): kept as the product in ext.x (render.comp) + if (stainCode <= 0.0 || Rs <= R) continue; + const uint sm = cell_mat(cells[i]); + if (sm == 0u || !ray_cell(sm) || (mflags(sm) & (F_STATIC | F_NOSTAIN)) != F_STATIC || (cells[i] & BURN_BIT) != 0u) continue; + const float fall = clamp(1.0 - (dist2 - reach2[bin]) / (Rs * Rs - R * R), 0.0, 1.0); + const uint rb = (uint(bin) + rot) % 360u; + const float rough = rnd01(hash3(rb, 0u, 43u)) * 0.5 + (sin(float(rb) / 90.0) + 1.0) * 0.25; // (00657960 fills DAT_012059f0 once, at the first explosion of the run: fixed per bin) + const float sv = (0.5 + 0.2 * rnd01(hash3(uint(i), uint(pc.frame), uint(k) + 44u))) * rough * fall * fall; + if (sv <= 0.0) continue; + // the burn level lives in bits 28-30 of the cell word (free on a static cell that does not burn; it is saved with the cell when the window moves): keep = 1 - L / 7, + // the new level rounded at random so that weak burns add up + const uint cw = cells[i]; + const float keep = (1.0 - float((cw >> 28) & 7u) / 7.0) * (1.0 - sv); + const float lv = (1.0 - keep) * 7.0; + const uint L = min(7u, uint(floor(lv + rnd01(hash3(uint(i), uint(pc.frame), uint(k) + 45u))))); + cells[i] = (cw & ~(7u << 28)) | (L << 28); + continue; + } + const uint m = cell_mat(cells[i]); + if (m != 0u && uintBitsToFloat(mats[3u * m + 2u].y) > maxDur) continue; + if (m != 0u && ray_cell(m)) { + const float left = cell_hp_left(i, m) - binDmg[bin]; + if (left >= 1.0) { // damaged, still there: the level of the damage rounded at random (weak hits add up), at most 3 quarters (bits 24-25; bit 26 is the frozen stain) + if ((mflags(m) & F_STATIC) != 0u) { + const float lv = (1.0 - left / float(cell_hp(m))) * 4.0; + const uint L = min(3u, uint(floor(lv + rnd01(hash3(uint(i), uint(pc.frame), uint(k) + 46u))))); + const uint cw = cells[i]; + cells[i] = (cw & ~(3u << 24)) | (L << 24); + } + continue; + } + cells[i] = 0u; ext[i] = vec2(0.0); + } else if (m != 0u) { cells[i] = 0u; ext[i] = vec2(0.0); } + // create_cell_material / create_cell_probability (00657960): every cell the blast reaches, destroyed or empty, becomes the material with that probability in percent (draw % 101 < p) + if (r.z > 0.5 && int(hash3(uint(w.x), uint(w.y), uint(pc.frame) * 7u + uint(k)) % 101u) < int(r.w)) { + cells[i] = uint(r.z) | (((hash3(uint(w.x), uint(w.y), 97u) >> 16) & 0xFFu) << 16); ext[i] = vec2(0.0); birth[i] = birth_frame(); + } + } +} diff --git a/shaders/far.glsl b/shaders/far.glsl new file mode 100644 index 0000000..50e4eb5 --- /dev/null +++ b/shaders/far.glsl @@ -0,0 +1,18 @@ +// Far blocks: 64 x 64 cell blocks of the world outside the simulated window, kept on the GPU (not simulated) for the projectiles that leave it - a black hole +// with accelerating shots eats on through them instead of meeting a wall at the window's edge. main.cpp makes them resident ahead of such projectiles (from the +// persistent store, the rest generated by terrain.comp shift_mode 3), edits.comp pass 1 applies the edits there, the dirty ones go back to the store. +// FarMap: (origin block x, y, map width, height), then a slot + 1 per block of the map (0 = not resident), then a dirty flag per slot, then the generation +// jobs: a count, then (slot, block x, block y, store-backed) each. +const int kFarMapW = 48, kFarMapH = 48, kFarSlots = 512, kFarJobs = 64; +const int kFarDirty = 4 + kFarMapW * kFarMapH, kFarJobCount = kFarDirty + kFarSlots, kFarJobList = kFarJobCount + 4; +layout(std430, set = 0, binding = 61) buffer FarCells { uint farcells[]; }; +layout(std430, set = 0, binding = 62) buffer FarMap { int farmap[]; }; + +// the slot + 1 of the block of world cell w (0: none) +int far_slot(ivec2 w) { + const ivec2 b = (w >> 6) - ivec2(farmap[0], farmap[1]); + if (b.x < 0 || b.y < 0 || b.x >= farmap[2] || b.y >= farmap[3]) return 0; + return farmap[4 + b.y * farmap[2] + b.x]; +} +bool far_in_map(ivec2 w) { const ivec2 b = (w >> 6) - ivec2(farmap[0], farmap[1]); return b.x >= 0 && b.y >= 0 && b.x < farmap[2] && b.y < farmap[3]; } +int far_index(int slot1, ivec2 w) { return (slot1 - 1) * 4096 + (w.y & 63) * 64 + (w.x & 63); } diff --git a/shaders/fog.comp b/shaders/fog.comp new file mode 100644 index 0000000..c1acbbe --- /dev/null +++ b/shaders/fog.comp @@ -0,0 +1,112 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 8, local_size_y = 8) in; +#include "common.glsl" + +// Fog of war, as FogOfWarRadiusSystem (lambda 00d8f000 -> 00aeef70): a persistent map of the world in 32 x 32 pixel texels (the engine's fog byte, 255 = fog, 0 = clear; +// kept here as its complement, the "clear" amount, so that the zeroed buffer is all fog). It only clears: what was seen stays seen. Every frame, for the player +// (FogOfWarRadiusComponent radius 200, data/entities/misc/fogofwar_radius.xml; the position lifted by 4 x uniform(0, 3) pixels): +// * the 3 x 3 texels around it: fog = min(fog, max(fog - 3 d, 255 d)), d = the distance from the texel centre in texels; +// * 36 rays (10 degrees apart) of the radius, stopped by the terrain: at 0, 1/4, 2/4 and 3/4 of the free part of each ray the texel loses 5 of fog +// (a texel crossed by several rays loses more; 51 frames clear one entirely). +// render.comp reads the map bilinearly (the texel centres at 32 n + 16) as post_final.frag reads tex_fog. +struct Ent { vec4 a, box, mv, ai; ivec4 st; vec4 hp; ivec4 x; ivec4 rg; ivec4 rg2; ivec4 aux; vec4 ex; vec4 ex2; ivec4 ex3; vec4 ex4; vec4 ex5; vec4 ex6; vec4 ex7; ivec4 ex8; vec4 ex9; vec4 ex10; vec4 ex11; vec4 ex12, ex13, ex14, ex15, ex16, ex17, ex18, ex19; }; +layout(std430, set = 0, binding = 22) readonly buffer Ents { Ent ents[]; }; +layout(std430, set = 0, binding = 49) buffer Fog { uint fog[]; }; // per texel: 255 - the fog byte (the clear amount) + +layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; }; // after the fire palettes: the biome map's width, height and fog_of_war_type per tile (main.cpp) +#include "biome_mods.glsl" +uint fog_type_at(vec2 w) { // 0 default, 1 HEAVY_CLEAR_AT_PLAYER, 2 HEAVY_CLEAR_WITH_MAGIC, 3 HEAVY_NO_CLEAR; outside the map: the default + const uint n = mats[mats.length() - 1u].w; + const uint fb = 1024u + 9u * n + 132u + n; + const int bw = int(tbl[fb]), bh = int(tbl[fb + 1u]); + const ivec2 t = ivec2(floor(w / 512.0)) + ivec2(35, 14); + if (bw <= 0 || t.x < 0 || t.y < 0 || t.x >= bw || t.y >= bh) return 0u; + return tbl[fb + 2u + uint(t.y * bw + t.x)]; +} +const int FOG_TW = 1152, FOG_TH = 896, FOG_OX = 18432, FOG_OY = 14336; // texels of 32 pixels from the world origin (-18432, -14336) +const float RADIUS = 256.0; // the player's (component_documentation: "256 is the default player has"; the X-ray card's fogofwar_radius.xml has 200) + +bool solid_at(ivec2 w) { // (world cell; outside the window: open) + const ivec2 p = w - ivec2(pc.view_x, pc.view_y); + if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false; + const uint m = cell_mat(cells[p.y * pc.gw + p.x]); + return m != 0u && (mflags(m) & (F_STATIC | F_SAND)) != 0u; +} +int texel_index(vec2 w) { + const ivec2 t = ivec2(floor((w + vec2(FOG_OX, FOG_OY)) / 32.0)); + if (t.x < 0 || t.y < 0 || t.x >= FOG_TW || t.y >= FOG_TH) return -1; + return t.y * FOG_TW + t.x; +} + +// the solid share of the 64 x 64 pixel region of a point, 0..255 (the coarse map of the world the 3 x 3 rule reads; outside the window: 0) +float coarse_at(vec2 w) { + const ivec2 r0 = ivec2(floor(w / 64.0)) * 64; + int n = 0, tot = 0; + for (int y = 2; y < 64; y += 4) for (int x = 2; x < 64; x += 4) { + const ivec2 p = r0 + ivec2(x, y) - ivec2(pc.view_x, pc.view_y); + if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) continue; + ++tot; + const uint m = cell_mat(cells[p.y * pc.gw + p.x]); + if (m != 0u && (mflags(m) & (F_STATIC | F_SAND)) != 0u) ++n; + } + return tot > 0 ? 255.0 * float(n) / float(tot) : 0.0; +} + +void main() { + // one workgroup per source: 0 the player (radius 256), then the other FogOfWarRadiusComponents (main.cpp, after the holes in the tables) + const uint t = gl_LocalInvocationIndex; + if (pc.pass_ != 0) return; + if (t >= 45u) { // 00c42d30: the biome's fog_of_war_delta (FOG_OF_WAR_REAPPEARS: 2) - every frame the fog bytes of the 17 x 17 texels around the player (ceil(427 / 32 x 0.5) + 1 = 8 each way) gain it + if (gl_WorkGroupID.x != 0u || (ents[0].st.w & 8) != 0) return; + const vec2 P0 = ents[0].a.xy; + const int delta = int(bitfieldExtract(int(bm_bytes(P0)), 0, 8)); // (a signed byte) + if (delta == 0) return; + for (uint k = t - 45u; k < 289u; k += 19u) { + const int ti = texel_index(P0 + vec2(float(int(k % 17u) - 8), float(int(k / 17u) - 8)) * 32.0); + if (ti < 0) continue; + uint old = fog[ti]; + for (int tries = 0; tries < 4; ++tries) { const uint nv = uint(clamp(int(min(old, 255u)) - delta, 0, 255)); const uint got = atomicCompSwap(fog[ti], old, nv); if (got == old) break; old = got; } // (fog[] holds 255 - the fog byte) + } + return; + } + const uint wg = gl_WorkGroupID.x; + vec2 P; + float R = RADIUS; + if (wg == 0u) { + if ((ents[0].st.w & 8) != 0) return; // (dead) + P = ents[0].a.xy; + } else { + const uint n = mats[mats.length() - 1u].w; + const uint sb = 1024u + 9u * n + 132u + n + 2u + 8192u + 1u + 64u * 6u; + if (int(wg) > int(uintBitsToFloat(tbl[sb]))) return; + const uint o = sb + 1u + (wg - 1u) * 3u; + P = vec2(uintBitsToFloat(tbl[o]), uintBitsToFloat(tbl[o + 1u])); + R = uintBitsToFloat(tbl[o + 2u]); + } + P -= vec2(0.0, 4.0 * float(hash3(uint(pc.frame) + wg * 131u, 61u, 7u) & 3u)); + if (t < 36u) { // a ray + const float a = float(t) * 0.17453294; + const vec2 dir = vec2(cos(a), sin(a)); + float len = R; + for (int s = 1; s < int(R); ++s) if (solid_at(ivec2(floor(P + dir * float(s))))) { len = float(s); break; } + for (int k = 0; k < 4; ++k) { + const vec2 sw = P + dir * (len * 0.25 * float(k)); + if (fog_type_at(sw) != 0u) continue; // only the default fog of war clears along the rays + const int i = texel_index(sw); + if (i >= 0 && fog[i] < 255u) atomicAdd(fog[i], 5u); + } + return; + } + // the 3 x 3 texels around the player + const int k = int(t) - 36; + const vec2 tc = (floor((P + vec2(FOG_OX, FOG_OY)) / 32.0) + vec2(float(k % 3 - 1), float(k / 3 - 1))) * 32.0 + 16.0 - vec2(FOG_OX, FOG_OY); + const int i = texel_index(tc); + if (i < 0 || fog_type_at(tc) > 1u) return; // the default and HEAVY_CLEAR_AT_PLAYER clear around the player + const float d = distance(P, tc) / 32.0; + const float old = 255.0 - float(min(fog[i], 255u)); + float target = 255.0 * d; + if (k % 3 != 1 && k / 3 != 1) target = max(target, (coarse_at(tc) * 16.0 / 1024.0 - d) * 255.0); // the four diagonal texels: a solid region stays fogged (the coarse byte x 16 / 1024) + const float nf = min(old, max(old - 3.0 * d, clamp(target, 0.0, 255.0))); + atomicMax(fog[i], uint(255.0 - nf)); +} diff --git a/shaders/fx_anims.comp b/shaders/fx_anims.comp new file mode 100644 index 0000000..254adcc --- /dev/null +++ b/shaders/fx_anims.comp @@ -0,0 +1,69 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 64) in; +#include "fx_common.glsl" + +// Explosion sprites and flashes: advance, draw the animated sprite (centred) and add a fading light. +// One workgroup per animation record (the dispatch is FANIM_ALL groups): its 64 threads share the sprite's pixels. (One thread per record drew a big +// explosion sprite alone - 100 000 pixels and more - and three bombs took 15 ms of GPU time.) +void main() { + const uint id = gl_WorkGroupID.x, lane = gl_LocalInvocationID.x; + const bool drawOnly = pc.pass_ == 1; // between ticks nothing advances + if (gl_GlobalInvocationID.x == 0u && !drawOnly) lpad[0] = lpad[0] * 5u / 6u; // camera shake decays + if (id >= uint(FANIM_ALL)) return; + FAnim a = fanims[id]; + barrier(); // (every lane has read the record before lane 0 advances it) + if (a.b.z <= 0) return; + int age = drawOnly ? a.b.y : a.b.y + 1; + if (age >= a.b.z) { if (!drawOnly && lane == 0u) fanims[id].b.z = 0; return; } + float k = 1.0 - float(age) / float(a.b.z); + if (!drawOnly && lane == 0u) { + fanims[id].b.y = age; + const float lk = a.c.y > 0.0 ? max(0.0, 1.0 - float(age) / a.c.y) : k; // (c.y: the light's own fade frames) + if (a.a.w > 0.0 && lk > 0.0) add_light(a.a.xy, a.a.z, a.a.w * lk, rgb_of(uint(a.b.w))); + } + if (a.b.x < 0) return; + int b = BOOM_BASE + a.b.x * BOOM_STRIDE; + ivec4 b0 = fti(b), b1 = fti(b + 1); + vec4 b3 = ftf(b + 3); + if (b0.y <= 0 || b0.z <= 0) return; + int frame = age / max(b1.x, 1); + if (frame >= b0.w) return; + // the sprite is placed with its pivot at the effect position and rotated by the angle (muzzle flashes point along the shot) + // c.z: a scale (0: 1; negative: drawn behind - only where no cell and no sprite is, the render_back sprite particles of the host, main.cpp); c.w: a tint (rgb) and alpha + // (bits 24-31) as bits (0: none); a host record (tinted) has its own y scale in c.y (the GPU's records keep their light's fade there) + const uint tw = floatBitsToUint(a.c.w); + const bool tinted = tw != 0u; + const float sx0 = a.c.z == 0.0 ? 1.0 : abs(a.c.z); + const vec2 sclv = max(vec2(sx0, tinted ? a.c.y : sx0), vec2(0.01)); + const float scl = max(sclv.x, sclv.y); + const bool behind = a.c.z < 0.0; + const vec3 tint = vec3(float((tw >> 16) & 255u), float((tw >> 8) & 255u), float(tw & 255u)) / 255.0; + const float alpha = tinted ? float(tw >> 24) / 255.0 : 1.0; + vec2 pivot = b3.zw, size = vec2(b0.yz); + float ca = cos(a.c.x), sa = sin(a.c.x); + float r = (length(max(pivot, size - pivot)) + 1.5) * scl; + int n = int(2.0 * r) + 2; + ivec2 lo = ivec2(floor(a.a.xy - vec2(r))); + bool additive = (b1.w & 8) != 0; + for (int j = int(lane); j < n * n; j += 64) { + ivec2 w = lo + ivec2(j % n, j / n); + vec2 d = vec2(w) + 0.5 - a.a.xy; + ivec2 sp = ivec2(floor(pivot + vec2(ca * d.x + sa * d.y, -sa * d.x + ca * d.y) / sclv)); + if (sp.x < 0 || sp.y < 0 || sp.x >= b0.y || sp.y >= b0.z) continue; + ivec2 q = w - ivec2(pc.view_x, pc.view_y); + if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue; + uint t = fxtex[b0.x + frame * b0.y * b0.z + sp.y * b0.y + sp.x]; + if ((t >> 24) == 0u) continue; + const int qi = q.y * pc.gw + q.x; + if (behind && (cell_mat(cells[qi]) != 0u || entlayer[qi] != 0u)) continue; // (behind the terrain and the sprites already drawn) + if (tinted) { + const vec3 c = vec3(float((t >> 16) & 255u), float((t >> 8) & 255u), float(t & 255u)) / 255.0 * tint; + const float al = float(t >> 24) / 255.0 * alpha; + if (al < 0.02) continue; + if (additive) t = 0x01000000u | packUnorm4x8(vec4(c.b * al, c.g * al, c.r * al, 0.0)); + else t = (uint(al * 255.0) << 24) | (packUnorm4x8(vec4(c.b, c.g, c.r, 0.0)) & 0xFFFFFFu); + entlayer[qi] = t; + } else entlayer[qi] = additive ? (0x01000000u | (t & 0xFFFFFFu)) : t; + } +} diff --git a/shaders/fx_common.glsl b/shaders/fx_common.glsl new file mode 100644 index 0000000..0ee6397 --- /dev/null +++ b/shaders/fx_common.glsl @@ -0,0 +1,208 @@ +// Shared declarations of the GPU wand/projectile/effect system (proj_spawn, proj_step, fx_particles, fx_anims, rigid_events). +// The CPU only runs the spell script (gun.lua) and uploads its result: tables of projectile types, effect definitions, and the shot tree +// (nodes and specs) of every cast. Flight, collisions, triggers, explosions, damage, emitters, particles, lights and animations run here. +#include "common.glsl" +#include "far.glsl" + +struct Ent { + vec4 a, box, mv, ai; + ivec4 st; + vec4 hp; + ivec4 x, rg, rg2, aux; + vec4 ex; // detection cone (cos of half angle), eye offset y, melee enabled, 0 + vec4 ex2; // ranged attack: projectile speed, gravity + ivec4 ex3; // blood material + 1, blood colour, damage types taken since the last check (bit 1 projectile, 2 explosion, 4 fire, 8 melee), 0 + vec4 ex4; // air in the lungs (frames), air maximum (0 = does not need air), damage per frame without air, frames left on fire + vec4 ex5; // swimming: swim_idle_buoyancy_coeff, swim_up_buoyancy_coeff, swim_drag, swim_extra_horizontal_drag + vec4 ex6; // swim_down_buoyancy_coeff, frames not swimming, pixel of the last liquid count (x, y) + vec4 ex7; // blood_multiplier (creatures); liquid inventory of a body: reserve, material + 1, frames of emission left + ivec4 ex8; vec4 ex9; vec4 ex10; vec4 ex11; vec4 ex12, ex13, ex14, ex15, ex16, ex17, ex18, ex19; // damage event of a body: hit point relative to the body x 1000, damage x 1000 pending +}; +layout(std430, set = 0, binding = 21) buffer EntLayer { uint entlayer[]; }; +layout(std430, set = 0, binding = 22) buffer Ents { Ent ents[]; }; +layout(std430, set = 0, binding = 23) readonly buffer Ctrl { ivec4 inp; ivec4 inp2; ivec4 inp3; vec4 outp; vec4 outp2; vec4 view; vec4 hand0; ivec4 hand1; vec4 hand2; } ctrl; +layout(std430, set = 0, binding = 39) readonly buffer EntPrev { vec4 prevpos[]; }; // entity positions before the last tick (for interpolation) +layout(std430, set = 0, binding = 27) buffer Edits { ivec4 ehead; vec4 ee[512]; }; // cell edits, two vec4 each (see edits.comp) + +layout(std430, set = 0, binding = 33) readonly buffer FxTables { uvec4 ft[]; }; // projectile types, explosion effects, emitters (words, see gun/gpu_fx.h) +layout(std430, set = 0, binding = 34) readonly buffer FxTex { uint fxtex[]; }; // sprite frames (ARGB) of projectiles and explosions +#include "fx_host.glsl" + +#include "fx_state.glsl" +// rigid bodies (read only): projectiles stop at them +layout(std430, set = 0, binding = 30) readonly buffer RShapes { uint rsh[]; }; +struct RBody { vec4 p; vec4 v; ivec4 i; vec4 q; vec4 l; vec4 n; vec4 m; ivec4 jn; vec4 ja0; vec4 ja1; vec4 ja2; vec4 ja3; }; +layout(std430, set = 0, binding = 31) readonly buffer RBodies { ivec4 rhdr; RBody rbodies[]; }; +layout(std430, set = 0, binding = 37) buffer Xev { ivec4 xhead; vec4 xe[512]; }; // explosion events for the rigid bodies: x, y, radius, power +struct FLight { vec4 a; vec4 b; }; // x, y, radius, strength / rgb +layout(std430, set = 0, binding = 38) buffer FxLights { uint lcount; uint lpad[3]; FLight lights[512]; }; + +const int TYPE_STRIDE = 18, BOOM_BASE = 9216, BOOM_STRIDE = 4, EMIT_BASE = 9472, EMIT_STRIDE = 4, LOAD_BASE = 13568; // (512 types: the scripts' variants of a projectile are types of their own) + +float uf(uint u) { return uintBitsToFloat(u); } +vec4 ftf(int i) { return uintBitsToFloat(ft[i]); } +ivec4 fti(int i) { return ivec4(ft[i]); } +float rnd01(uint h) { return float(h & 0xFFFFu) / 65536.0; } + +bool solid_at(ivec2 w) { + ivec2 p = w - ivec2(pc.view_x, pc.view_y); + if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) { // outside the window: a resident far block (far.glsl), else a wall + const int s = far_slot(w); + if (s == 0) return true; + const uint fm = cell_mat(farcells[far_index(s, w)]); + return fm != 0u && (mflags(fm) & (F_STATIC | F_SAND)) != 0u; + } + uint m = cell_mat(cells[p.y * pc.gw + p.x]); + return m != 0u && (mflags(m) & (F_STATIC | F_SAND)) != 0u; +} + + +void add_light(vec2 pos, float radius, float strength, vec3 rgb) { + uint i = atomicAdd(lcount, 1u); + if (i < 512u) { lights[i].a = vec4(pos, radius, strength); lights[i].b = vec4(rgb, 0.0); } +} + +uint add_anim(vec2 pos, int boom, float radius, float strength, int maxage, uint rgb, float angle) { // the record + uint s = atomicAdd(fcnt[2], 1u) % uint(FANIM_MAX); + fanims[s].a = vec4(pos, radius, strength); + fanims[s].b = ivec4(boom, 0, maxage, int(rgb & 0xFFFFFFu)); + fanims[s].c = vec4(angle, 0.0, 0.0, 0.0); + return s; +} + +void add_edit(vec4 q, vec4 r) { + uint i = uint(atomicAdd(ehead.x, 1)); + if (i < 256u) { ee[2u * i] = q; ee[2u * i + 1u] = r; } +} + +// A world point inside a rigid body's pixel mask? (the grid is filled by rigid_raster.comp) +layout(std430, set = 0, binding = 40) readonly buffer BodyGrid { uint bodyGrid[]; }; +bool body_hit(vec2 w) { + ivec2 p = ivec2(floor(w)) - ivec2(pc.view_x, pc.view_y); + return p.x >= 0 && p.y >= 0 && p.x < pc.gw && p.y < pc.gh && bodyGrid[p.y * pc.gw + p.x] != 0u; +} + +// The entity slot of the rigid body that covers a world point, -1 when none. +int body_slot(vec2 w) { + ivec2 p = ivec2(floor(w)) - ivec2(pc.view_x, pc.view_y); + if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return -1; + return int(bodyGrid[p.y * pc.gw + p.x] & 0x7FFFFFFFu) - 1; // (bit 31: the characters-pass mark of rigid_raster.comp) +} + +// Who damaged an entity last: the slot + 1 of the attacker (0 = unknown) in the low 12 bits of ex8.w, the frame in the rest. The host reads it when the hp drops (script_damage_received). +void note_attacker(int victim, int attacker_plus_1) { + if (attacker_plus_1 > 0 && attacker_plus_1 < 4096) ents[victim].ex8.w = attacker_plus_1 | ((pc.frame & 0xFFFFF) << 12); +} + +// A hit that the victim's multipliers took to nothing (a protection): the frame in ex11.w of a creature or the player, for the host's BLOCK message (00b071f0) +void note_block(int victim, float raw, float dealt) { + const int kind = ents[victim].st.x; + if (raw > 0.0 && dealt <= 0.0 && (kind == 1 || kind == 2 || kind == 3)) ents[victim].ex11.w = float(pc.frame); +} + +// A damage event on a body with a liquid inventory: the corpse part emits (entities.comp). +void body_damage(int slot, vec2 hit, float dmg) { + if (slot < 0 || ents[slot].ex7.y <= 0.0) return; + atomicAdd(ents[slot].ex8.z, max(1, int(dmg * 1000.0))); + const vec2 r = hit - ents[slot].a.xy; const float ca = cos(ents[slot].a.z), sa = sin(ents[slot].a.z); + const vec2 rel = vec2(ca * r.x + sa * r.y, -sa * r.x + ca * r.y); // in the body's frame (entities.comp turns the leak back with the rotation) + atomicExchange(ents[slot].ex8.x, int(rel.x * 1000.0)); atomicExchange(ents[slot].ex8.y, int(rel.y * 1000.0)); +} + +// The damage multiplier the perks give a shooter; applied ONCE when the shot is made (Message_Shot, 00b06870: projectile damage, and the explosion damage unless not_scaled_by_gamefx) +float dealt_of(int shooter) { + if (shooter <= 0) return 1.0; + const Ent s = ents[shooter - 1]; + return (s.st.x == 1 || s.st.x == 2 || s.st.x == 3) && s.ex7.y > 0.0 ? s.ex7.y : 1.0; +} + +// The liquid material at a position (0 when there is none). +uint liquid_at_pos(vec2 pos) { + ivec2 p = ivec2(floor(pos)) - ivec2(pc.view_x, pc.view_y); + if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return 0u; + uint m = cell_mat(cells[p.y * pc.gw + p.x]); + if (m == 0u) return 0u; + uint f = mflags(m); + return ((f & F_LIQUID) != 0u && (f & (F_SAND | F_STATIC)) == 0u) ? m : 0u; +} + +// A projectile pushes the liquid cells around its position aside (to the nearest free cell away from it). +void displace_liquid(vec2 pos) { + { ivec2 p0 = ivec2(floor(pos)) - ivec2(pc.view_x, pc.view_y); if (p0.x < 1 || p0.y < 1 || p0.x >= pc.gw - 1 || p0.y >= pc.gh - 1) return; + uint m0 = cell_mat(cells[p0.y * pc.gw + p0.x]); if (m0 == 0u || (mflags(m0) & F_LIQUID) == 0u) return; } // only when the projectile is inside a liquid + for (int dy = -1; dy <= 1; ++dy) + for (int dx = -1; dx <= 1; ++dx) { + ivec2 w = ivec2(floor(pos)) + ivec2(dx, dy); + ivec2 p = w - ivec2(pc.view_x, pc.view_y); + if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) continue; + int i = p.y * pc.gw + p.x; + uint c = cells[i]; + uint m = cell_mat(c); + if (m == 0u) continue; + uint f = mflags(m); + if ((f & F_LIQUID) == 0u || (f & (F_SAND | F_STATIC)) != 0u) continue; + vec2 d = vec2(w) + 0.5 - pos; + d = length(d) < 0.01 ? vec2(0.0, -1.0) : normalize(d); + for (int r = 1; r <= 6; ++r) { + ivec2 t = w + ivec2(round(d * float(r))); + ivec2 q = t - ivec2(pc.view_x, pc.view_y); + if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) break; + int ti = q.y * pc.gw + q.x; + if (atomicCompSwap(cells[ti], 0u, c) == 0u) { ext[ti] = ext[i]; cells[i] = 0u; ext[i] = vec2(0.0); break; } + } + } +} + +vec3 rgb_of(uint c) { return vec3(float((c >> 16) & 255u), float((c >> 8) & 255u), float(c & 255u)) / 255.0; } + +// Explosion visuals: animated sprite, light flash, sparks and material sparks. +// radius >= 0: the explosion's own radius (a shot with Explosive Projectile: the file's 2 + 15 per modifier); the light is that x light_radius_coeff (00657960), the table +// holds it for the file's radius +void boom_fx(int boom, vec2 pos, uint seed, float radius) { + int b = BOOM_BASE + boom * BOOM_STRIDE; + ivec4 b0 = fti(b), b1 = fti(b + 1), b2 = fti(b + 2); + vec4 b3 = ftf(b + 3); + float rad = b3.x; + atomicMax(lpad[0], uint(b3.y * 1500.0)); // camera shake (milli-cells), decays in fx_anims + int frames = max(b0.w, 1), wait = max(b1.x, 1); + int maxage = b0.z > 0 ? frames * wait + 2 : 9; + // the light (ConfigExplosion light_*): radius explosion_radius x light_radius_coeff, colour light_r/g/b, fading over light_fade_time (b1.w bits 4-15 / 16-27) + const bool lightOn = (b1.w & 4) != 0; + float lrad = float((uint(b1.w) >> 4) & 0xFFFu); + if (radius >= 0.0) lrad = rad > 0.0 ? lrad * radius / rad : radius * 8.0; // (the coefficient default 8: 004b2780) + lrad = min(lrad, 4095.0); + const int lframes = max(1, int((uint(b1.w) >> 16) & 0xFFFu)); + const uint an = add_anim(pos, boom, lightOn ? max(lrad, 9.0) : 0.0, lightOn ? 0.6 : 0.0, max(max(maxage, 9), lightOn ? lframes : 0), uint(b1.y), 0.0); + if (lightOn) fanims[an].c.y = float(lframes); // (the light fades over its own time, the sprite plays over its own) + const int sp0 = int(uint(b2.x) & 0xFFu); + const uint sparkRgb = uint(b2.x) >> 8; + int ns = (b1.w & 1) != 0 ? sp0 + int(rnd01(hash3(seed, 1u, 7u)) * float(b2.y - sp0 + 1)) : 0; + for (int i = 0; i < ns && i < 64; ++i) { + float a = rnd01(hash3(seed, uint(i), 11u)) * 6.2832, sp = 0.8 + rnd01(hash3(seed, uint(i), 12u)) * 2.4; + emit_particle(pos, vec2(cos(a), sin(a)) * sp - vec2(0.0, 0.6), 18.0 + rnd01(hash3(seed, uint(i), 13u)) * 27.0, 0.1, sparkRgb, true, true); + } + int nm = (b1.w & 2) != 0 ? b2.z + int(rnd01(hash3(seed, 2u, 8u)) * float(b2.w - b2.z + 1)) : 0; + if ((b1.w & 10) == 10 && b0.z == 0) { // an ambient emitter (a portal): material sparks spread over a disc of the radius, drifting away from its centre, no gravity, long-lived, additive + for (int i = 0; i < nm && i < 160; ++i) { + float a = rnd01(hash3(seed, uint(i), 31u)) * 6.2832, rr = sqrt(rnd01(hash3(seed, uint(i), 32u))) * rad, sp = 0.05 + rnd01(hash3(seed, uint(i), 33u)) * 0.25; + vec2 d = vec2(cos(a), sin(a)); + emit_particle(pos + d * rr, d * sp, 150.0 + rnd01(hash3(seed, uint(i), 34u)) * 60.0, 0.0, uint(b1.z), true, true); + } + nm = 0; + } + for (int i = 0; i < nm && i < 64; ++i) { + float a = rnd01(hash3(seed, uint(i), 21u)) * 6.2832, sp = 0.5 + rnd01(hash3(seed, uint(i), 22u)) * 1.5; + emit_particle(pos, vec2(cos(a), sin(a)) * sp - vec2(0.0, 0.4), 20.0 + rnd01(hash3(seed, uint(i), 23u)) * 30.0, 0.12, uint(b1.z), true, true); + } +} + +// The damage of a projectile hit on entity k (TakeDamage_Impl 00ab9a90: every damage type goes through the victim's multiplier of that type): the projectile channel, the +// damage_by_type channels of the type (table words 12-14: melee, electricity, fire, ice / slice, drill, radioactive, poison / healing, curse, holy, explosion) and the wand's adds (f.x electricity, f.y ice) +float hit_damage(int k, float proj, int tb, vec4 f) { + const Ent v = ents[k]; + const vec4 c0 = ftf(tb + 12), c1 = ftf(tb + 13), c2 = ftf(tb + 14); + return proj * v.ex9.x + + c0.x * v.ex9.w + (c0.y + f.x) * v.ex12.x + c0.z * v.ex9.z + (c0.w + f.y) * v.ex12.w + + c1.x * v.ex12.z + c1.y * v.ex12.y + c1.z * v.ex13.z + c1.w * v.ex13.w + + c2.x * v.ex13.x + c2.y * v.ex14.y + c2.z * v.ex14.z + c2.w * v.ex9.y; +} diff --git a/shaders/fx_hand.comp b/shaders/fx_hand.comp new file mode 100644 index 0000000..d6bd6bd --- /dev/null +++ b/shaders/fx_hand.comp @@ -0,0 +1,102 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 64) in; +#include "fx_common.glsl" + +// The player's right arm and the item in its hand, drawn into the entity layer every frame. The arm (player_arm.xml) hangs at the body's +// right_arm_root hotspot (the host follows the stand / walk frames, so it breathes with the body), turns toward the aim point (the cursor, or +// ahead of the player) and is mirrored when aiming left; the item is held at the arm's "hand" hotspot, turned the same way about its grip. +// The arm header is in the first words of the hand texture region (hand1.x - its offset is absolute): [0] arm on, [1] arm pixels (absolute), +// [2..3] arm size, [4..5] arm root relative to the body (floats), [6..7] the arm sprite's pivot (offset_x, offset_y), [8..9] its hand hotspot; +// [48] cape on, [49..50] the corner of its 32 x 32 image (words 64..1087, rasterized by the host) relative to the body: drawn behind the body (empty pixels only). +const uint HAND_BASE = (1u << 20) - 32768u; + +uint hw(uint i) { return fxtex[HAND_BASE + i]; } + +// draws a w x h image whose pixel `pivot` sits at `org`, turned by (c, s), mirrored vertically when `mirror` +void draw_turned(vec2 org, vec2 pivot, float c, float s, bool mirror, int w, int h, uint off) { + float r = length(vec2(float(w), float(h))) + 2.0; + int n = int(2.0 * r) + 2; + ivec2 lo = ivec2(floor(org - vec2(r))); + for (int i = int(gl_LocalInvocationID.x); i < n * n; i += 64) { + ivec2 dst = lo + ivec2(i % n, i / n); + vec2 d = vec2(dst) + 0.5 - org; + vec2 l = vec2(c * d.x + s * d.y, -s * d.x + c * d.y); // the image frame, relative to the pivot + if (mirror) l.y = -l.y; + ivec2 sp = ivec2(floor(pivot + l)); + if (sp.x < 0 || sp.y < 0 || sp.x >= w || sp.y >= h) continue; + ivec2 q = dst - ivec2(pc.view_x, pc.view_y); + if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue; + uint t = fxtex[off + uint(sp.y * w + sp.x)]; + const uint ta = t >> 24; + if (ta == 0u) continue; + const uint di = uint(q.y * pc.gw + q.x); + const uint under = entlayer[di]; + if (ta < 255u && (under >> 24) != 0u) { // a translucent texel over the body: mixed with it (source over), not cut through it + uint mixd = max(ta, under >> 24) << 24; + for (uint sh = 0u; sh < 24u; sh += 8u) mixd |= (((t >> sh) & 0xFFu) * ta + ((under >> sh) & 0xFFu) * (255u - ta)) / 255u << sh; + entlayer[di] = mixd; + } else entlayer[di] = t; + } +} + +// The items NPCs hold (ItemPickUpperComponent is_in_npc): at word 32640 of the hand region the count, then 12 words per entry: [0] the slot + 1, [1] the image +// (absolute), [2..3] its size, [4..5] the grip, [6..7] the hand relative to the body, [8..9] the aim point (floats); drawn like the player's item, turned toward the aim +void draw_npc_items() { + const uint n = min(hw(32640u), 10u); + for (uint k = 0u; k < n; ++k) { + const uint b = 32641u + k * 12u; + const int sl = int(hw(b)) - 1; + if (sl <= 0 || sl >= int(ents.length()) || (ents[sl].st.w & 8) != 0) continue; + vec2 cur = ents[sl].a.xy, prv = prevpos[sl].xy; + vec2 body = distance(prv, cur) < 10.0 ? mix(prv, cur, ctrl.view.z) : cur; + vec2 hand = body + vec2(uintBitsToFloat(hw(b + 6u)), uintBitsToFloat(hw(b + 7u))); + vec2 aim = vec2(uintBitsToFloat(hw(b + 8u)), uintBitsToFloat(hw(b + 9u))); + float ang = length(aim - hand) > 0.5 ? atan(aim.y - hand.y, aim.x - hand.x) : 0.0; + float c = cos(ang), s = sin(ang); + draw_turned(hand, vec2(uintBitsToFloat(hw(b + 4u)), uintBitsToFloat(hw(b + 5u))), c, s, c < 0.0, int(hw(b + 2u)), int(hw(b + 3u)), hw(b + 1u)); + } + memoryBarrierBuffer(); + barrier(); +} + +void main() { + draw_npc_items(); + if (ctrl.inp.w < 1 || ents[0].st.x != 3 || (ents[0].st.w & 8) != 0 || (ents[0].ex8.x & 8192) != 0) return; + vec2 cur = ents[0].a.xy, prv = prevpos[0].xy; + vec2 body = distance(prv, cur) < 10.0 ? mix(prv, cur, ctrl.view.z) : cur; + if (hw(48u) == 1u) { // the cape, behind the body + ivec2 corner = ivec2(floor(body + vec2(uintBitsToFloat(hw(49u)), uintBitsToFloat(hw(50u))) + 0.5)); + for (int i = int(gl_LocalInvocationID.x); i < 1024; i += 64) { + uint t = hw(64u + uint(i)); + if ((t >> 24) == 0u) continue; + ivec2 q = corner + ivec2(i % 32, i / 32) - ivec2(pc.view_x, pc.view_y); + if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue; + if ((entlayer[q.y * pc.gw + q.x] >> 24) == 0u) entlayer[q.y * pc.gw + q.x] = t; + } + memoryBarrierBuffer(); + barrier(); + } + if (ctrl.hand0.z < 0.5) return; + const bool arm = hw(0u) == 1u; + vec2 root = arm ? body + vec2(uintBitsToFloat(hw(4u)), uintBitsToFloat(hw(5u))) : body + vec2(0.0, -5.0); + vec2 aim = ctrl.hand0.xy; + float ang = length(aim - root) > 0.5 ? atan(aim.y - root.y, aim.x - root.x) : 0.0; + float c = cos(ang), s = sin(ang); + bool mirror = c < 0.0; + vec2 hand = root; + if (arm) { // the hand: the arm's hand hotspot relative to its pivot, turned and mirrored with the arm + vec2 piv = vec2(uintBitsToFloat(hw(6u)), uintBitsToFloat(hw(7u))); + vec2 hl = vec2(uintBitsToFloat(hw(8u)), uintBitsToFloat(hw(9u))) + 0.5 - piv; + if (mirror) hl.y = -hl.y; + hand = root + vec2(c * hl.x - s * hl.y, s * hl.x + c * hl.y); + } + { // the item's recoil (00b51670): hand2.z px back along the aim, turned muzzle-up by hand0.w radians (the arm keeps the aim) + const float ia = ang + (mirror ? ctrl.hand0.w : -ctrl.hand0.w); + const float ic = cos(ia), is = sin(ia); + draw_turned(hand - vec2(c, s) * ctrl.hand2.z, ctrl.hand2.xy, ic, is, mirror, ctrl.hand1.y, ctrl.hand1.z, uint(ctrl.hand1.x)); + } + memoryBarrierBuffer(); + barrier(); + if (arm) draw_turned(root, vec2(uintBitsToFloat(hw(6u)), uintBitsToFloat(hw(7u))), c, s, mirror, int(hw(2u)), int(hw(3u)), hw(1u)); // the hand over the grip +} diff --git a/shaders/fx_host.glsl b/shaders/fx_host.glsl new file mode 100644 index 0000000..c8c9ef7 --- /dev/null +++ b/shaders/fx_host.glsl @@ -0,0 +1,7 @@ +// Data the host uploads for the GPU wand / projectile system: cast requests, shot nodes and specs (also appended to by the GPU). +layout(std430, set = 0, binding = 35) buffer FxHost { // written by the host: cast requests, shot nodes and specs + ivec4 hhead; // x, y: number of requests in list 0 / 1 + vec4 hcast[1024]; // 2 lists x 256 requests x 2 vec4: (x, y, dir x, dir y), (node or -boom-1, team, seed, flags) + vec4 hnodes[8192]; // 2048 nodes x 4 vec4 + ivec4 hspecs[16384]; // 4096 specs x 4 ivec4 +}; diff --git a/shaders/fx_particles.comp b/shaders/fx_particles.comp new file mode 100644 index 0000000..c6d5248 --- /dev/null +++ b/shaders/fx_particles.comp @@ -0,0 +1,263 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 64) in; +#include "fx_common.glsl" +layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; }; +layout(std430, set = 0, binding = 56) buffer PartGlow { uint partglow[]; }; // the glowing particles added up per glow texel (r, g, b x 256; glow.comp reads it) +#include "simplex3.glsl" + +// Particles: cosmetic ones (emitter trails, sparks) fade and vanish; "real" ones (blood, acid, slime...: max life < 0 encodes the material) +// fly like projectiles and turn into a cell of the material where they land. Both draw into the entity layer. + +// Blood on the ground: a red droplet (cosmetic or real) that hits a static cell stains it (bit 31 of the cell word, which static cells do not use); render.comp tints it. +void stain_at(ivec2 q, int material, uint seed) { + if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) return; + const vec3 mc = rgb_of(mats[3u * uint(material)].x); + if (!(mc.r > 1.5 * mc.g && mc.r > 1.5 * mc.b)) return; // only reddish liquids (blood) stain + for (int k = 0; k < 5; ++k) { + const ivec2 o = k == 0 ? ivec2(0) : (k == 1 ? ivec2(1, 0) : (k == 2 ? ivec2(-1, 0) : (k == 3 ? ivec2(0, 1) : ivec2(0, -1)))); + const ivec2 t = q + o; + if (t.x < 0 || t.y < 0 || t.x >= pc.gw || t.y >= pc.gh) continue; + if (k > 0 && (hash3(seed, uint(k), 93u) & 1u) != 0u) continue; + const uint m = cell_mat(cells[t.y * pc.gw + t.x]); + if (m != 0u && (mflags(m) & F_STATIC) != 0u && (cells[t.y * pc.gw + t.x] & BURN_BIT) == 0u) atomicOr(cells[t.y * pc.gw + t.x], 0x80000000u); + } +} + +vec3 cell_colour(uint cw) { return rgb_of(mats[3u * cell_mat(cw)].x); } + +// A particle of displaced liquid (docs/DISPLACEMENT_AND_FIRE.md, 00713e20 / 00712390 / 0071fba0): it moves by its OLD velocity, accelerates (a quarter of gravity in +// mode 1), marches cell by cell (at most 60 steps) and ends where it hits liquid or solid; it then goes back into the grid, keeping its identity. +void displaced_particle(uint id, FPart p) { + vec2 pos = p.a.xy, vel = p.a.zw; + uint kind = p.c.y; + uint cw = p.c.x; + if (pc.pass_ == 1) { // between ticks: only draw + pos -= vel * (1.0 - ctrl.view.z); + ivec2 q = ivec2(floor(pos)) - ivec2(pc.view_x, pc.view_y); + if (q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh) { + uvec3 d8 = uvec3(cell_colour(cw) * 255.0 + 0.5); + entlayer[q.y * pc.gw + q.x] = 0xFF000000u | (d8.r << 16) | (d8.g << 8) | d8.b; + } + return; + } + uint seed = id * 7u + uint(pc.frame); + ivec2 here = ivec2(floor(pos)); + if (kind == 3u) { // waiting for a free place + if (reinsert(cw, here, seed, floatBitsToUint(p.b.z))) { fparts[id].b.x = 0.0; return; } + // No place: out of the window it is gone (00712390: a particle out of the loaded area dies; it waited there forever and filled the pool - 1000+ after 25 s). + // In the window the search of its column ended on a solid (a pocket under an overhang, a pool under ice, a column shifted into the rock): it drifts a cell, + // down into a free cell (air, gas), else sideways, and searches again from there; after 10 s it is dropped. (It used to wait where it was: blood and water + // pixels frozen in the air near pools.) + const ivec2 hq = here - ivec2(pc.view_x, pc.view_y); + const uint tries = p.c.w & 0xFFFFFu; + if (hq.x < 0 || hq.y < 0 || hq.x >= pc.gw || hq.y >= pc.gh || tries >= 600u) { fparts[id].b.x = 0.0; return; } + ivec2 step = ivec2(0, 1); + if (hq.y + 1 >= pc.gh || (mflags(cell_mat(cells[(hq.y + 1) * pc.gw + hq.x])) & (F_STATIC | F_LIQUID)) != 0u) { + const int sx = (hash3(seed, tries, 67u) & 1u) != 0u ? 1 : -1; + step = ivec2(sx, 0); + if (hq.x + sx < 0 || hq.x + sx >= pc.gw || (mflags(cell_mat(cells[hq.y * pc.gw + hq.x + sx])) & F_STATIC) != 0u) step = ivec2(0); + } + fparts[id].a.xy = pos + vec2(step); + fparts[id].c.w = p.c.w + 1u; + return; + } + const float g = 0.1667; + vec2 target = pos + vel; + vel.y += g * (kind == 1u ? 0.25 : 1.0); + if (kind == 1u && vel.y > 0.0) kind = 2u; + // 00712390: probes at unit-length increments along the move, at most 60, then the final target is committed + const float mlen = length(target - pos); + int steps = min(60, int(mlen)); + vec2 dir = mlen > 1e-6 ? (target - pos) / mlen : vec2(0.0); + vec2 cur = pos; + uint budget = p.c.z; + bool dead = false; + float dens = mdensity(cell_mat(cw)); + for (int s = 0; s < steps && !dead; ++s) { + vec2 nx = cur + dir; + ivec2 q = ivec2(floor(nx)) - ivec2(pc.view_x, pc.view_y); + if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) { dead = true; break; } + uint m = cell_mat(cells[q.y * pc.gw + q.x]); + if (m != 0u) { + uint f = mflags(m); + if ((f & F_STATIC) != 0u) { dead = true; break; } + if ((f & F_LIQUID) != 0u) { + if (kind == 2u) { dead = true; break; } // mode 0 stops at liquid (and powder) + float eff = (vel.y > -0.83 || (p.c.w & 0x100000u) != 0u) ? 0.0 : dens; // mode 1: effective density 0 (an upward speed below 50 px/s, or a draw at creation) unless moving up fast; it must be lighter than the target + if (!(eff < mdensity(m)) || budget == 0u) { dead = true; break; } + budget -= 1u; + } + } + cur = nx; + } + if (!dead && int(mlen) <= 60) { // the target cell itself + ivec2 q = ivec2(floor(target)) - ivec2(pc.view_x, pc.view_y); + if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) dead = true; + else { + uint m = cell_mat(cells[q.y * pc.gw + q.x]); + if (m != 0u) { + uint f = mflags(m); + if ((f & F_STATIC) != 0u) dead = true; + else if ((f & F_LIQUID) != 0u) { + if (kind == 2u) dead = true; + else { + float eff = (vel.y > -0.83 || (p.c.w & 0x100000u) != 0u) ? 0.0 : dens; + if (!(eff < mdensity(m)) || budget == 0u) dead = true; else budget -= 1u; + } + } + } + if (!dead) cur = target; + } + } else if (!dead) cur = target; // beyond the 60 probes the end point is taken as it is + pos = cur; + if (dead) { + if (reinsert(cw, ivec2(floor(pos)), seed, floatBitsToUint(p.b.z))) { fparts[id].b.x = 0.0; return; } + kind = 3u; // no place yet: it keeps trying + } + fparts[id].a = vec4(pos, vel); + fparts[id].c = uvec4(cw, kind, budget, (p.c.w & 0x100000u) | ((p.c.w & 0xFFFFFu) + 1u)); + ivec2 q = ivec2(floor(pos)) - ivec2(pc.view_x, pc.view_y); + if (q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh) { + vec3 col = cell_colour(cw); + uvec3 c8 = uvec3(col * 255.0 + 0.5); + entlayer[q.y * pc.gw + q.x] = 0xFF000000u | (c8.r << 16) | (c8.g << 8) | c8.b; + } +} + +void main() { + uint id = gl_GlobalInvocationID.x; + if (id >= uint(FPART_MAX)) return; + FPart p = fparts[id]; + if (p.b.x <= 0.0) return; + if (p.c.y != 0u) { displaced_particle(id, p); return; } + const bool real = p.b.y < 0.0; + vec2 pos = p.a.xy, vel = p.a.zw; + if (pc.pass_ == 1) { // between ticks: only draw, between the last two positions + pos -= vel * (1.0 - ctrl.view.z); + } else { + p.b.x -= 1.0; + bool gravDone = false; + bool landed = false; vec2 landPos = pos; int landMat = 0; + if (real) { + const int material = int(-p.b.y) - 1; + int steps = max(1, int(ceil(max(abs(vel.x), abs(vel.y))))); + vec2 prevp = pos; + bool done = false; // landed (the cell is left) + for (int s = 0; s < steps && p.b.x > 0.0; ++s) { + vec2 np = pos + vel / float(steps); + ivec2 q = ivec2(floor(np)) - ivec2(pc.view_x, pc.view_y); + if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) { p.b.x = 0.0; break; } + if (cell_mat(cells[q.y * pc.gw + q.x]) != 0u) { // landed: the material stays in the last free cell + stain_at(q, material, id + uint(pc.frame) * 7u); + landed = true; landPos = prevp; landMat = material; + p.b.x = 0.0; done = true; + break; + } + prevp = pos; + pos = np; + } + if (p.b.x <= 0.0 && !done) { landed = true; landPos = pos; landMat = material; } // the life ran out in the air: it becomes a cell that falls + } else if (p.c.x == 1u) { // a droplet that stops at the first cell it meets + const int steps = max(1, int(ceil(max(abs(vel.x), abs(vel.y))))); + for (int s = 0; s < steps; ++s) { + const vec2 np = pos + vel / float(steps); + const ivec2 q = ivec2(floor(np)) - ivec2(pc.view_x, pc.view_y); + if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh || cell_mat(cells[q.y * pc.gw + q.x]) != 0u) { + if (p.c.z > 0u && q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh) stain_at(q, int(p.c.z) - 1, id + uint(pc.frame) * 7u); + p.b.x = 0.0; break; + } + pos = np; + } + } else if ((p.c.w & 2u) != 0u || p.b.y < 0.0) { // collide_with_grid 0 + pos += vel; + } else { + // 00712390, mode 0: probes at unit steps toward the target (at most 60, int(distance + 0.5)); a liquid / powder or solid cell (not gas, fire, air) ends the move + // at the last free point. With the bounce flag (6a:20, set at creation) the velocity bounces: vy flips when |dx| <= |dy| else vx; |vx| < 10 px/s gets + // + uniform(-10, 10); both are scaled by uniform(0.1, 0.3); a life above 1 s at a speed below 40 px/s becomes uniform(0.5, 1.5) s; below 10 px/s the flag + // clears. Without it the particle dies (6a:10). Out of the loaded area: dead. (Velocities here are per frame: px/s / 60, lives in frames.) + const vec2 target = pos + vel; // (the old velocity; the forces come before the collision, so the bounce acts on the new one) + const float dist = length(vel); + const int n = min(60, int(dist + 0.5)); + const vec2 dir = dist > 0.0 ? vel / dist : vec2(0.0); + vel.y += p.b.z; gravDone = true; + vec2 cur = pos; + bool hit = false; + for (int s = 0; s < n; ++s) { + const vec2 nx = cur + dir; + const ivec2 q = ivec2(floor(nx)) - ivec2(pc.view_x, pc.view_y); + if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) { p.b.x = 0.0; break; } + const uint m = cell_mat(cells[q.y * pc.gw + q.x]); + if (m != 0u && (mflags(m) & (F_GAS | F_FIRE | F_AIR)) == 0u) { hit = true; break; } + cur = nx; + } + if (hit) { + if ((p.c.w & 4u) != 0u) p.b.x = 0.0; + else { + const uint sd = id * 13u + uint(pc.frame) * 7u; + if (abs(dir.x) <= abs(dir.y)) vel.y = -vel.y; else vel.x = -vel.x; + if (abs(vel.x) < 10.0 / 60.0) vel.x += (-10.0 + 20.0 * rnd01(hash3(sd, 1u, 71u))) / 60.0; + const float f = 0.1 + 0.2 * rnd01(hash3(sd, 2u, 72u)); + vel *= f; + const float sp2 = dot(vel, vel) * 3600.0; + if (p.b.x > 60.0 && sp2 < 1600.0) p.b.x = (0.5 + rnd01(hash3(sd, 3u, 73u))) * 60.0; + if (sp2 < 100.0) fparts[id].c.w = p.c.w | 4u; + } + pos = cur; + } else if (p.b.x > 0.0) pos = target; + } + if (!gravDone) vel += vec2(0.0, p.b.z); + if (!real && p.c.x == 0u && ((p.c.z >> 19) & 0xFFu) != 0u) { // attractor_force (00713e20): v += (attractor - pos) x dt x strength, the attractor the emitting projectile while it lives + const FProj ap = fprojs[(p.c.z >> 8) & 0x7FFu]; + if (ap.d.w != 0) vel += (ap.a.xy - pos) * float((p.c.z >> 19) & 0xFFu) / 3600.0; + } + if (!real && p.c.x == 0u && (p.c.w >> 20) != 0u) { // airflow (00713e20): angle = 3D simplex (00872560) of (target x scale, target y scale, time) x pi; v += (-sin, cos) x airflow_force (px/s, per update, no dt) + const float force = float(p.c.w >> 20) / 1000.0, scale = float((p.c.w >> 8) & 0xFFFu) / 4096.0; + const float ang = simplex3(vec3(pos * scale, float(pc.frame) / 60.0)) * 3.1415927; // (the time: the particle system's clock, here the frame / 60) + vel += vec2(-sin(ang), cos(ang)) * force / 60.0; + } + fparts[id].a = vec4(pos, vel); + fparts[id].b.x = p.b.x; + if (landed) { // 00716600 / 0071fba0: the cell goes back into the grid where the particle ended, else the first free (or lighter) cell up to 128 above, drifting + // sideways; with no place yet it waits as a returning cell (not lost: a barrel's 750 units of spill all come out) + const uint cw = uint(landMat) | (((hash3(id + uint(pc.frame) * 7u, uint(landMat), 91u) >> 16) & 0xFFu) << 16); + const uint bv = birth_frame(); + const ivec2 lq = ivec2(floor(landPos)) - ivec2(pc.view_x, pc.view_y); + if (lq.x >= 0 && lq.y >= 0 && lq.x < pc.gw && lq.y < pc.gh && !reinsert(cw, ivec2(floor(landPos)), id * 7u + uint(pc.frame), bv)) { + fparts[id].a = vec4(landPos, 0.0, 0.0); + fparts[id].b = vec4(1.0e6, 1.0, uintBitsToFloat(bv), 0.0); + fparts[id].c = uvec4(cw, 3u, 0u, 0u); + } + return; + } + } + if (p.b.x <= 0.0) return; + ivec2 q = ivec2(floor(pos)) - ivec2(pc.view_x, pc.view_y); + if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) return; + uint bits = floatBitsToUint(p.b.w); + float a = (!real && (bits & 0x01000000u) != 0u) ? clamp(p.b.x / p.b.y, 0.0, 1.0) : 1.0; + if (!real && p.c.x == 0u && (bits & 0x02000000u) == 0u) a *= 1.0 - float(p.c.z & 0xFFu) / 255.0; // the material's alpha (c.z = 255 - alpha) + if ((bits & 0x02000000u) != 0u) { // additive: the colour is scaled by the remaining life, alpha 1 marks it for render.comp + vec3 c = rgb_of(bits) * a; + uvec3 q8 = uvec3(c * 255.0 + 0.5); + entlayer[q.y * pc.gw + q.x] = 0x02000000u | (q8.r << 16) | (q8.g << 8) | q8.b; // (alpha 2: an additive particle; its glow is summed below) + // tex_glow_source_particles: every glowing particle adds its light to the glow source (the overlapping particles of a dense trail add up to a white-hot core) + const uint gt = uint((q.y >> 1) * ((pc.gw + 1) / 2) + (q.x >> 1)) * 3u; + atomicAdd(partglow[gt], uint(c.r * 256.0 + 0.5)); + atomicAdd(partglow[gt + 1u], uint(c.g * 256.0 + 0.5)); + atomicAdd(partglow[gt + 2u], uint(c.b * 256.0 + 0.5)); + } else { + entlayer[q.y * pc.gw + q.x] = (uint(a * 255.0) << 24) | (bits & 0xFFFFFFu); + } + if (!real && p.c.x == 0u && (p.c.z & 0x8000000u) != 0u && (bits & 0x02000000u) != 0u) { // draw_as_long: a streak back along the motion, as long as a frame's travel (at most 8 px), fading + const float sl = min(length(vel), 8.0); + const vec2 bd = length(vel) > 1e-4 ? -normalize(vel) : vec2(0.0); + for (int s = 1; s <= int(sl); ++s) { + const ivec2 qs = ivec2(floor(pos + bd * float(s))) - ivec2(pc.view_x, pc.view_y); + if (qs.x < 0 || qs.y < 0 || qs.x >= pc.gw || qs.y >= pc.gh) break; + const vec3 c = rgb_of(bits) * a * (1.0 - float(s) / (sl + 1.0)); + const uvec3 q8 = uvec3(c * 255.0 + 0.5); + entlayer[qs.y * pc.gw + qs.x] = 0x02000000u | (q8.r << 16) | (q8.g << 8) | q8.b; + } + } +} diff --git a/shaders/fx_state.glsl b/shaders/fx_state.glsl new file mode 100644 index 0000000..bf47361 --- /dev/null +++ b/shaders/fx_state.glsl @@ -0,0 +1,133 @@ +// State of the GPU projectile / effect system (device local, all simulated by proj_*.comp and fx_*.comp). +const int FP_MAX = 32768, FPART_MAX = 16384, FANIM_MAX = 256, FANIM_ALL = 512; // (the GPU's ring is 0..255, the host writes 256..511) +// FProj.d.z bit 30: a CPU query materialized this instance; automatic Lua yields to its twin. +struct FProj { vec4 a; vec4 b; vec4 c; ivec4 d; vec4 e; vec4 f; }; // d.w: the cast serial (0 = dead; the host's twins match it), d.z bit 10 = friendly fire of the shot; f: the wand's damage_electricity_add, damage_ice_add, knockback_force add, trail material (material + 1 | amount << 12) // e: rotation angle, angular velocity (thrown bodies) // pos, vel / damage, life left, gravity, friction / explosion radius, damage, age, bounces / type, spec, fired mask + team, alive +struct FPart { vec4 a; vec4 b; uvec4 c; }; // pos, vel / life, max life, gravity, colour bits (rgb, bit 24 = fades) +struct FAnim { vec4 a; ivec4 b; vec4 c; }; // x, y, light radius, light strength / boom, age, max age, colour bits / angle +layout(std430, set = 0, binding = 36) buffer FxState { + uint fcnt[4]; // projectile ring, particle ring, animation ring + FProj fprojs[FP_MAX]; // (src/gun/gpu_fx.h kMaxProj) + FPart fparts[16384]; + FAnim fanims[512]; +}; + +// A free record (00717400 / 007174e0: the pools return null when they are full, a live particle is never replaced): the search starts at the ring position and takes the first dead +// record of the next 64; -1 when there is none. +int alloc_part() { + const uint s0 = atomicAdd(fcnt[1], 1u); + for (uint k = 0u; k < 64u; ++k) { + const uint s = (s0 + k) % uint(FPART_MAX); + if (fparts[s].b.x <= 0.0) return int(s); + } + return -1; +} + +// colour bits: rgb, bit 24 = fades with its life, bit 25 = additive. c.w of a cosmetic: bit 1 = no grid collision (collide_with_grid 0), bit 2 = the bounce is spent +// (00712010 sets the bounce flag 6a:20 on every record; fx_particles.comp) +int emit_particle_g(vec2 pos, vec2 vel, float life, float grav, uint rgb, bool fade, bool additive, bool collide) { // the record, -1 when the pool is full + const int si = alloc_part(); + if (si < 0) return -1; + const uint s = uint(si); + fparts[s].a = vec4(pos, vel); + fparts[s].b = vec4(life, life, grav, uintBitsToFloat((rgb & 0xFFFFFFu) | (fade ? 0x01000000u : 0u) | (additive ? 0x02000000u : 0u))); + fparts[s].c = uvec4(0u, 0u, 0u, collide ? 0u : 2u); // c.z: 255 - the material's alpha (drawn opacity of a non-additive cosmetic) + return si; +} +// the cosmetic constructor 00716ce0 without force-create: the particle is made only where the grid slot is empty (and loaded) +bool cell_free_at(vec2 w) { + const ivec2 q = ivec2(floor(w)) - ivec2(pc.view_x, pc.view_y); + return q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh && cell_mat(cells[q.y * pc.gw + q.x]) == 0u; +} +void emit_particle(vec2 pos, vec2 vel, float life, float grav, uint rgb, bool fade, bool additive) { emit_particle_g(pos, vel, life, grav, rgb, fade, additive, true); } +// A droplet without a cell that ends where it meets any cell (the cosmetic blood spray): c.x = 1; with a material (c.z = material + 1) it leaves a cell of it there, so the spray feeds the pools. +uint emit_particle_hit(vec2 pos, vec2 vel, float life, float grav, uint rgb, int material) { // returns 0xFFFFFFFF when the pool is full + const int si = alloc_part(); + if (si < 0) return 0xFFFFFFFFu; + const uint s = uint(si); + fparts[s].a = vec4(pos, vel); + fparts[s].b = vec4(life, life, grav, uintBitsToFloat((rgb & 0xFFFFFFu) | 0x01000000u)); + fparts[s].c = uvec4(1u, 0u, uint(material + 1), 0u); + return s; +} +// A particle that becomes a cell of the material when it lands (blood, acid... "real" particles): max life is -(material + 1). +// the acceleration of a particle record (00712010 copies 0115499c / 011549a0, which the particle manager resets to (0, pixels_per_physics_unit x 10) = (0, 100) px/s^2, +// 007128c0 / 00712bc0); the real particles (00716af0 -> 007168c0) keep it: 100 / 3600 cells per frame^2 +const float PART_GRAV = 100.0 / 3600.0; +bool emit_material_particle(vec2 pos, vec2 vel, float life, float grav, uint rgb, int material) { // false when the pool is full + const int si = alloc_part(); + if (si < 0) return false; + const uint s = uint(si); + fparts[s].a = vec4(pos, vel); + fparts[s].b = vec4(life, -float(material + 1), grav, uintBitsToFloat((rgb & 0xFFFFFFu) | 0x04000000u)); + fparts[s].c = uvec4(0u); // a flag in the top byte keeps the bits a normal float (a zero top byte is a denormal and may be flushed) + return true; +} + +// ---- liquid taken out of the grid by an entity, a projectile or a body (docs/DISPLACEMENT_AND_FIRE.md): the cell becomes a particle that keeps its identity +// (the whole cell word) and goes back into the grid when it has landed. c.y: 1 = mode 1 (moving up, passes liquids with a penetration budget c.z), 2 = mode 0 +// (falls, stops at liquid or solid), 3 = waiting for a free place (retries every tick). +void emit_displaced(vec2 pos, vec2 vel, uint cellword, uint kind, uint birthv) { // birthv: the age (birth frame) of the cell travels with it (00717670 / 007168c0 hand over the same cell object) + const int si = alloc_part(); + if (si < 0) return; // the pool is full: the cell is gone, as in the original (the caller cleared the grid slot first) + const uint s = uint(si); + fparts[s].a = vec4(pos, vel); + fparts[s].b = vec4(1.0e6, 1.0, uintBitsToFloat(birthv), 0.0); + fparts[s].c = uvec4(cellword, kind, kind == 1u ? 20u : 0u, (hash3(s, cellword, 77u) % 101u) < 50u ? 0x100000u : 0u); // bit 20: effective density zero (an inclusive draw below 50) +} + +// Takes a liquid cell (not a powder) out of the grid and makes it a particle with the velocity v (cells per frame). Returns whether it did. +bool displace_cell(ivec2 w, vec2 v) { + ivec2 p = w - ivec2(pc.view_x, pc.view_y); + if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false; + int i = p.y * pc.gw + p.x; + uint c = cells[i]; + uint bv = birth[i]; + uint m = cell_mat(c); + if (m == 0u) return false; + uint f = mflags(m); + if ((f & F_LIQUID) == 0u || (f & (F_SAND | F_STATIC)) != 0u) return false; + if (atomicCompSwap(cells[i], c, 0u) != c) return false; // the grid slot is cleared before the particle exists + ext[i] = vec2(0.0); + emit_displaced(vec2(w) + 0.5, v, c, 1u, bv); // mode 1; it turns into mode 0 when it moves down (fx_particles.comp) + return true; +} + +// Is the cell at the world position a liquid that counts for the drag (liquid, not a powder)? +bool liquid_nonsand(ivec2 w) { + ivec2 p = w - ivec2(pc.view_x, pc.view_y); + if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false; + uint m = cell_mat(cells[p.y * pc.gw + p.x]); + if (m == 0u) return false; + uint f = mflags(m); + return (f & F_LIQCELL) != 0u && (f & F_POWDER) == 0u; // liquid and not powder (a static liquid counts too) +} + +// 0071fba0: puts a cell back into the grid. Searches from (x, y) upward to y - 128: an empty cell accepts it, a non-solid cell that is lighter is exchanged (the +// lighter cell then looks for a place of its own: it becomes a waiting particle); equal density is rejected, a solid cell ends the search; after more than +// five steps a rejection may shift the column (20 %, then 50 % right / left). +bool reinsert(uint cw, ivec2 w, uint seed, uint birthv) { + float dens = mdensity(cell_mat(cw)); + for (int step = 0; step <= 128; ++step) { + ivec2 t = ivec2(w.x, w.y - step); + ivec2 q = t - ivec2(pc.view_x, pc.view_y); + if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue; // not loaded: no random draw either + int ti = q.y * pc.gw + q.x; + uint occ = cells[ti]; + uint om = cell_mat(occ); + if (om == 0u) { + if (atomicCompSwap(cells[ti], 0u, cw) == 0u) { ext[ti] = vec2(0.0); birth[ti] = birthv; return true; } + continue; + } + uint of = mflags(om); + if ((of & F_STATIC) != 0u) return false; // a solid cell stops the search + if (mdensity(om) < dens && atomicCompSwap(cells[ti], occ, cw) == occ) { // the lighter cell is queued, not destroyed + ext[ti] = vec2(0.0); + const uint obirth = birth[ti]; + birth[ti] = birthv; + emit_displaced(vec2(t) + 0.5, vec2(0.0), occ, 3u, obirth); + return true; + } + if (step > 5 && (hash3(seed, uint(step), 61u) % 101u) < 20u) w.x += (hash3(seed, uint(step), 62u) % 101u) < 50u ? 1 : -1; + } + return false; +} diff --git a/shaders/glow.comp b/shaders/glow.comp new file mode 100644 index 0000000..96ee084 --- /dev/null +++ b/shaders/glow.comp @@ -0,0 +1,99 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 8, local_size_y = 8) in; +#include "common.glsl" + +// The glow of the materials (gfx_glow of materials.xml), as the game's data/shaders/post_glow1.frag and post_glow2.frag do it: the glow builds up over frames. +// pass 1 (post_glow1): H = 0.1 x the sum of 11 taps of the previous frame's glow, 1.5 texels apart, horizontally +// pass 2 (post_glow2): D = the sum of 11 taps of H vertically / 12.2; N = the new source x 4 ("HDR": gfx_glow 255 is the old default brightness, up to 1000); +// glow = N x 0.125 + mix(D, N, 0.05) +// In a steady area the glow settles near 3 x the source and spreads into a wide soft halo (the white core of a dense fire). Half resolution (a texel = 2 x 2 cells); +// the previous frame's glow follows the camera (its view is kept after the texels). render.comp adds it to the lights and blends it like post_final.frag. +layout(std430, set = 0, binding = 46) readonly buffer GlowTab { uint gtab[]; }; +layout(std430, set = 0, binding = 47) buffer GlowA { vec4 ga[]; }; // the glow (persistent), then one vec4: the view of the frame that wrote it +layout(std430, set = 0, binding = 48) buffer GlowB { vec4 gb[]; }; // the horizontal pass + +layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; }; +layout(std430, set = 0, binding = 21) readonly buffer EntLayer { uint entlayer[]; }; +layout(std430, set = 0, binding = 56) readonly buffer PartGlow { uint partglow[]; }; // the glowing particles of the frame added up per texel (fx_particles.comp) // the sprites and particles of the frame: the additive particles (alpha 1) are the particle glow +#define FPAL_TABLE tbl +// ---- the fire palettes of the cell blitting (main.cpp loads them after the tables; 00722490 the lookup, 00724580 the use) ---- +uint fpal_word(uint k) { return FPAL_TABLE[1024u + 9u * mats[mats.length() - 1u].w + k]; } +vec3 fpal_rgb(uint w) { return vec3(float((w >> 16) & 255u), float((w >> 8) & 255u), float(w & 255u)) / 255.0; } +vec3 fire_lookup(uint at, uint row, uint w, float t) { // the row of the image at t x width: the first / last pixel outside, else the two neighbours mixed + const float x = float(w) * t; + const uint r0 = at + row * w; + if (x >= float(w)) return fpal_rgb(fpal_word(r0 + w - 1u)); + if (x <= 0.0) return fpal_rgb(fpal_word(r0)); + const uint i0 = uint(floor(x)); + if (float(i0) == x || i0 + 1u >= w) return fpal_rgb(fpal_word(r0 + i0)); + return mix(fpal_rgb(fpal_word(r0 + i0)), fpal_rgb(fpal_word(r0 + i0 + 1u)), x - float(i0)); +} +uint fire_row(uint m) { return min(fpal_word(132u + m), 5u); } // CellData +0x68 fire_colors_index +float fire_age(uint b) { return float(birth_age(b)) + float(hash3(b & BIRTH_MASK, 77u, 5u) % 21u); } // 00709ef0: the flame's frame is the creation frame - uniform(0, 20) +vec3 fire_colour(uint m, uint b) { const float a = fire_age(b) / 40.0; return fire_lookup(0u, fire_row(m), 8u, a * a); } // colors_fire.png at (age / 40)^2 +vec3 fire_glow_colour(uint m, uint b) { const float a = fire_age(b) / 40.0; return fire_lookup(48u, fire_row(m), 8u, fire_age(b) < 15.0 ? a : a * a); } // colors_fire_glow.png (young: age / 40) +vec3 burning_colour(uint m, uint h) { return fpal_rgb(fpal_word(96u + fire_row(m) * 6u + h % 6u)); } // colors_lava.bmp: a random pixel of the row +const float TAP = 1.5; // texels between the taps (post_glow*.frag: one_per_glow_texture_size x 1.5) + +vec3 fetch(bool fromA, vec2 t, int hw, int hh) { // bilinear, zero outside + const vec2 q = t - 0.5; + const ivec2 q0 = ivec2(floor(q)); + const vec2 f = q - vec2(q0); + vec3 r = vec3(0.0); + for (int k = 0; k < 4; ++k) { + const ivec2 s = q0 + ivec2(k & 1, k >> 1); + if (s.x < 0 || s.y < 0 || s.x >= hw || s.y >= hh) continue; + const float w = ((k & 1) != 0 ? f.x : 1.0 - f.x) * ((k >> 1) != 0 ? f.y : 1.0 - f.y); + r += (fromA ? ga[s.y * hw + s.x].rgb : gb[s.y * hw + s.x].rgb) * w; + } + return r; +} + +void main() { + const int hw = (pc.gw + 1) / 2, hh = (pc.gh + 1) / 2; + const ivec2 q = ivec2(gl_GlobalInvocationID.xy); + if (pc.pass_ == 0) return; // (the source is read in pass 2) + if (q.x >= hw || q.y >= hh) return; + const vec2 t = vec2(q) + 0.5; + if (pc.pass_ == 1) { + // the previous frame's glow, moved with the camera (the view it was written in; half a texel per cell) + const vec4 pv = ga[hw * hh]; + const vec2 sh = pv.w > 0.5 ? (vec2(float(pc.view_x), float(pc.view_y)) - pv.xy) * 0.5 : vec2(1.0e6); + vec3 acc = vec3(0.0); + for (int x = -5; x <= 5; ++x) acc += fetch(true, t + sh + vec2(float(x) * TAP, 0.0), hw, hh); + gb[q.y * hw + q.x] = vec4(acc * 0.1, 1.0); + return; + } + // pass 2 + vec3 dec = vec3(0.0); + for (int y = -5; y <= 5; ++y) dec += fetch(false, t + vec2(0.0, float(y) * TAP), hw, hh); + dec *= 1.0 / 12.2; + vec3 src = vec3(0.0); // the cells of the texel: colour x gfx_glow / 1000, averaged (the cell blitting's HDR range), x 4 + for (int k = 0; k < 4; ++k) { + const ivec2 c = min(q * 2 + ivec2(k & 1, k >> 1), ivec2(pc.gw - 1, pc.gh - 1)); + const uint cw = cells[c.y * pc.gw + c.x]; + const uint m = cell_mat(cw); + if (m == 0u) continue; + const uint g = gtab[m] & 0xFFFFu; + if ((mflags(m) & F_FIRE) != 0u) { src += fire_glow_colour(m, birth[c.y * pc.gw + c.x]); continue; } // a flame: colors_fire_glow.png by its age, as it is (the blitter writes the palette colour, not scaled by gfx_glow) + if ((cw & BURN_BIT) != 0u) { src += burning_colour(m, hash3(uint(c.x + pc.view_x), uint(c.y + pc.view_y), uint(pc.frame) + 3u)) * 0.25; continue; } // a burning cell: its colour / 4 (00722340 with 0x40) + if (g == 0u) continue; + const uint mc = mats[3u * m].x; + src += vec3(float((mc >> 16) & 0xFFu), float((mc >> 8) & 0xFFu), float(mc & 0xFFu)) / 255.0 * (float(g) / 1000.0); + } + vec3 part = vec3(0.0); // post_glow2.frag: + tex_glow_source_particles x 2.5 (the glowing particles: here the additive ones of the frame, fx_particles.comp) + for (int k = 0; k < 4; ++k) { + const ivec2 c = min(q * 2 + ivec2(k & 1, k >> 1), ivec2(pc.gw - 1, pc.gh - 1)); + const uint e = entlayer[c.y * pc.gw + c.x]; + if ((e >> 24) == 1u) part += vec3(float((e >> 16) & 255u), float((e >> 8) & 255u), float(e & 255u)) / 255.0; // (additive sprites and projectiles) + else if ((e >> 24) == 253u) src += vec3(float((e >> 16) & 255u), float((e >> 8) & 255u), float(e & 255u)) / 255.0 * 0.25; // a burning pixel of a body (sprites.comp): its colour / 4, as a burning cell + } + { // the particles: all of them, added up (fx_particles.comp) + const uint gt = uint(q.y * hw + q.x) * 3u; + part += vec3(float(partglow[gt]), float(partglow[gt + 1u]), float(partglow[gt + 2u])) / 256.0; + } + const vec3 N = src * 0.25 * 4.0 + part * 0.25 * 2.5; + ga[q.y * hw + q.x] = vec4(N * 0.125 + mix(dec, N, 0.05), 1.0); + if (q.x == 0 && q.y == 0) ga[hw * hh] = vec4(float(pc.view_x), float(pc.view_y), 0.0, 1.0); // (the next frame's pass 1 reads all texels before this pass runs again) +} diff --git a/shaders/intent.comp b/shaders/intent.comp new file mode 100644 index 0000000..1a82b4d --- /dev/null +++ b/shaders/intent.comp @@ -0,0 +1,266 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 8, local_size_y = 8) in; +#include "common.glsl" + +// Movement intent. Every mobile cell decides at most one move per pass following the +// original engine's rules (noita.exe CLiquidCell / CGasCell / CFireCell updates, see +// docs/ENGINE_ANALYSIS.md). Conflicts are resolved afterwards by claim arbitration, then +// apply.comp performs the winning swaps. Several passes per frame stand in for the +// original's multi-cell steps. + +uint rnd(uint i, uint salt) { return hash3(i, uint(pc.frame) * 16u + uint(pc.pass_), salt + uint(pc.seed)); } +int rand100(uint i, uint salt) { return int(rnd(i, salt) % 101u); } // rand() % 101 +float rand01(uint i, uint salt) { return float(rnd(i, salt) & 0x7FFFu) / 32767.0; } + +bool in_bounds(ivec2 p) { return p.x >= 0 && p.y >= 0 && p.x < pc.gw && p.y < pc.gh; } +uint idx(ivec2 p) { return uint(p.y * pc.gw + p.x); } +bool is_air(uint m) { return (mflags(m) & F_AIR) != 0u; } + +// FUN_0070b200: may a liquid/powder `mm` enter a cell holding material `tm`? +bool can_enter_liquid(uint mm, uint tm, bool roll25) { + uint tf = mflags(tm); + if ((tf & F_AIR) != 0u) return true; + if ((tf & (F_GAS | F_FIRE)) != 0u) return true; // gas and fire are always displaced + if ((tf & F_STATIC) != 0u) return false; // solids are never entered + if (!roll25) return false; // dense-through-light swaps only 25% of the time + if ((mflags(mm) & F_SAND) != 0u && (tf & F_SAND) != 0u) return false; // powder never through powder + return mdensity(tm) < mdensity(mm); +} + +// FUN_0070a5a0: may a gas `mm` enter a cell holding material `tm`? +bool can_rise(uint mm, uint tm, bool roll25) { + uint tf = mflags(tm); + if ((tf & F_AIR) != 0u) return true; + if ((tf & (F_STATIC | F_LIQUID)) != 0u) return false; + if (tm == mm) return false; + return roll25 && mdensity(tm) > mdensity(mm); +} + +void main() { + ivec2 p = ivec2(gl_GlobalInvocationID.xy); + if (p.x >= pc.gw || p.y >= pc.gh) return; + uint i = idx(p); + intent[i] = uvec4(NONE, 0u, 0u, 0u); + + uint c = cells[i]; + uint m = cell_mat(c); + uint f = mflags(m); + if ((f & (F_AIR | F_STATIC)) != 0u) return; + + vec2 e = ext[i]; + float vy = e.x, vx = e.y; + float nvy = vy, nvx = vx; + uint target = NONE; + uint flip = 0u; + uint prio = ((hash3(i, uint(pc.frame) * 16u + uint(pc.pass_), 777u + uint(pc.seed)) & 0xFFFu) << 20) | i; // unique per cell (12 random bits, the cell index in 20) + bool roll25 = rand100(i, 11u) > 74; + const int P = pc.pass_; + + if ((f & F_LIQUID) != 0u) { + const bool sand = (f & F_SAND) != 0u; + { // 0070c6e0: an active liquid / powder with a positive lifetime disappears when its age (frames since it got its material) reaches it + const int lt = mlifetime(m); + if (lt > 0 && int(birth_age(birth[i])) >= lt) { cells[i] = 0u; ext[i] = vec2(0.0); return; } + } + // Powders with liquid_sticks_to_ceiling (soil, mud, moss...): 0070b2d0 attaches them to the cell on one side / above before they would fall, 0070c6e0 releases them again. + const uint ceilP = sand ? mceiling(m) : 0u; + if (ceilP > 0u) { + const ivec2 W = p + ivec2(pc.view_x, pc.view_y); + if ((c & ATTACH_BIT) != 0u) { // passive: never moves by itself + if (P == 0) { + const uint r1 = rnd(i, 91u) % 101u; + if (r1 < 20u) { // ceiling check + const uint st = (c >> 24) & 3u; + const ivec2 sp2 = p + (st == 1u ? ivec2(1, 0) : st == 3u ? ivec2(-1, 0) : ivec2(0, -1)); + bool support = false; + if (in_bounds(sp2)) { + const uint nc = cells[idx(sp2)], nm = cell_mat(nc); + support = nm != 0u && ((mflags(nm) & F_STATIC) != 0u || ((mflags(nm) & F_SAND) != 0u && (nc & ATTACH_BIT) != 0u)); + } + bool release = !support; + if (support) { + const uint r2 = rnd(i, 92u) % 101u; + const ivec2 bp = p + ivec2(0, 1); + release = r2 < ceilP && in_bounds(bp) && cell_mat(cells[idx(bp)]) == 0u; + } + if (release) cells[i] = c & ~(ATTACH_BIT | (3u << 24)); + } + } + return; + } + // attachment: one location chosen by a hash of the coordinates (0070acc0): even rows give 7 + u % 3 (7 = left, else above), odd rows 5 - u % 3 (5 = right, else above) + { + int u = W.x + 0x67577458 + 91 * W.y; + u = int((uint((u >> 13) ^ u)) * 214013u + 2531011u); + u = ((u >> ((W.x & 15) + 1)) ^ u) & 32767; + const int code = (W.y & 1) == 0 ? 7 + u % 3 : 5 - u % 3; + const ivec2 ap = p + (code == 5 ? ivec2(1, 0) : code == 7 ? ivec2(-1, 0) : ivec2(0, -1)); + if (in_bounds(ap)) { + const uint nc = cells[idx(ap)], nm = cell_mat(nc); + if (nm != 0u && ((mflags(nm) & F_STATIC) != 0u || ((mflags(nm) & F_SAND) != 0u && (nc & ATTACH_BIT) != 0u))) { + cells[i] = (c & ~(3u << 24)) | ATTACH_BIT | ((code == 5 ? 1u : code == 7 ? 3u : 2u) << 24); + ext[i] = vec2(0.0); + return; + } + } + } + } + const float g = mgravity(m); + const bool falling = vy > 0.0; + // powders that are already falling skip a frame with probability (1 - gravity) * 0.5 + if (sand && falling && rand01(i, 3u) < (1.0 - g) * 0.5) return; + + const int extra = vy > 1.0 ? int(ceil((vy - 1.0) / 2.0)) : 0; // extra cells per frame from accumulated velocity + const bool downAllowed = (P == 0) || (P <= extra); + + // drift of extra fall steps: 1/8 left, 1/8 right, else straight; liquids follow vx when it is large + int dx = 0; + if (P > 0) { + if (!sand && abs(vx) >= 1.0) dx = vx >= 0.0 ? 1 : -1; + else if (!sand) { + // 0070b2d0 / 0070b7c0: the drift is drawn afresh only when (r & 127) < 64 (slime: < 12), otherwise the previous one of this update stays; the extra passes of one frame + // replay that chain + const uint gateLimit = (f & F_SLIME) != 0u ? 12u : 64u; + for (int k = 1; k <= P; ++k) { + const uint rg = hash3(i, uint(pc.frame) * 16u + uint(k), 5u + uint(pc.seed)); + if (((rg >> 16) & 127u) < gateLimit) { + const uint r = hash3(i, uint(pc.frame) * 16u + uint(k), 6u + uint(pc.seed)) & 7u; + dx = r == 1u ? -1 : (r == 2u ? 1 : 0); + } + } + } else if ((rnd(i, 5u) & 1u) == 0u) { + uint r = rnd(i, 6u) & 7u; + dx = r == 1u ? -1 : (r == 2u ? 1 : 0); + } + } + + bool belowFree = false; + bool belowFalling = false; + ivec2 below = p + ivec2(0, 1); + if (in_bounds(below)) { + const uint bm = cell_mat(cells[idx(below)]); + belowFree = can_enter_liquid(m, bm, roll25); + // The original updates bottom-up, so a falling cell below has already moved by the time this cell + // runs. We decide from a snapshot, so wait for it instead of sliding sideways past it. + if ((mflags(bm) & F_LIQUID) != 0u) { + // already falling, or about to: it has free space of its own below it + belowFalling = ext[idx(below)].x > 0.0; + ivec2 below2 = below + ivec2(0, 1); + if (!belowFalling && in_bounds(below2)) + belowFalling = can_enter_liquid(bm, cell_mat(cells[idx(below2)]), false); + } + } + + if (downAllowed) { + ivec2 t = p + ivec2(dx, 1); + if (in_bounds(t) && can_enter_liquid(m, cell_mat(cells[idx(t)]), roll25)) { + target = idx(t); + if (P == 0) nvy = falling ? vy + g * 0.25 : 1.0e-4; // 0070b2d0: the falling state adds gravity / 4, a newly falling cell resets vy (the tiny value marks the falling state) + // 0070bdc0: after the fall the lateral loop still runs (while |vx| >= 1: a cell sideways, vx *= liquid_damping), so the sideways speed of a falling liquid + // dies within a frame or two; the GPU makes one move per pass, so the damping is applied here (a drop kept vx = +-1 from a diagonal slide and drifted + // diagonally on every extra fall step: the diagonal chains of falling water / oil) + nvx = (!sand && (f & F_SLIME) == 0u && P == 0 && abs(vx) >= 1.0) ? vx * mdamping(m) : vx; + } + } + + if (target == NONE && !belowFree && belowFalling) return; // wait for the falling cell below to move on + + if (target == NONE && P == 0) { + if (belowFree == false) nvy = 0.0; // blocked below: stop falling + if (sand) { + // diagonal slide with probability liquid_viscosity %, left first then right + int visc = mviscosity(m); + if (visc < 100 && rand100(i, 7u) < visc) { + ivec2 l = p + ivec2(-1, 1), r = p + ivec2(1, 1); + if (in_bounds(l) && can_enter_liquid(m, cell_mat(cells[idx(l)]), roll25)) target = idx(l); + else if (in_bounds(r) && can_enter_liquid(m, cell_mat(cells[idx(r)]), rand100(i, 12u) > 74)) target = idx(r); + } + } else if ((f & F_SLIME) != 0u && !belowFree) { + // 0070b7c0 (slime): the viscosity roll ends the move (no diagonal fallback), then left-down, right-down in this order, then a lateral step only when exactly one side is + // free: vx += 0.9, one move when |vx| >= 1 (vx -= 1); two free sides do nothing + int visc = mviscosity(m); + if (!(visc < 100 && rand100(i, 7u) < visc)) { + ivec2 l = p + ivec2(-1, 1), r = p + ivec2(1, 1); + if (in_bounds(l) && can_enter_liquid(m, cell_mat(cells[idx(l)]), roll25)) target = idx(l); + else if (in_bounds(r) && can_enter_liquid(m, cell_mat(cells[idx(r)]), roll25)) target = idx(r); + else { + ivec2 lp = p + ivec2(-1, 0), rp = p + ivec2(1, 0); + bool lv = in_bounds(lp), rv = in_bounds(rp); + bool lo = lv && cell_mat(cells[idx(lp)]) != 0u, ro = rv && cell_mat(cells[idx(rp)]) != 0u; + if (rv && !ro && (!lv || lo)) { nvx = vx + 0.9; if (nvx >= 1.0) { target = idx(rp); nvx -= 1.0; } } + else if (lv && !lo && (!rv || ro)) { nvx = vx - 0.9; if (nvx <= -1.0) { target = idx(lp); nvx += 1.0; } } + } + } + } else if (!belowFree) { + // lateral flow: accelerate toward the single free side, then move when |vx| reaches 1 + ivec2 lp = p + ivec2(-1, 0), rp = p + ivec2(1, 0); + bool lf = in_bounds(lp) && can_enter_liquid(m, cell_mat(cells[idx(lp)]), roll25); + bool rf = in_bounds(rp) && can_enter_liquid(m, cell_mat(cells[idx(rp)]), rand100(i, 13u) > 74); + if (lf != rf) { + nvx = vx + (rf ? mflow(m) : -mflow(m)); + if (rf && nvx >= 1.0) { target = idx(rp); nvx *= mdamping(m); } + else if (lf && nvx <= -1.0) { target = idx(lp); nvx *= mdamping(m); } + } + if (target == NONE) { + // random diagonal-down side, then the other + int dir = (rnd(i, 8u) & 1u) != 0u ? 1 : -1; + ivec2 a = p + ivec2(dir, 1), b = p + ivec2(-dir, 1); + if (in_bounds(a) && can_enter_liquid(m, cell_mat(cells[idx(a)]), roll25)) { target = idx(a); nvx = float(dir); } + else if (in_bounds(b) && can_enter_liquid(m, cell_mat(cells[idx(b)]), rand100(i, 14u) > 74)) { target = idx(b); nvx = float(-dir); } + } + } + } + + // damped ballistic lateral travel while |vx| >= 1 (one cell per pass, vx *= damping per cell) + if (target == NONE && !sand && (f & F_SLIME) == 0u && !belowFree && abs(nvx) >= 1.0) { // (slime has no inertia loop: 0070b7c0 returns) + int s = nvx >= 0.0 ? 1 : -1; + ivec2 t = p + ivec2(s, 0); + if (in_bounds(t) && can_enter_liquid(m, cell_mat(cells[idx(t)]), roll25)) { target = idx(t); nvx *= mdamping(m); } + // blocked: the velocity stays (the original keeps it until its state / activity logic changes it) + } + } else if ((f & F_GAS) != 0u) { + if (P != 0) return; // gases act once per frame + const int lifetime = mlifetime(m); + if (lifetime > 0 && vy >= float(lifetime)) { cells[i] = 0u; ext[i] = vec2(0.0); return; } // 0070a670: the move counter (0070aad0 counts the successful moves) against the lifetime + uint sp = mgasbits(m); + if (rand100(i, 21u) <= int(sp & 255u)) { + if (rand100(i, 22u) < int((sp >> 8) & 255u)) { + ivec2 t = p + ivec2(0, -1); + if (in_bounds(t) && can_rise(m, cell_mat(cells[idx(t)]), roll25)) target = idx(t); + } + if (target == NONE && rand100(i, 23u) < int((sp >> 16) & 255u)) { + int dir = (c & DIR_BIT) != 0u ? 1 : -1; + ivec2 a = p + ivec2(dir, 0), b = p + ivec2(-dir, 0); + if (in_bounds(a) && can_rise(m, cell_mat(cells[idx(a)]), roll25)) target = idx(a); + else if (in_bounds(b) && can_rise(m, cell_mat(cells[idx(b)]), rand100(i, 25u) > 74)) { target = idx(b); flip = 1u; } + } + if (target == NONE && rand100(i, 24u) < int((sp >> 24) & 255u)) { + ivec2 t = p + ivec2(0, 1); + if (in_bounds(t) && is_air(cell_mat(cells[idx(t)]))) target = idx(t); // downward only into empty cells + } + } + } else if ((f & F_FIRE) != 0u) { + if (P != 0) return; + if (rand100(i, 31u) < 35) { + ivec2 up = p + ivec2(0, -1); + if (in_bounds(up) && is_air(cell_mat(cells[idx(up)]))) target = idx(up); + else if (rand100(i, 32u) >= 35) { + int s = rand100(i, 33u) < 50 ? -1 : 1; + ivec2 a = p + ivec2(s, 0), b = p + ivec2(-s, 0); + if (in_bounds(a) && is_air(cell_mat(cells[idx(a)]))) target = idx(a); + else if (in_bounds(b) && is_air(cell_mat(cells[idx(b)]))) target = idx(b); + } + } + } + + if ((f & F_FIRE) != 0u && target == NONE && (rnd(i, 30u) & 15u) == 0u) { cells[i] = 0u; return; } // 00709fc0: the extinction test (1/16) sits at the tail: a successful move and the early returns bypass it + if ((f & F_GAS) != 0u && target != NONE) nvy = vy + 1.0; // 0070aad0: the gas age counts its successful moves + if (target != NONE) { + intent[i] = uvec4(target | (flip << 31), prio, floatBitsToUint(nvy), floatBitsToUint(nvx)); + atomicMax(claims[i], prio); + atomicMax(claims[target], prio); + } else { + ext[i] = vec2(nvy, nvx); // resting updates (velocity bookkeeping) + } +} diff --git a/shaders/light_cells.comp b/shaders/light_cells.comp new file mode 100644 index 0000000..f639fc6 --- /dev/null +++ b/shaders/light_cells.comp @@ -0,0 +1,23 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 8, local_size_y = 8) in; +#include "common.glsl" + +// The light of the entities (LightComponent, up to 128 near the window) at every cell of the window, once per frame: render.comp's lighting() reads it. +// (render.comp summed every light for every output pixel - some 36 pixels per cell in a large window - and grew slow with the lanterns and fires of a +// long session.) +layout(std430, set = 0, binding = 45) readonly buffer Lit { uvec4 lhead; vec4 lent[]; }; // count, then (x, y, radius, strength), (r, g, b, 0) per light +layout(std430, set = 0, binding = 57) buffer LitCells { uvec2 litc[]; }; // per cell: the summed light, half floats (r g, b) + +void main() { + const ivec2 cp = ivec2(gl_GlobalInvocationID.xy); + if (cp.x >= pc.gw || cp.y >= pc.gh) return; + const vec2 w = vec2(cp + ivec2(pc.view_x, pc.view_y)) + 0.5; + vec3 L = vec3(0.0); + for (uint i = 0u; i < min(lhead.x, 128u); ++i) { + const vec4 a = lent[2u * i], b = lent[2u * i + 1u]; + const float d = distance(w, a.xy) / a.z; + if (d < 1.0) L += b.rgb * (1.0 - d) * (1.0 - d) * (1.0 - d) * 1.1 * a.w; + } + litc[cp.y * pc.gw + cp.x] = uvec2(packHalf2x16(L.rg), packHalf2x16(vec2(L.b, 0.0))); +} diff --git a/shaders/lua_native_runtime.glsl b/shaders/lua_native_runtime.glsl new file mode 100644 index 0000000..5fbce6e --- /dev/null +++ b/shaders/lua_native_runtime.glsl @@ -0,0 +1,121 @@ +// Native C++ Lua bytecode target. Only self/owner handles are visible. +struct NgpValue { uint tag; float n; uint s; }; +struct NgpTuple { NgpValue v[8]; int count; }; +FProj ngp_proj; +vec3 ngp_owner; +float ngp_owner_id; +bool ngp_failed, ngp_loaded; +uint ngp_line; +uvec4 ngp_error; +vec3 ngp_surface(vec2 start,vec2 end); + +NgpValue ngp_nil() { return NgpValue(0u,0.0,0u); } +NgpValue ngp_num(float n) { return NgpValue(1u,n,0u); } +NgpValue ngp_bool(bool b) { return NgpValue(2u,b?1.0:0.0,0u); } +NgpValue ngp_string(uint s) { return NgpValue(3u,0.0,s); } +NgpTuple ngp_empty() { NgpTuple t;t.count=0;for(int i=0;i<8;i++)t.v[i]=ngp_nil();return t; } +NgpTuple ngp_one(NgpValue v) { NgpTuple t=ngp_empty();t.count=1;t.v[0]=v;return t; } +void ngp_fault(uint code) {if(!ngp_failed)ngp_error=uvec4(code,ngp_line,1u,0u);ngp_failed=true;} +bool ngp_truth(NgpValue v) {return v.tag!=0u&&!(v.tag==2u&&v.n==0.0);} +bool ngp_equal(NgpValue a,NgpValue b) {return a.tag==b.tag&&(a.tag==0u||(a.tag>=3u?a.s==b.s:a.n==b.n));} +float ngp_number(NgpValue v) { + float n;if(v.tag==1u)return v.n; + if(v.tag==3u&&ngp_parse_number(v.s,n))return n; + ngp_fault(1u);return 0.0; +} +bool ngp_less(NgpValue a,NgpValue b,bool equal) { + if(a.tag==3u&&b.tag==3u)return equal?ngp_rank(a.s)<=ngp_rank(b.s):ngp_rank(a.s)=10u&&api<=15u){ + int count=(api==12u||api==15u)?2:1; + if(a.count!=count){ngp_fault(2u);return ngp_empty();} + float x=ngp_number(a.v[0]), y=count==2?ngp_number(a.v[1]):0.0, value=0.0; + if(api==10u)value=abs(x); + if(api==11u)value=sqrt(x); + // GLSL atan(0,0) is undefined; LuaJIT/libm returns signed zero or pi. + if(api==12u){ + if(x==0.0)value=(floatBitsToUint(y)&0x80000000u)!=0u?3.141592653589793:0.0; + else if(y==0.0)value=1.5707963267948966; + else value=atan(abs(x),y); + if((floatBitsToUint(x)&0x80000000u)!=0u)value=-value; + } + if(api==13u)value=cos(x); + if(api==14u)value=sin(x); + if(api==15u)value=max(x,y); + return ngp_one(ngp_num(value)); + } + ngp_fault(3u);return ngp_empty(); +} diff --git a/shaders/lua_native_terrain.glsl b/shaders/lua_native_terrain.glsl new file mode 100644 index 0000000..bd99957 --- /dev/null +++ b/shaders/lua_native_terrain.glsl @@ -0,0 +1,15 @@ +// Same half-cell samples and last-free result as the engine's CPU raytrace. +// ngp_solid_at supplies terrain only; rigid bodies are outside this API. +bool ngp_solid_at(vec2 position); +vec3 ngp_surface(vec2 start, vec2 end) { + float distance=length(end-start); + if(isnan(distance)||isinf(distance)||distance>512.0) return vec3(start, -1.0); + int steps=max(1,int(ceil(distance*2.0))); + vec2 previous=start; + for(int k=1;k<=steps;++k) { + vec2 point=start+(end-start)*float(k)/float(steps); + if(ngp_solid_at(point))return vec3(previous,1.0); + previous=point; + } + return vec3(end,0.0); +} diff --git a/shaders/paint.comp b/shaders/paint.comp new file mode 100644 index 0000000..998edfe --- /dev/null +++ b/shaders/paint.comp @@ -0,0 +1,21 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 8, local_size_y = 8) in; +#include "common.glsl" + +void main() { + ivec2 p = ivec2(gl_GlobalInvocationID.xy); + if (p.x >= pc.gw || p.y >= pc.gh || pc.brush_mat < 0) return; + ivec2 d = p - ivec2(pc.brush_x, pc.brush_y); + if (d.x * d.x + d.y * d.y > pc.brush_r * pc.brush_r) return; + uint h = hash3(uint(p.x), uint(p.y), uint(pc.frame + pc.seed) + uint(pc.pass_) * 131u); + int i = p.y * pc.gw + p.x; + if (pc.brush_mat == 0) { cells[i] = 0u; ext[i] = vec2(0.0); return; } + // only place into empty space, and thin out fluids so they pour + if (cell_mat(cells[i]) != 0u) return; + if ((mflags(uint(pc.brush_mat)) & F_STATIC) == 0u && (h & 3u) != 0u) return; + // bits 16-23: colour variation, bit 26: initial horizontal direction of gases + cells[i] = uint(pc.brush_mat) | (((h >> 8) & 0xFFu) << 16) | (((h >> 20) & 1u) << 26); + ext[i] = vec2(0.0); + birth[i] = birth_frame(); +} diff --git a/shaders/particle_req.comp b/shaders/particle_req.comp new file mode 100644 index 0000000..005c476 --- /dev/null +++ b/shaders/particle_req.comp @@ -0,0 +1,38 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 64) in; +#include "fx_common.glsl" + +// Particles requested by the host (effects of the rigid bodies: the splash of a body that hits something, ...): count, then per request (position, velocity), (life, gravity), +// (colour bits, mode, material). Mode 0: a cosmetic droplet that ends at the first cell it meets, stains it and leaves a cell of `material`, mode 2: the same without the cell (stains only), mode 1: a real particle of the material. +struct PartReq { vec4 a; vec4 b; uvec4 c; }; +layout(std430, set = 0, binding = 51) buffer PartReqBuf { uvec4 rhead; PartReq reqs[]; }; + +void main() { + const uint i = gl_GlobalInvocationID.x; + if (i >= min(rhead.x, 1024u)) return; + const PartReq r = reqs[i]; + if (r.c.y == 0u) emit_particle_hit(r.a.xy, r.a.zw, r.b.x, r.b.y, r.c.x, int(r.c.z)); + else if (r.c.y == 2u) { const uint ps = emit_particle_hit(r.a.xy, r.a.zw, r.b.x, r.b.y, r.c.x, int(r.c.z)); if (ps != 0xFFFFFFFFu) fparts[ps].c.w = 1u; } // stains only, leaves no cell + else if (r.c.y == 3u) emit_particle(r.a.xy, r.a.zw, r.b.x, 0.0, r.c.x, true, true); // a light ray (SpriteParticleEmitterComponent, additive): fades, no cell + else if (r.c.y == 4u) { if ((r.c.w & 2u) == 0u || cell_free_at(r.a.xy)) { + // c.w bit 2: a glowing material (gfx_glow): additive, the colour scaled by the material's alpha; else bits 8-15 = 255 - alpha, the opacity + const bool glow = (r.c.w & 4u) != 0u; + const float al = 1.0 - float((r.c.w >> 8) & 0xFFu) / 255.0; + const uint rgb = glow ? ((uint(float((r.c.x >> 16) & 0xFFu) * al + 0.5) << 16) | (uint(float((r.c.x >> 8) & 0xFFu) * al + 0.5) << 8) | uint(float(r.c.x & 0xFFu) * al + 0.5)) : r.c.x; + const int ps = emit_particle_g(r.a.xy, r.a.zw, r.b.x, r.b.y, rgb, (r.c.w & 8u) == 0u, glow, (r.c.w & 1u) == 0u); // c.w bit 3: fade_based_on_lifetime 0 + if (ps >= 0 && !glow) fparts[ps].c.z = (r.c.w >> 8) & 0xFFu; + if (ps >= 0 && r.b.z > 0.0) fparts[ps].c.w |= (min(uint(r.b.z * 1000.0 + 0.5), 4095u) << 20) | (min(uint(r.b.w * 4096.0 + 0.5), 4095u) << 8); // airflow_force (b.z), airflow_scale (b.w) + } } // a cosmetic particle (ParticleEmitterComponent emit_cosmetic_particles): fades, no cell; c.w bit 0: collide_with_grid 0, bit 1: cosmetic_force_create 0 + else if (r.c.y == 5u) { // create_real_particles: the cell itself, in the empty cell (or over a gas / liquid unless only-if-space: c.w bit 1) + const ivec2 q = ivec2(floor(r.a.xy)) - ivec2(pc.view_x, pc.view_y); + if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) return; + const int ci = q.y * pc.gw + q.x; + const uint nc = r.c.z | (((hash3(uint(ci), uint(pc.frame), 63u) >> 16) & 0xFFu) << 16); + const uint old = atomicCompSwap(cells[ci], 0u, nc); + if (old == 0u) { ext[ci] = vec2(0.0); birth[ci] = birth_frame(); } + else if ((r.c.w & 2u) == 0u && (mflags(cell_mat(old)) & (F_GAS | F_LIQUID)) != 0u && (mflags(cell_mat(old)) & (F_STATIC | F_SAND)) == 0u && atomicCompSwap(cells[ci], old, nc) == old) { ext[ci] = vec2(0.0); birth[ci] = birth_frame(); } + } + else if (r.c.y == 6u) emit_displaced(r.a.xy, r.a.zw, r.c.z | (((hash3(i, uint(pc.frame), 64u) >> 16) & 0xFFu) << 16), 2u, birth_frame()); // GameCreateParticle (007b3120 -> 00716af0): a cell of the material handed to the particle system like a displaced one (AddAsParticle 007168c0): it falls and goes back into the grid, waiting for a free place + else emit_material_particle(r.a.xy, r.a.zw, r.b.x, r.b.y, r.c.x, int(r.c.z)); +} diff --git a/shaders/procgrad.glsl b/shaders/procgrad.glsl new file mode 100644 index 0000000..d1626e1 --- /dev/null +++ b/shaders/procgrad.glsl @@ -0,0 +1,119 @@ +// Shared by terrain.comp and render.comp: the 70-scaled simplex noise, the biome-map topology lookup and the gradient of the procedural biomes (FUN_0087e8d0). +// Needs: perm (binding 8), ffloor, ps, atab (areas.glsl) declared before the include. +vec2 grad12(uint g) { + const vec2 G[12] = vec2[12](vec2(1, 1), vec2(-1, 1), vec2(1, -1), vec2(-1, -1), vec2(1, 0), vec2(-1, 0), vec2(1, 0), vec2(-1, 0), vec2(0, 1), vec2(0, -1), vec2(0, 1), vec2(0, -1)); + return G[g]; +} +float simplex70(float x, float y) { + const float F2 = 0.36602540378, G2 = 0.21132486540; + float s = (x + y) * F2; + int i = ffloor(x + s), j = ffloor(y + s); + float t = float(i + j) * G2; + float x0 = x - (float(i) - t), y0 = y - (float(j) - t); + int i1 = x0 > y0 ? 1 : 0, j1 = 1 - i1; + float x1 = x0 - float(i1) + G2, y1 = y0 - float(j1) + G2; + float x2 = x0 - 1.0 + 2.0 * G2, y2 = y0 - 1.0 + 2.0 * G2; + uint ii = uint(i) & 255u, jj = uint(j) & 255u; + float n = 0.0; + float t0 = 0.5 - x0 * x0 - y0 * y0; + if (t0 >= 0.0) { t0 *= t0; n += t0 * t0 * dot(grad12(ps(ii + ps(jj)) % 12u), vec2(x0, y0)); } + float t1 = 0.5 - x1 * x1 - y1 * y1; + if (t1 >= 0.0) { t1 *= t1; n += t1 * t1 * dot(grad12(ps(ii + uint(i1) + ps(jj + uint(j1))) % 12u), vec2(x1, y1)); } + float t2 = 0.5 - x2 * x2 - y2 * y2; + if (t2 >= 0.0) { t2 *= t2; n += t2 * t2 * dot(grad12(ps(ii + 1u + ps(jj + 1u)) % 12u), vec2(x2, y2)); } + return 70.0 * n; +} +float fbm70(float x, float y) { + const vec4 M = vec4(0.84147, 0.5403, 0.5403, -0.84147); + const float K[3] = float[3](2.02, 2.33, 2.01); + float sum = simplex70(x, y) * 0.5; + float w = 0.25; + for (int o = 0; o < 3; ++o) { + float nx = (M.x * x + M.z * y) * K[o], ny = (M.y * x + M.w * y) * K[o]; + x = nx; y = ny; + sum += simplex70(x, y) * w; + w *= 0.5; + } + return sum / 0.9375; +} +// The topology record (offset in atab) of a biome-map tile, 0xFFFFFFFF when there is none (FUN_0087d870: x wraps, y is clamped). +uint tile_rec(int tx, int ty) { + int W = int(atab[2]), H = int(atab[3]); + tx = ((tx % W) + W) % W; + ty = clamp(ty, 0, H - 1); + return atab[atab[4] + uint(ty * W + tx)]; +} +// word 20 of the record: biome edge flags (bit 0 noise_biome_edges, bit 1 big_noise_biome_edges, bit 2 fat_biome_edges) +bool bedge(uint rec, uint bit) { return (atab[rec + 20u] & bit) != 0u; } +// FUN_0087d9a0: the biome-map tile whose biome a world cell belongs to. Within 42 cells of the edge of a tile whose neighbour is another biome, the lookup point is +// displaced by noise (wavy biome borders): small (2.5 cells) unless both biomes have big_noise_biome_edges (default on): 11 * simplex + 30 * sin / cos. +ivec2 border_tile(ivec2 w) { + const int ix = w.x + 17920, iy = w.y + 7168; + const int fx = ix & 511, fy = iy & 511; + const int W = int(atab[2]), H = int(atab[3]); + const int tx = ix >> 9, ty = clamp(iy >> 9, 0, H - 1); + const ivec2 here = ivec2(tx, ty); + const uint b = tile_rec(tx, ty); + if (b == 0xFFFFFFFFu || !bedge(b, 1u) || bedge(b, 4u)) return here; + ivec2 n = ivec2(-1000000); + const bool l = fx <= 41, r = fx >= 471, t = fy <= 41, bt = fy >= 471; + if (l && tile_rec(tx - 1, ty) != b) n = ivec2(tx - 1, ty); + else if (t && tile_rec(tx, ty - 1) != b) n = ivec2(tx, ty - 1); + else if (r && tile_rec(tx + 1, ty) != b) n = ivec2(tx + 1, ty); + else if (bt && tile_rec(tx, ty + 1) != b) n = ivec2(tx, ty + 1); + else if (l && t && tile_rec(tx - 1, ty - 1) != b) n = ivec2(tx - 1, ty - 1); + else if (l && bt && tile_rec(tx - 1, ty + 1) != b) n = ivec2(tx - 1, ty + 1); + else if (r && t && tile_rec(tx + 1, ty - 1) != b) n = ivec2(tx + 1, ty - 1); + else if (r && bt && tile_rec(tx + 1, ty + 1) != b) n = ivec2(tx + 1, ty + 1); + if (n.x == -1000000) return here; + const uint nb = tile_rec(n.x, n.y); + if (nb == 0xFFFFFFFFu || !bedge(nb, 1u)) return here; + const float xf = float(ix), yf = float(iy); + float xx, yy; + if (!bedge(b, 2u) || !bedge(nb, 2u)) { + const float d = simplex70(xf * 0.05, yf * 0.05) * 2.5; + xx = xf + d; yy = yf + d; + } else { + const float n11 = simplex70(xf * 0.05, yf * 0.05) * 11.0; + xx = xf + n11 + 30.0 * sin(yf * 0.005); + yy = yf + n11 + 30.0 * cos(xf * 0.005); + } + const ivec2 t2 = ivec2(int(xx) >> 9, int(yy) >> 9); + const uint r2 = tile_rec(t2.x, t2.y); + // the displaced tile must have its area loaded: otherwise the cell would be placeholder rock (a column of rock stood in the sky at the edge of the loaded region) + if (r2 != 0xFFFFFFFFu && bedge(r2, 1u) && t2.x >= 0 && t2.y >= 0 && t2.x < W && t2.y < H && atab[atab[1] + uint(t2.y) * atab[2] + uint(t2.x)] != 0u) return t2; + return here; +} +uint topo_rec(ivec2 w) { // the topology record of the biome the world cell belongs to + const ivec2 t = border_tile(w); + return tile_rec(t.x, t.y); +} +int area_of_cell(ivec2 w) { // the loaded area of that biome, -1 when none + const ivec2 t = border_tile(w); + if (t.x < 0 || t.y < 0 || t.x >= int(atab[2]) || t.y >= int(atab[3])) return -1; + return int(atab[atab[1] + uint(t.y) * atab[2] + uint(t.x)]) - 1; +} +float tf(uint rec, int k) { return uintBitsToFloat(atab[rec + uint(k)]); } +vec2 grad_limits(uint rec, float X) { // FUN_0087eaf0: the start and end of the gradient at X + float sY = tf(rec, 11), eY = tf(rec, 12); + int mode = int((atab[rec] >> 16) & 15u); + if (mode == 0) return vec2(sY, eY); + if (mode == 3) { float d = (X - tf(rec, 13)) * tf(rec, 14); return vec2(sY + d, eY + d); } + float xp = uintBitsToFloat(atab[6]) + X, sc = tf(rec, 15); // the noise offset of the world (header word 6) + float s = (mode == 2 ? fbm70(xp * sc, sY * sc) : float(simplex70(xp * sc, sY * sc))) * tf(rec, 16) + sY; + float e = float(simplex70(xp * sc, eY * sc)) * tf(rec, 17) + eY; + return vec2(s, e); +} +float proc_gradient(ivec2 w, uint rec) { // FUN_0087e8d0: 0 above the start, 1 below the end, linear between; blended with the left neighbour within 41 cells of the tile edge + float X = float(w.x), Y = float(w.y); + vec2 L = grad_limits(rec, X); + int fx = (w.x + 17920) & 511; + if (Y <= 380.0 && fx <= 41) { + ivec2 wl = ivec2(w.x - fx - 1, w.y); + uint recL = topo_rec(wl); + if (recL != 0xFFFFFFFFu && recL != rec) L = mix(L, grad_limits(recL, X), 1.0 - float(fx) / 41.0); + } + if (L.x >= Y) return 0.0; + if (Y >= L.y || L.y == L.x) return 1.0; + return (Y - L.x) / (L.y - L.x); +} diff --git a/shaders/proj_lua.comp b/shaders/proj_lua.comp new file mode 100644 index 0000000..ca6134d --- /dev/null +++ b/shaders/proj_lua.comp @@ -0,0 +1,25 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 64) in; +#include "fx_common.glsl" +#include "generated/proj_lua_generated.glsl" + +// Serial prevents a recycled projectile slot from executing a previous task. +// x=cast serial, y=scheduled frame, z=1 pending / 2 complete, w=fault code. +layout(std430, set = 0, binding = 64) buffer LuaTasks { uvec4 luaTasks[]; }; + +void main() { + uint id = gl_GlobalInvocationID.x; + if (id >= uint(FP_MAX)) return; + uvec4 task = luaTasks[id]; + if (task.z != 1u) return; + luaTasks[id].z = 2u; + FProj p = fprojs[id]; + if (task.y != uint(pc.frame) || task.x != uint(p.d.w) || p.d.w == 0 || (p.d.z & (1 << 29)) != 0) { luaTasks[id].w = 13u; return; } + int owner = int(p.e.z) - 1; + if (owner < 0 || owner >= ctrl.inp.w) { luaTasks[id].w = 3u; return; } + uvec4 fault = ngp_execute(p, vec3(ents[owner].a.xy, 0.0), float(owner + 1)); + luaTasks[id].w = fault.x; + // Bindings write only this projectile. No state races between invocations. + fprojs[id] = p; +} diff --git a/shaders/proj_lua_dispatch.glsl b/shaders/proj_lua_dispatch.glsl new file mode 100644 index 0000000..3d39819 --- /dev/null +++ b/shaders/proj_lua_dispatch.glsl @@ -0,0 +1,50 @@ +// Shared by the runtime-compiled program and the offline reference target. +layout(std430,binding=64) buffer LuaTasks { uvec4 luaTasks[]; }; +void main() { + uint id=gl_GlobalInvocationID.x; + if(id>=32768u)return; + FProj p=fprojs[id]; + uvec4 task=luaTasks[id]; + // Gather all additive fields into this target invocation. No invocation + // writes another projectile, so overlapping fields cannot race or lose force. + if(p.d.w!=0&&(p.d.z&(1<<29))==0&&p.d.x>=0&&p.d.x<512) { + uint flags=ft[p.d.x*18+11].y; + if((flags&(3u<<29))==(3u<<29)) { + uint count=luaTasks[32768].y==uint(pc.frame)?min(luaTasks[32768].x,512u):0u; + for(uint field=0u;field=0 && p.d.x<512) { + const uint flags=ft[p.d.x*18+11].y; + const uint period=(flags>>21)&255u; + // bit 30 transfers this instance to a CPU query's materialized twin. + if((flags&(1u<<20))!=0u && (p.d.z&(1<<30))==0 && period!=0u) { + if((uint(pc.frame)+uint(p.d.w))%period!=0u)return; + if(p.a.xfloat(pc.view_x+pc.gw+128)||p.a.y>float(pc.view_y+pc.gh+128))return; + if(task.x==uint(p.d.w)&&task.y==uint(pc.frame)&&task.z==2u)return; + task=uvec4(uint(p.d.w),uint(pc.frame),257u,0u); // program 1, pending + luaTasks[id]=task; + } + } + if((task.z&255u)!=1u)return; + uint program=task.z>>8; + luaTasks[id].z=2u; + if(task.y!=uint(pc.frame)||task.x!=uint(p.d.w)||p.d.w==0||(p.d.z&(1<<29))!=0){luaTasks[id].w=13u;return;} + int owner=int(p.e.z)-1; + bool known=owner>=0&&owner> 30) & 1) != 0; // the host has run the script_shot scripts on this shot: its velocity is final + const bool exact = (int(r1.y) & 4) != 0 && (fti(tb + 1).w < 0 || done); // a particle entity the host created (its velocity was drawn by SetStartVelocityComponent on the CPU): exactly that velocity + float ang = base + (rnd01(hs.x) - 0.5) * n1.x + (exact ? 0.0 : (rnd01(hs.y) - 0.5) * 2.0 * t1.z); + if (count > 1 && n1.y != 0.0) ang += -n1.y + 2.0 * n1.y * float(i) / float(n1.y < 3.1241393 ? count - 1 : count); // pattern_degrees (00c0b440): the aim of the first root projectile is turned by -pattern, then a step of 2 pattern / (N - 1, or N when the pattern is 179 degrees or more) per projectile + float speed = (t0.x + (t0.y - t0.x) * rnd01(hs.z)) * n1.z; + if ((int(r1.y) & 4) != 0) speed = length(r0.zw); // a script's shot: its own velocity + if ((fti(tb + 16).x & 256) != 0) speed = 5400.0 / 60.0; // projectile_type LIGHTNING (00c0d290 case 1): the aim direction x 5400 px/s, whatever the speed range + const int shooterSlot = int(r1.w) >> 1; + if (shooterSlot > 0 && !done && count == 1 && s0.y == 0 && fti(tb + 1).w >= 0 && (ents[shooterSlot - 1].ex8.x & 256) != 0 && ents[shooterSlot - 1].st.x != 3) { + // Message_Shot (00c0d290): the shooter's script_shot scripts see the projectile first; the host makes the runtime copy, lets them change it and shoots it again (event kind -3: two entries) + uint xi = uint(atomicAdd(xhead.x, 2)); + if (xi + 1u < 512u) { xe[xi] = vec4(r0.xy, -3.0, float(1 + type)); xe[xi + 1u] = vec4(ang, speed, base, float(shooterSlot + ((int(r1.y) & 3) << 20))); } + continue; + } + if ((fti(tb + 16).x & 512) != 0) { // projectile_type MATERIAL_PARTICLE (00c0d290 case 3): on_death_emit_particle_count real particles of on_death_emit_particle_type with the launch + // velocity and the VelocityComponent gravity, and no projectile (it is removed at once) + const int dp = fti(tb + 17).w, dmat = (dp & 0xFFFF) - 1, dn = min(dp >> 16, 64); + for (int k = 0; k < dn && dmat >= 0; ++k) emit_material_particle(r0.xy, vec2(cos(ang), sin(ang)) * speed, max(t0.w, 60.0), t1.x, mats[3u * uint(dmat)].x, dmat); + continue; + } + const uint serial = atomicAdd(fcnt[0], 1u); // the cast serial identifies the projectile beyond its ring slot + uint slot = serial % uint(FP_MAX); + for (uint k = 0u; k < 64u && fprojs[slot].d.w != 0; ++k) slot = (serial + k + 1u) % uint(FP_MAX); // (a live record is not overwritten: the next free one of 64, else the ring's) + if (fprojs[slot].d.w != 0) slot = serial % uint(FP_MAX); + FProj p; + p.a = vec4(r0.xy, cos(ang) * speed, sin(ang) * speed); + { // lifetime: -1 never runs out (the timer of 00c06fc0 skips it), lifetime_randomness: + random(-r, r) (ProjectileComponent +50) + const int lr = fti(tb + 17).z; + const float lrand = lr > 0 ? float(int(hash3(seed, uint(i), 91u) % uint(2 * lr + 1)) - lr) : 0.0; + p.b = vec4(t0.z + n2.x, t0.w < 0.0 ? 1e9 : max(1.0, t0.w + n1.w + lrand), t1.x + n2.y, t1.y); + } + p.c = vec4(ftf(tb + 2).x + n2.w, ftf(tb + 2).y + n3.x, 0.0, n2.z); + p.e = vec4(rnd01(hs.w) * 6.2832, (fti(tb + 1).w & 32) != 0 ? (rnd01(hs.z) - 0.5) * 0.5 : speed, float(int(r1.w) >> 1), float((int(r1.y) >> 3) & 0xFFFFFF)); // e.y: the spin of a thrown body, else the speed at the start (mInitialSpeed, damage_scaled_by_speed) // e.z: the slot + 1 of the shooter (0 = unknown), e.w: its herd + 1 + const int shotBits = int(n3.w); // bit 0 friendly fire, bits 1-12 trail material + 1, bits 13- its amount + p.f = vec4(n0.z, n0.w, n3.z, float(shotBits >> 1)); // the wand's electricity / ice adds, knockback add, trail + p.c.w += float(ft[tb + 7].y); // bounces_left of the projectile file + { // the perks scale the shot once, here: the projectile damage and (unless not_scaled_by_gamefx) the explosion damage + const float dm = dealt_of(int(p.e.z)); + p.b.x *= dm; + if ((fti(tb + 1).w & 32768) == 0) p.c.y *= dm; + } + p.d = ivec4(type, first + i, ((int(r1.y) & 3) << 8) | ((shotBits & 1) << 10) | (min(int(n3.y) & 8191, 4095) << 12) | (((int(n3.y) >> 13) & 15) << 24) | (n2.z > 0.0 ? (1 << 28) : 0), int(serial + 1u)); // bits 24-27: lightning_count, bit 28: the wand added bounces (00c0b440 then sets bounce_at_any_angle) // team in bits 8-9, the critical chance (percent) from bit 12 // team in bits 8-9 (1 the player's wand, 2 enemies), fired triggers in the low bits + fprojs[slot] = p; + if ((fti(tb + 11).y & 1) != 0) { // a projectile with components the GPU does not simulate: the host makes its runtime twin (event kind -4, two entries) + uint xi = uint(atomicAdd(xhead.x, 2)); + if (xi + 1u < 512u) { xe[xi] = vec4(r0.xy, -4.0, float(slot)); xe[xi + 1u] = vec4(float(serial + 1u), float(type), float(shooterSlot), float(int(r1.y) & 3)); } + } + if ((int(r1.w) & 1) != 0) { // muzzle flash of the first projectiles of a cast + ivec4 t6 = fti(tb + 6); + float fr = uf(ft[tb + 6].w); + if (fr > 0.0 && i == 0) add_anim(r0.xy, -1, min(fr, 40.0) * 0.5, 0.55, 5, uint(t6.z), 0.0); + int mb = int(ft[tb + 7].x); + if (mb >= 0 && i == 0) { // the muzzle flash sprite, along the shot + ivec4 m0 = fti(BOOM_BASE + mb * BOOM_STRIDE), m1 = fti(BOOM_BASE + mb * BOOM_STRIDE + 1); + add_anim(r0.xy, mb, 0.0, 0.0, max(m0.w, 1) * max(m1.x, 1) + 2, uint(m1.y), base); + } + // (the cast flash is the muzzle_flash_file sprite and the light only: no sparks of a colour of ours - the wall pieces of WALL_SQUARE made gold dust) + } + } + } + barrier(); + if (gl_LocalInvocationID.x == 0u) { if (list == 0) hhead.x = 0; else hhead.y = 0; } +} diff --git a/shaders/proj_step.comp b/shaders/proj_step.comp new file mode 100644 index 0000000..7b1aa04 --- /dev/null +++ b/shaders/proj_step.comp @@ -0,0 +1,722 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 64) in; +#include "fx_common.glsl" +layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; }; +#include "biome_mods.glsl" + +// Flight of the wand projectiles, one invocation per projectile: gravity, air friction, collisions with the world (die or bounce) +// and with creatures, triggers (timer, hit, death) that queue the next shot of the wand, explosions (damage, holes, material, push of rigid +// bodies, visuals), particle emitters, light and the sprite. +const int TF_COLLISION_DIE = 1, TF_EXPLODE = 2, TF_HOLE = 4, TF_ROTATE = 8, TF_ADDITIVE = 16, TF_PHYSICS = 32, TF_DIE_IN_LIQUID = 64, TF_FRIENDLY = 128, TF_NO_PLAYER = 256, TF_HIT_EFFECTS = 512; + +// The entity a projectile touches: the player or a creature / prop. The shooter's herd is spared (ex8.y of an entity holds its herd + 1) unless the projectile has friendly fire. +// What the shooter deals: the DAMAGE_MULTIPLIER game effect etc. (ex7.y of creatures and the player, set by the host) +// The critical hit of a hit: the chance (percent) of the shot, +100 with the CRITICAL_HIT_BOOST of the type while the hit entity has the condition, + what the shooter's game effects add (ex7.z); +// the damage is multiplied by 5 (DAMAGE_CRITICAL_HIT_MULTIPLIER); a chance above 100 % multiplies the multiplier by the extra, the victim's critical_damage_resistance scales it +float crit_mult(int chance, int flags, int shooter, int victim, uint seed) { + float c = float(chance); + const int cond = (flags >> 10) & 7; + if (cond != 0 && victim >= 0) { // the HitEffectComponent conditions are on the HIT entity + const Ent s = ents[victim]; + const int z = s.ex8.z; + const bool fresh = ((z >> 8) & 0xFFFFFF) + 900 > (pc.frame & 0xFFFFFF); + bool ok = false; + if (cond == 1) ok = s.ex4.w > 0.0; + else if (cond == 2) ok = fresh && (z & 4) != 0; + else if (cond == 3) ok = fresh && (z & 2) != 0; + else if (cond == 4) ok = fresh && (z & 1) != 0; + if (ok) c += 100.0; + } + if (shooter > 0) { const Ent s = ents[shooter - 1]; if (s.st.x == 1 || s.st.x == 2 || s.st.x == 3) c += s.ex7.z; } + if (c <= 0.0) return 1.0; + // 00ab9a90: rand % 100 + 1 <= chance; m = the packet's multiplier (5), x chance / 100 above 100 %; the victim's critical_damage_resistance r gives (1 - r) (m - 1) + 1, no crit at r >= 1 + const float r = victim >= 0 ? ents[victim].ex19.x : 0.0; + if (r >= 1.0 || float(hash3(seed, 17u, 99u) % 100u) >= c) return 1.0; + const float m = c > 100.0 ? 5.0 * c / 100.0 : 5.0; + return (1.0 - r) * (m - 1.0) + 1.0; +} + +// A critical hit (00ab9a90: the multiplier after the resistance above 1): the host loads data/entities/particles/critical_hit.xml at the point of the message and marks the damage number (event kind -9) +void note_crit(vec2 at, int victim) { + uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(at, -9.0, float(victim)); + if (victim >= 0) atomicOr(ents[victim].ex3.z, 0x10000); // a critical hit this tick: the victim's hit flash is red (00ab5b00: shader 6 + crit) +} + +// DAMAGE_BOOST of a hit effect: the type flags hold the stain it needs on the victim (bits 24-26, 1 + the stain bit) and the factor (bits 27-30) +float boost_of(int flags, int victim) { + const int need = (flags >> 24) & 7, mult = (flags >> 27) & 15; + if (need == 0 || mult == 0) return 1.0; + const int z = ents[victim].ex8.z; + return (((z >> (need - 1)) & 1) != 0 && ((z >> 8) & 0xFFFFFF) + 900 > (pc.frame & 0xFFFFFF)) ? float(mult) : 1.0; +} + +vec2 gAhead; // the next point of the projectile's path (a body's pixel there is a hit on its entity) +// 00c08d10: the shooter (mWhoShot) is hit only when collide_with_shooter_frames is not -1 and has run out (bits 16-23 of the flags: 255 = -1, never); friendly_fire +// (00c091f0) only lets the shooter's herd be hit. A shot with the friendly_fire modifier (PIERCING_SHOT...) gets friendly_fire 1 and 6 frames (00c0b440): shot_flags. +bool shooter_safe(int flags, int age) { const int f = (flags >> 16) & 255; return f == 255 || age < f; } +int shot_flags(int typeFlags, int dz) { return ((dz >> 10) & 1) != 0 ? (typeFlags & ~(255 << 16)) | TF_FRIENDLY | (6 << 16) : typeFlags; } +bool victim_at(vec2 pos, int shooter, int herd, int flags, int age, out int hit) { + hit = -1; + for (int k = 0; k < ctrl.inp.w; ++k) { + const int kind = ents[k].st.x; + if (kind == 0 && (ents[k].st.w & 64) != 0 && ents[k].ex19.w > 0.5) { // a worm's segment (WormComponent hitbox_radius around every part): the hit goes to the worm + const int root = int(ents[k].ex19.w) - 1; + if (root <= 0 || root >= ctrl.inp.w || !(ents[root].st.x == 1 || ents[root].st.x == 2) || (ents[root].st.w & 8) != 0) continue; + if (root == shooter - 1 && shooter_safe(flags, age)) continue; + if ((flags & TF_FRIENDLY) == 0 && !(shooter > 0 && (ents[shooter - 1].ex8.x & 4) != 0) && herd != 0 && ents[root].ex8.y == herd) continue; + if (distance(pos, ents[k].a.xy) <= ents[k].ex19.z + 1.0) { hit = root; return true; } + continue; + } + if (k == 0 ? (kind != 3 || (flags & TF_NO_PLAYER) != 0) : !(kind == 1 || kind == 2 || (kind == 4 && ents[k].hp.x < 1.0e8))) continue; + if ((ents[k].st.w & (8 | 4096)) != 0) continue; // (4096: its sprites, damage model and hitbox are switched off - Olematon away from an Evil Eye) + if (k == shooter - 1 && shooter_safe(flags, age)) continue; + if ((flags & TF_FRIENDLY) == 0 && !(shooter > 0 && (ents[shooter - 1].ex8.x & 4) != 0) && herd != 0 && ents[k].ex8.y == herd && kind != 4) continue; // (a berserk shooter has no herd: its projectiles hurt everybody) + if (kind == 4 && ents[k].ex7.w > 0.5) { if (body_slot(gAhead) == k || body_slot(pos) == k) { hit = k; return true; } continue; } // no HitboxComponent: its pixels (00c073b0) + const float r = k == 0 ? 1.0 : 2.0; + const vec2 d = pos - ents[k].a.xy; + if (d.x >= ents[k].box.x - r && d.x <= ents[k].box.y + r && d.y >= ents[k].box.z - r && d.y <= ents[k].box.w + r) { hit = k; return true; } + } + return false; +} + +bool creature_at(vec2 pos, float r, out int hit) { + hit = -1; + for (int k = 1; k < ctrl.inp.w; ++k) { + int kind = ents[k].st.x; + if (!(kind == 1 || kind == 2 || (kind == 4 && ents[k].hp.x < 1.0e8)) || (ents[k].st.w & 8) != 0) continue; + vec2 d = pos - ents[k].a.xy; + if (d.x >= ents[k].box.x - r && d.x <= ents[k].box.y + r && d.y >= ents[k].box.z - r && d.y <= ents[k].box.w + r) { hit = k; return true; } + } + return false; +} + +void queue_shot(int node, vec2 pos, vec2 vel, uint seed, int team, int shooter, int herd) { + float len = length(vel); + vec2 dir = len < 1e-6 ? vec2(0.0, 1.0) : vel / len; + int next = (pc.frame + 1) & 1; + int slot = next == 0 ? atomicAdd(hhead.x, 1) : atomicAdd(hhead.y, 1); + if (slot >= 256) return; + hcast[(next * 256 + slot) * 2] = vec4(pos, dir); // at the trigger's position itself (00c0b440 puts the new projectile there) + hcast[(next * 256 + slot) * 2 + 1] = vec4(float(node), float(team + (herd << 3)), float(seed & 0xFFFFFFu), float(shooter * 2)); +} + +// The triggers of a projectile (recorded by the natives 00b283e0 / 00b28510 / 00b28600 into three lists of the ProjectileComponent: timers +0x610, hit-world +0x61c, death +0x628; +// kinds here 1 timer, 2 hit world, 3 death). mode 1: the update's countdown (00c06c60): a timer whose delay has run out fires and goes; 2: a hit on the world (00c073b0 -> 00c06250): +// the hit-world and the waiting timers fire and go; 3: an impact on a creature (00c091f0): the hit-world and the waiting timers fire and stay (a projectile that pierces casts +// them again at its next impact); 4: the lifetime runs out (00c06fc0): the waiting timers fire; 5: the projectile is removed (00c0e3e0): the death triggers fire +void fire_triggers(int spec, inout int fired, int mode, int age, vec2 pos, vec2 vel, uint seed, int team, int shooter, int herd) { + ivec4 s0 = hspecs[spec * 4]; + for (int t = 0; t < min(s0.y, 3); ++t) { + ivec4 tr = hspecs[spec * 4 + 1 + t]; // kind, delay, node + if ((fired & (1 << t)) != 0 || tr.z < 0) continue; + // (mode 1: the counter starts at the delay and fires when the decrement reaches 0: a delay of 0 never does) + bool go = mode == 1 ? (tr.x == 1 && tr.y > 0 && age == tr.y) : (mode == 2 || mode == 3) ? (tr.x == 1 || tr.x == 2) : mode == 4 ? tr.x == 1 : tr.x == 3; + if (!go) continue; + vec2 v = vel; + if (mode == 5 && dot(vel, vel) * 3600.0 < 100.0) { // 00c0e3e0: nearly at rest (|v| < 10 px/s): a random direction (and length 20..80 px/s) for each death trigger + const float ang = rnd01(hash3(seed, uint(t), 131u)) * 6.2831855; + v = vec2(cos(ang), sin(ang)) * (rnd01(hash3(seed, uint(t), 132u)) * 60.0 + 20.0) / 60.0; + } + if (mode != 3) fired |= 1 << t; + queue_shot(mode == 2 ? tr.w : tr.z, pos, v, seed + uint(t), team, shooter, herd); // (.w: the payload with its child_speed_multiplier, 00c06250) + } +} + +void explode(vec2 pos, float radius, float damage, int type, int team, int shooter, int herd, int critChance, uint critSeed) { + int tb = type * TYPE_STRIDE; + vec4 t2 = ftf(tb + 2); + ivec4 t1 = fti(tb + 1); + int cellMat = int(ft[tb + 2].w); + float cellProb = uf(ft[tb + 3].x); + if (radius <= 0.0) return; + for (int k = 1; k < ctrl.inp.w; ++k) // corpse parts in range bleed + if (ents[k].st.x == 4 && ents[k].ex7.y > 0.0 && distance(ents[k].a.xy, pos) <= radius) body_damage(k, pos, 1.0); + // 00657960 (docs/EXPLOSION_KNOCKBACK_AND_VINES.md): every entity whose position is within explosion_radius, except the config's dont_damage_this (the shooter only when the projectile has + // explosion_dont_damage_shooter), takes the config's damage in full (only with damage_mortals; the perks scaled it at the Shot) and gets a knockback: scalar = radius * knockback_force * + // 10 * (0.5 t + 0.5), t = 1 - distance / radius clamped; its direction is the radial physics force (lerp of physics_explosion_power.min / .max by t: zero = no kick); it is compared with + // minimum_knockback_force before the division by the CharacterDataComponent mass, the vertical part is always upward, and a killing hit gives none + const vec4 t8 = ftf(tb + 8); + const bool mortals = t8.w > 0.5; + const vec2 pw = unpackHalf2x16(ft[tb + 7].w); // physics_explosion_power.min / .max + if (mortals && damage > 0.0) { + for (int k = 0; k < ctrl.inp.w; ++k) { + int kind = ents[k].st.x; + if (k == 0 ? (ents[0].st.w & 8) != 0 : (!(kind == 1 || kind == 2 || (kind == 4 && ents[k].hp.x < 1.0e8)) || (ents[k].st.w & (8 | 4096)) != 0)) continue; + if ((t1.w & 16384) != 0 && k == shooter - 1) continue; + const vec2 c = ents[k].a.xy; + const float dist = distance(c, pos); + if (dist > radius) continue; + const float ck = crit_mult(critChance, t1.w, shooter, k, critSeed + uint(k) * 7919u); + if (ck > 1.0) note_crit(c, k); + const float dk = damage * ents[k].ex9.y * ck; // the victim's explosion multiplier (and protections); the damage message carries the projectile's ConfigDamageCritical (00657960 copies chance / multiplier), TakeDamage rolls it per victim + note_block(k, damage, dk); + atomicAdd(ents[k].x.x, int(dk * 1000.0)); + note_attacker(k, shooter); + if (type >= 0) ents[k].ex19.yz = vec2(float(type + 1), float(pc.frame & 0xFFFFF)); + atomicOr(ents[k].ex3.z, 2); + const vec2 away = normalize(c - pos + vec2(0.0, -0.01)); + { // 00657960: the packet's impulse = the direction x ((power.max - power.min) x (1 - d / R) + power.min) x physics_multiplier_ragdoll_force x 3600 (the blood of the hit + // and the ragdoll take it); RAGDOLL_FX of the explosion packet: the constructor's NORMAL + const float a = ((pw.y - pw.x) * clamp(1.0 - dist / radius, 0.0, 1.0) + pw.x) * (float((fti(tb + 11).y >> 8) & 4095) / 256.0) * 3600.0; + if (k != 0) { // (not the player: its aux.x is the maximum hp - a blast there made the respawn hp 765) + atomicExchange(ents[k].aux.x, int(away.x * a * 1000.0)); atomicExchange(ents[k].aux.y, int(away.y * a * 1000.0)); + atomicAnd(ents[k].ex3.z, ~0xF000); atomicOr(ents[k].ex3.z, 1 << 12); + } + } + const float tt = clamp(1.0 - dist / radius, 0.0, 1.0); + const float physF = mix(pw.x, pw.y, tt); // (the multiplier +0x108 defaults to 1) + const float sc = radius * t8.z * 10.0 * (0.5 * tt + 0.5); + if ((kind == 1 || kind == 3) && t8.z != 0.0 && physF != 0.0 && dist > 0.0 && ents[k].hp.x - dk > 0.0 && abs(sc) >= ents[k].ex11.x && (ents[k].ex8.x & 96) == 0 && ents[k].ex8.y > 0) { // (96: KNOCKBACK or KNOCKBACK_IMMUNITY on it; ex8.y > 0: it has a GenomeDataComponent) + const float mass = intBitsToFloat(ents[k].rg2.w); + vec2 imp = (c - pos) / dist * sc; + if (mass != 0.0) imp /= mass; + imp.y = -abs(imp.y); + ents[k].ex11.yz = imp / 60.0; // the pending kick (pixels per second -> cells per frame); the entity pass adds it, inverts it when confused and starts the knockback state + } + } + } + if ((t1.w & TF_HOLE) != 0) { + if (radius <= 64.0) add_edit(vec4(pos, radius, t2.z), vec4(2.0, ftf(tb + 7).z, cellMat >= 0 ? float(cellMat) : 0.0, cellMat >= 0 ? cellProb * 100.0 : 0.0)); // angular rays (explode_rays.comp), which also create the cells of create_cell_material + else add_edit(vec4(pos, radius * 0.5, t2.z), vec4(0.0)); + } + if (((fti(tb + 9).w >> 12) & 255) != 0) { uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(pos, -2.0, float(1 + type)); } // config_explosion.load_this_entity: the host spawns the entities (event kind -2) + if (cellMat >= 0 && cellProb > 0.0 && ((t1.w & TF_HOLE) == 0 || radius > 64.0)) add_edit(vec4(pos, radius * 0.6, 0.0), vec4(1.0, float(cellMat), cellProb, 0.0)); + { // the force on the rigid bodies (00657960): the event is a pair, (x, y, radius in pixels, -1) then (force min, force max) + const uint ph = ft[tb + 7].w; + const vec2 fr = unpackHalf2x16(ph); + uint xp = uint(atomicAdd(xhead.x, 2)); + if (xp + 1u < 512u) { xe[xp] = vec4(pos, radius, -1.0); xe[xp + 1u] = vec4(fr, float(1 + type), 0.0); } // (z: the type, for the explosion's sound) + } +} + +// The sprite of a projectile, rotated along the flight when it is not round. +// scale: the entity's transform scale (a script's EntitySetTransform on the twin: summon_portal_vibrate.lua pulses the portal 0.1 .. 0.42); the host keeps it as -scale in e.x of +// a projectile that is no thrown body (e.x is the tumble angle of those only) +void draw_sprite(vec2 pos, vec2 vel, int age, ivec4 t1, ivec4 t5, ivec4 t6, float spin, float scale) { + ivec2 sz = ivec2(t5.y, t5.z); + scale = max(scale, 0.01); + if (sz.x > 0 && sz.y > 0) { + int frame = (age / max(t6.x, 1)) % max(t5.w, 1); + float ang = (t1.w & TF_PHYSICS) != 0 ? spin : ((t1.w & TF_ROTATE) != 0 ? atan(vel.y, vel.x) : 0.0); // thrown bodies tumble, long sprites follow the flight + float c = cos(ang), s = sin(ang); + float r = 0.5 * length(vec2(sz)) * scale + 1.0; + ivec2 lo = ivec2(floor(pos - vec2(r))); + int n = int(2.0 * r) + 2; + for (int j = 0; j < n * n; ++j) { + ivec2 w = lo + ivec2(j % n, j / n); + vec2 d = vec2(w) + 0.5 - pos; + ivec2 sp = ivec2(floor(vec2(sz) * 0.5 + vec2(c * d.x + s * d.y, -s * d.x + c * d.y) / scale)); + if (sp.x < 0 || sp.y < 0 || sp.x >= sz.x || sp.y >= sz.y) continue; + ivec2 q = w - ivec2(pc.view_x, pc.view_y); + if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue; + uint t = fxtex[t5.x + frame * sz.x * sz.y + sp.y * sz.x + sp.x]; + if ((t >> 24) != 0u) entlayer[q.y * pc.gw + q.x] = (t1.w & TF_ADDITIVE) != 0 ? (0x01000000u | (t & 0xFFFFFFu)) : t; // alpha 1 marks additive light + } + } else if ((t1.w & 8192) == 0) { // (an entity with only emitters has no body to draw) + ivec2 w = ivec2(floor(pos)) - ivec2(pc.view_x, pc.view_y); + for (int y = -1; y <= 1; ++y) + for (int x = -1; x <= 1; ++x) { + ivec2 q = w + ivec2(x, y); + if (q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh) entlayer[q.y * pc.gw + q.x] = 0xFF000000u | uint(t6.y); + } + } +} + +// ---- VERLET projectiles (the tentacles: projectile_type VERLET, a VerletPhysicsComponent chain in a child entity, a VerletWeaponComponent on the root) ---- +// 00c0d290 (the shot, case 2): every point of the chain starts at the origin with prev = origin - aim x speed (so it moves speed px a frame), the projectile +// itself does not move; each frame the chain integrates (00c37d30: cur + (cur - prev) x velocity_dampening, liquid_damping in a liquid; a solid cell +// stops the point halfway or lifts it one cell) and relaxes twice (00c38310, stiffness x 0.5), its first point held at the origin (follow_entity_transform; +// the weapon system 00c3fdc0 puts the chain on the shooter's hand every frame); the projectile's position is the last point. 00c3fdc0 hurts with every point: +// min(damage_coeff x |v|^2, damage_max), nothing under damage_min_step, the boxes of the creatures within damage_radius (not the shooter; the player at most once in +// 20 frames, mPlayerCooldownEnd), times (lifetime left) / fade_duration_frames in the last frames. 17 records a slot: the 16 points (position, previous), then +// (the anchor: its offset from the shooter or its position, 1 when it follows the shooter, the player's cooldown end). +// TODO(to-do #38): the stains of the cells (stain_cells_probability). +layout(std430, set = 0, binding = 63) buffer VerletPts { vec4 vpts[]; }; +const int VERLET_BASE = 13696, VERLET_STRIDE = 20; + +void verlet_draw(uint id, int vi, int n, float fadeK, uint seed) { + const int vb = VERLET_BASE + vi * VERLET_STRIDE; + const int nparts = fti(vb + 1).w; + if (nparts <= 0) return; + const uint base = id * 17u; + for (int i = 0; i < n; ++i) { + const ivec4 s = fti(vb + 4 + min(i, nparts - 1)); + const vec4 sf = ftf(vb + 4 + min(i, nparts - 1)); + if (s.x < 0) continue; + const int w = s.y & 0xFFFF, h = s.y >> 16; + if (w <= 0 || h <= 0) continue; + const vec2 a = vpts[base + uint(i)].xy; + vec2 d = i + 1 < n ? vpts[base + uint(i) + 1u].xy - a : (i > 0 ? a - vpts[base + uint(i) - 1u].xy : vec2(1.0, 0.0)); + float len = length(d); + const vec2 dir = len > 0.01 ? d / len : vec2(1.0, 0.0), nrm = vec2(-dir.y, dir.x); + if (i + 1 >= n || len < float(w)) len = max(len, float(w)); // (the last point's sprite at its own length; scale_sprite_x stretches the others along their link) + const vec2 lo = min(min(a + nrm * (-sf.w), a + nrm * (float(h) - sf.w)), min(a + dir * len + nrm * (-sf.w), a + dir * len + nrm * (float(h) - sf.w))); + const vec2 hi = max(max(a + nrm * (-sf.w), a + nrm * (float(h) - sf.w)), max(a + dir * len + nrm * (-sf.w), a + dir * len + nrm * (float(h) - sf.w))); + const ivec2 c0 = ivec2(floor(lo)) - 1, c1 = ivec2(ceil(hi)) + 1; + if (c1.x - c0.x > 64 || c1.y - c0.y > 64) continue; + for (int y = c0.y; y <= c1.y; ++y) + for (int x = c0.x; x <= c1.x; ++x) { + const vec2 rel = vec2(x, y) + 0.5 - a; + const float u = dot(rel, dir), v = dot(rel, nrm) + sf.w; + if (u < 0.0 || u >= len || v < 0.0 || v >= float(h)) continue; + const int sx = min(int(u / len * float(w)), w - 1), sy = int(v); + const uint t = fxtex[s.x + sy * w + sx]; + if ((t >> 24) == 0u) continue; + if (fadeK < 1.0 && rnd01(hash3(uint(x * 73 + y * 151), seed, 11u)) > fadeK) continue; // (the fade: pixels drop out) + const ivec2 q = ivec2(x, y) - ivec2(pc.view_x, pc.view_y); + if (q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh) entlayer[q.y * pc.gw + q.x] = t; + } + } +} + +bool verlet_blocking(vec2 p) { return solid_at(ivec2(int(floor(p.x)), int(ceil(p.y)))); } + +void verlet_step(uint id, FProj p, int tb, int vi, int age, int fired, int team, int spec, int shooter, int herd, uint seed) { + const int vb = VERLET_BASE + vi * VERLET_STRIDE; + const ivec4 v0i = fti(vb), v1i = fti(vb + 1); + const vec4 v0 = ftf(vb), v1 = ftf(vb + 1), v2 = ftf(vb + 2); + const int n = clamp(v0i.x, 2, 16); + const float rest = v0.y, stiff = v0.z, damp = v0.w; + const uint base = id * 17u; + vec2 pts[16], prv[16]; + const bool shooterOk = shooter > 0 && shooter - 1 < ctrl.inp.w && (ents[shooter - 1].st.w & 8) == 0; + if (age <= 1) { // the shot (00c0d290 case 2) + const vec2 origin = p.a.xy, v = p.a.zw * 60.0; // (the aim x the speed: speed px a frame) + for (int k = 0; k < n; ++k) { pts[k] = origin; prv[k] = origin - v; } + const bool follow = shooterOk && distance(origin, ents[shooter - 1].a.xy) < 24.0; // (cast from the hand: it follows the shooter; one a trigger made somewhere stays there) + vpts[base + 16u] = vec4(follow ? origin - ents[shooter - 1].a.xy : origin, follow ? 1.0 : 0.0, -1.0); + } else for (int k = 0; k < n; ++k) { const vec4 q = vpts[base + uint(k)]; pts[k] = q.xy; prv[k] = q.zw; } + vec4 aux = vpts[base + 16u]; + const vec2 anchor = aux.z > 0.5 ? (shooterOk ? ents[shooter - 1].a.xy + aux.xy : pts[0]) : aux.xy; + if (age > 1) { + for (int k = 1; k < n; ++k) { // 00c37d30 + vec2 delta = (pts[k] - prv[k]) * damp, np = pts[k] + delta; + if (v1i.x != 0 && verlet_blocking(np)) { + const vec2 mid = (pts[k] + np) * 0.5; + if (!verlet_blocking(mid)) np = mid; + else if (!verlet_blocking(mid - vec2(0.0, 1.0))) np = pts[k] - vec2(0.0, 1.0); + else np = pts[k]; + } else if (liquid_nonsand(ivec2(floor(np)))) np = pts[k] + (pts[k] - prv[k]) * v1.y; + prv[k] = pts[k]; pts[k] = np; + } + for (int it = 0; it < 2; ++it) // 00c38310 + for (int k = 0; k + 1 < n; ++k) { + const vec2 d = pts[k + 1] - pts[k]; + const float l = length(d); + if (l < 1e-5) continue; + const float diff = (l - rest) / max(l, 0.01) * stiff * 0.5; + pts[k] += d * diff * 0.5; pts[k + 1] -= d * diff * 0.5; + } + } + pts[0] = anchor; prv[0] = anchor; + const int lifeLeft = int(p.b.y) - age, fade = max(v1i.z, 1); + const float fadeK = lifeLeft < fade ? max(float(lifeLeft), 0.0) / float(fade) : 1.0; + // 00c3fdc0: the damage of every point + for (int k = 0; k < n; ++k) { + const vec2 v = pts[k] - prv[k]; + float dmg = min(v2.w * dot(v, v), v2.z); + if (dmg < v2.y) continue; + dmg *= fadeK; + if (dmg <= 0.0) continue; + const float r = v2.x; + for (int e = 0; e < ctrl.inp.w; ++e) { + const int kind = ents[e].st.x; + if (!(kind == 1 || kind == 2 || kind == 3) || (ents[e].st.w & (8 | 4096)) != 0 || e == shooter - 1) continue; + const vec2 rel = pts[k] - ents[e].a.xy; + if (rel.x < ents[e].box.x - r || rel.x > ents[e].box.y + r || rel.y < ents[e].box.z - r || rel.y > ents[e].box.w + r) continue; + if (kind == 3) { if (float(pc.frame) < aux.w) continue; aux.w = float(pc.frame + 20); } // mPlayerCooldownEnd + if (kind != 3 && herd != 0 && ents[e].ex8.y == herd) continue; // (the shooter's herd is spared) + atomicAdd(ents[e].x.x, int(dmg * 1000.0)); + note_attacker(e, shooter); + ents[e].ex19.yz = vec2(float(p.d.x + 1), float(pc.frame & 0xFFFFF)); + atomicOr(ents[e].ex3.z, 1); + if (e != 0) { const vec2 imp = v * 60.0 * ftf(vb + 3).x; atomicExchange(ents[e].aux.x, int(imp.x * 1000.0)); atomicExchange(ents[e].aux.y, int(imp.y * 1000.0)); } // the damage packet's impulse: impulse_coeff x the point's velocity (the blood and the ragdoll of the hit) + } + } + // 00c3fdc0: a point going back toward the shooter (its velocity toward the anchor) faster than 10 px a frame takes what is at it along: every entity whose position is within + // damage_radius of the point (not the shooter; creatures, the player, items) gets the point's velocity (VelocityComponent mVelocity, a knockback for a character) + for (int k = 1; k < n; ++k) { + const vec2 v = pts[k] - prv[k]; + if (dot(anchor - pts[k], v) <= 0.0 || dot(v, v) <= 100.0) continue; // (|v|^2 > 100: faster than 10 px a frame) + const float r = v2.x; + for (int e = 0; e < ctrl.inp.w; ++e) { + const int kind = ents[e].st.x; + if (!(kind == 1 || kind == 2 || kind == 3 || kind == 4) || (ents[e].st.w & (8 | 32 | 128 | 4096)) != 0 || e == shooter - 1) continue; // (not dead, not a rigid body, not a hovering wand, not hidden) + const vec2 d = ents[e].a.xy - pts[k]; + if (abs(d.x) > r || abs(d.y) > r) continue; + if (kind == 1 || kind == 3) ents[e].ex11.yz = v - ents[e].a.zw; // (00ab9910: a character's knockback; the entity pass adds the kick and holds it in the knockback state) + else ents[e].a.zw = v; + } + } + // 00c3fdc0: the physics bodies within physics_force_radius of a point (a Box2D query of the box) get their velocity moved toward the point's: the host applies + // v += (the point's displacement - v) x physics_impulse_coeff / 6 (event -30000 - the body's entity slot, the displacement as two halves) + if (ftf(vb + 3).z != 0.0) for (int k = 1; k < n; ++k) { + const float fr = ftf(vb + 3).y; + int bs = -1; + for (int q = 0; q < 5 && bs < 0; ++q) { + const vec2 o = q == 0 ? vec2(0.0) : (q == 1 ? vec2(fr, 0.0) : (q == 2 ? vec2(-fr, 0.0) : (q == 3 ? vec2(0.0, fr) : vec2(0.0, -fr)))); + if (body_hit(pts[k] + o)) bs = body_slot(pts[k] + o); + } + if (bs < 0) continue; + uint xi = uint(atomicAdd(xhead.x, 1)); + if (xi < 512u) xe[xi] = vec4(ftf(vb + 3).z, 0.0, -30000.0 - float(bs), uintBitsToFloat(packHalf2x16(pts[k] - prv[k]))); // (x: physics_impulse_coeff) + } + vpts[base + 16u] = aux; + const vec2 tip = pts[n - 1], tipV = pts[n - 1] - prv[n - 1]; + bool alive = true, expired = false; + if (spec >= 0) fire_triggers(spec, fired, 1, age, tip, tipV, seed, team, shooter, herd); + { // the ProjectileSystem's own hit (00c073b0 / 00c091f0) at the root, which 00c3fdc0 puts on the last point: the projectile's damage, and its hit triggers on every hit + // (00c091f0 casts the +0x61c and +0x610 lists and removes nothing: a piercing tentacle with a trigger casts its payload at each collision) + const int tflags = fti(tb + 11).y, tw = fti(tb + 1).w; + int hk; + gAhead = tip; + if (age > 1 && (tflags & 8) == 0 && ((tflags & 4) == 0 || age % max(fti(tb + 11).z & 0xFFFF, 1) == 0) + && victim_at(tip, shooter, herd, shot_flags(tw, p.d.z), age, hk)) { + const float hitDmg = hit_damage(hk, p.b.x, tb, p.f) * boost_of(tw, hk) * fadeK; + if (hitDmg > 0.0) atomicAdd(ents[hk].x.x, int(hitDmg * 1000.0)); + note_attacker(hk, shooter); + ents[hk].ex19.yz = vec2(float(p.d.x + 1), float(pc.frame & 0xFFFFF)); + atomicOr(ents[hk].ex3.z, 1); + if (spec >= 0) fire_triggers(spec, fired, 3, age, tip, tipV, seed, team, shooter, herd); + if ((tflags & 4) == 0) alive = false; + } + } + if (age >= int(p.b.y)) { alive = false; expired = true; } + if (!alive && spec >= 0) { if (expired) fire_triggers(spec, fired, 4, age, tip, tipV, seed, team, shooter, herd); fire_triggers(spec, fired, 5, age, tip, tipV, seed, team, shooter, herd); } + for (int k = 0; k < n; ++k) vpts[base + uint(k)] = vec4(pts[k], prv[k]); + if (alive) verlet_draw(id, vi, n, fadeK, seed); + p.a = vec4(tip, 0.0, 0.0); + p.c.z = float(age); + p.d.z = fired | (team << 8) | (p.d.z & ~0x3FF); + p.d.w = alive ? p.d.w : 0; + fprojs[id] = p; +} + +void main() { + uint id = gl_GlobalInvocationID.x; + if (id >= uint(FP_MAX)) return; + FProj p = fprojs[id]; + if (p.d.w == 0) return; + int type = p.d.x, tb = type * TYPE_STRIDE; + ivec4 t1 = fti(tb + 1), t3 = fti(tb + 3), t5 = fti(tb + 5), t6 = fti(tb + 6); + vec4 t4 = ftf(tb + 4); + const int vflag = fti(tb + 16).x; + if ((vflag & 4096) != 0 && pc.pass_ == 1) { // a VERLET chain between ticks: drawn as it is + const int vi = ((vflag >> 13) & 31) - 1; + const int fade = max(fti(VERLET_BASE + vi * VERLET_STRIDE + 1).z, 1), lifeLeft = int(p.b.y) - int(p.c.z); + verlet_draw(id, vi, clamp(fti(VERLET_BASE + vi * VERLET_STRIDE).x, 2, 16), lifeLeft < fade ? max(float(lifeLeft), 0.0) / float(fade) : 1.0, hash3(id, uint(pc.frame), 5u)); + return; + } + if (pc.pass_ == 1) { // between ticks: only draw, between the last two positions + draw_sprite(p.a.xy - p.a.zw * (1.0 - ctrl.view.z), p.a.zw, int(p.c.z), t1, t5, t6, p.e.x + p.e.y * ctrl.view.z, (t1.w & TF_PHYSICS) == 0 && p.e.x < 0.0 ? -p.e.x : 1.0); + return; + } + uint seed = hash3(id, uint(pc.frame), 5u); + vec2 start = p.a.xy, pos = p.a.xy, vel = p.a.zw; + { // waiting at the edge of what is loaded (far.glsl): its next position outside the window, inside the far map, in a block not resident (a biome-map tile + // without its area yet, or a block not generated yet): it stays where it is, its age frozen, and goes on from there once the block is resident + const ivec2 nw = ivec2(floor(pos + vel)); + const ivec2 lq = nw - ivec2(pc.view_x, pc.view_y); + if ((lq.x < 0 || lq.y < 0 || lq.x >= pc.gw || lq.y >= pc.gh) && far_in_map(nw) && far_slot(nw) == 0) return; + } + int age = int(p.c.z) + 1, fired = p.d.z & 255, team = (p.d.z >> 8) & 3, bounces = int(p.c.w), spec = p.d.y, shooter = int(p.e.z), herd = int(p.e.w); + if ((vflag & 4096) != 0) { verlet_step(id, p, tb, ((vflag >> 13) & 31) - 1, age, fired, team, spec, shooter, herd, seed); return; } // VERLET: the chain (verlet_step) + if ((fti(tb + 11).x & 1) != 0) { // HomingComponent (00b323f0): the nearest entity with the target tag within detect_distance (or the shooter with target_who_shot) pulls the velocity + const vec4 h = ftf(tb + 10); // detect_distance, homing_velocity_multiplier, homing_targeting_coeff, max_turn_rate + const int hf = fti(tb + 11).x, shooterSlot = int(p.e.z); + int target = -1; + float bestD = h.x * h.x; + // 00b323f0: predefined_target first (alive, not invisible), then the shooter with target_who_shot, then the nearest tagged entity; invisible ones (INVISIBILITY) never + const bool shooterAlive = shooterSlot > 0 && (ents[shooterSlot - 1].st.w & 8) == 0 && (ents[shooterSlot - 1].ex8.x & 8192) == 0; + const int pre = fti(tb + 11).w; + vec2 preAim = vec2(0.0); bool preOk = false; + if (pre > 0 && pre - 1 < ctrl.inp.w && (ents[pre - 1].st.w & 8) == 0 && (ents[pre - 1].ex8.x & 8192) == 0) target = pre - 1; + else if (pre < 0 && -pre - 1 < FP_MAX && fprojs[-pre - 1].d.w != 0) { preAim = fprojs[-pre - 1].a.xy; preOk = true; } // a projectile (homing_projectile.lua aims at other shots) + if (target >= 0 || preOk) {} + else if ((hf & 4) != 0 && shooterAlive) target = shooterSlot - 1; + else for (int k = 0; k < ctrl.inp.w; ++k) { + const int kind = ents[k].st.x; + if (!(kind == 1 || kind == 2 || kind == 3) || (ents[k].st.w & 8) != 0 || (ents[k].ex8.x & 8192) != 0 || k == shooterSlot - 1 || (ents[k].ex8.x & ((hf >> 4) & 0x1FFF)) == 0) continue; + const vec2 dd = pos - ents[k].a.xy; + const float dsq = dot(dd, dd); + if (dsq < bestD) { bestD = dsq; target = k; } + } + if (target >= 0 || preOk) { + const vec2 tp = preOk ? preAim : ents[target].a.xy + vec2(ents[target].box.x + ents[target].box.y, ents[target].box.z + ents[target].box.w) * 0.5; // the centre of its hitbox + const vec2 delta = tp - pos; + const float dist = length(delta); + if (dist < h.x) { + if ((hf & 2) == 0) { // the velocity keeps homing_velocity_multiplier of itself and gains homing_targeting_coeff x the offset x dt x (1 - distance / detect_distance), in pixels per second + const float k = max(1.0 - dist / h.x, 0.0); + vel = vel * h.y + h.z * delta * (1.0 / 60.0) * k / 60.0; + } else { // just_rotate_towards_target: the velocity turns toward the target by at most max_turn_rate + const float sp = length(vel); + const float av = sp > 0.0 ? atan(vel.y, vel.x) : 0.0, at = dist > 0.0 ? atan(delta.y, delta.x) : 0.0; + const float TAU = 6.2831855; + float A = av - at; A = A - TAU * floor(A / TAU); + const float turn = min(A, h.w); + const float ang = (TAU - A <= A) ? turn : -turn; + vel = vec2(cos(ang) * vel.x - sin(ang) * vel.y, cos(ang) * vel.y + sin(ang) * vel.x); + } + } + } + } + // VelocityComponent (00c370c0): gravity, then v -= v dt air_friction (a negative friction speeds it up), then the terminal velocity (apply_terminal_velocity, pixels per second) + const vec4 t15 = ftf(tb + 15), t16f = ftf(tb + 16), t17 = ftf(tb + 17); + const int f16 = fti(tb + 16).x; + vel.y += p.b.z * bm_gravity(pos); vel.x += t17.x; // (gravity_y x the biome's entity_gravity_y_multiplier, 00c370c0) + vel -= vel * p.b.w; + if ((f16 & 64) != 0 && (t1.w & TF_PHYSICS) == 0 && dot(vel, vel) * 3600.0 > t16f.z * t16f.z) vel = normalize(vel) * (t16f.z / 60.0); + const bool inert = ((p.d.z >> 11) & 1) != 0; // on_collision_remove_projectile: the entity stuck where it hit, without its ProjectileComponent + if (inert) vel = vec2(0.0); + const bool removed = ((p.d.z >> 29) & 1) != 0; // the host removed it (EntityKill of its twin): the component goes, nothing of the update runs + const bool wasStuck = (p.d.z & 0x800) != 0; + if (spec >= 0 && !inert && !removed) fire_triggers(spec, fired, 1, age, pos, vel, seed, team, shooter, herd); // the countdown runs before the flight of the update (00c06c60) + int steps = max(1, int(ceil(max(abs(vel.x), abs(vel.y))))); + float spin = p.e.x, omega = p.e.y; + bool alive = !removed, hitWorld = false, stuck = false, hitEntity = false; + bool touchedWorld = false; vec2 trigVel = vec2(0.0); // a world hit this frame (bounce or stop): the hit-world triggers fire with the velocity reflected about the surface (00c06250) + // a thrown physics body (the bomb) rests on its shape, not on its centre: the lowest point of its image counts as well (half the smaller side below the position) + const vec2 foot = (t1.w & TF_PHYSICS) != 0 && t5.y > 0 ? vec2(0.0, 0.5 * float(min(t5.y, t5.z))) : vec2(0.0); + const int tflags = fti(tb + 11).y; // 2: collide_with_world 0 (goes through terrain), 4: penetrate_entities (hits every damage_every_x_frames frames, does not die), 8: collide_with_entities 0 + const bool noWorld = (tflags & 2) != 0 || inert; + // penetrate_world (00c073b0): no collision with the world; when the path of the frame crosses it, the step is penetrate_world_velocity_coeff of the velocity + const bool penWorld = (f16 & 1) != 0 && !noWorld; + vec2 stepVel = vel; + if (penWorld) { + for (int i = 1; i <= steps; ++i) if (solid_at(ivec2(floor(pos + vel * (float(i) / float(steps)))))) { stepVel = vel * t15.x; break; } + } + const float gpEnergy = (f16 & 1) == 0 ? t15.y : 0.0; // ground_penetration_coeff x VelocityComponent mass + for (int i = 0; i < steps && alive; ++i) { + vec2 np = pos + stepVel / float(steps); + const bool bodyAt = body_hit(np) && (shooter <= 0 || body_slot(np) != shooter - 1); // (not the body of its shooter: a physics creature shoots from inside itself) + if (!noWorld && !penWorld && bodyAt && !solid_at(ivec2(floor(np)))) body_damage(body_slot(np), pos, p.b.x); // (the hit point: the last free position) + bool blocked = !noWorld && !penWorld && (solid_at(ivec2(floor(np))) || solid_at(ivec2(floor(np + foot))) || bodyAt); + if (blocked && gpEnergy > 0.0 && solid_at(ivec2(floor(np))) && !bodyAt) { + // ground_penetration (00c08a30, per cell of the ray): the kinetic energy coeff x mass x 0.5 x speed^2 is spent on the cell's hp; a cell that loses all of it is removed and the + // projectile flies on, slower; a cell harder than ground_penetration_max_durability_to_destroy or a speed of 10 px/s or less stops it + const ivec2 c = ivec2(floor(np)); + const ivec2 q = c - ivec2(pc.view_x, pc.view_y); + const float v2 = dot(vel, vel) * 3600.0; + if (q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh && v2 > 100.0) { + const int ci = q.y * pc.gw + q.x; + const uint m = cells[ci] & 0xFFFFu; + const float dur = uintBitsToFloat(mats[3u * m + 2u].y); + const int maxd = fti(tb + 15).z; + if (!(maxd > 0 && dur > float(maxd))) { + const float energy = gpEnergy * 0.5 * v2; + const float hp = float((mats[3u * m + 2u].z >> 20) * 50u); + vel *= 1.0 - min(hp, energy) / energy; + stepVel = vel; + if (hp - min(hp, round(energy)) < 1.0) { cells[ci] = 0u; ext[ci] = vec2(0.0); blocked = false; } + } + } + } + if (blocked) { + if (t1.w < 0 && solid_at(ivec2(floor(np)))) { vel = vec2(0.0); break; } // a particle entity (SimplePhysicsComponent, 00c17fc0): a hit on the terrain keeps the position before it and zeroes the velocity (DieIfSpeedBelow then removes it) + if ((t1.w & TF_PHYSICS) != 0) { // a thrown body: bounces with loss, rolls and comes to rest; only the fuse makes it explode + const int brk = (fti(tb + 11).z >> 16) & 0xFFFF; // a breakable one (the egg): an impact faster than its break speed kills it, it explodes (projectiles.cpp break_speed) + if (brk > 0 && length(vel) * 60.0 > float(brk)) { alive = false; hitWorld = true; break; } + bool bx = solid_at(ivec2(floor(vec2(np.x, pos.y)))) || solid_at(ivec2(floor(vec2(np.x, pos.y) + foot))), by = solid_at(ivec2(floor(vec2(pos.x, np.y)))) || solid_at(ivec2(floor(vec2(pos.x, np.y) + foot))); + if (bx || !by) { vel.x = -vel.x * 0.4; vel.y *= 0.85; omega = -omega * 0.5 + vel.y * 0.15; } + if (by || !bx) { vel.y = -vel.y * 0.4; vel.x *= 0.85; omega = omega * 0.6 + vel.x * 0.2; } + if (length(vel) < 0.25) vel = vec2(0.0); + break; + } + if (body_hit(np) && t16f.w > 0.0 && body_slot(np) >= 0) { // 00c073b0: the body hit gets ApplyLinearImpulse(the frame's displacement x physics_impulse_coeff x VelocityComponent mass / 10) at the point (event -10000 - the entity slot) + uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(np, -10000.0 - float(body_slot(np)), uintBitsToFloat(packHalf2x16(vel * t16f.w * ftf(tb + 9).y / 10.0))); + } + // the hit (00c073b0): bounces_left counts down at every hit; with some left, the velocity is reflected about the surface normal (16 rays of 5 px around the last free position, + // 0063fd80) and scaled by bounce_energy, unless the reflection points back into the wall (r.v <= 0.75, bounce_at_any_angle allows it); bounce_always then turns it around with a + // random turn of +-0.1 rad. Without a bounce the projectile stops there (velocity 0) and the death of 00c08b90 follows: on_collision_die kills it, on_collision_remove_projectile + // leaves it stuck, otherwise it stays where it stopped. + const int bl = bounces; + bounces = max(bl - 1, -1); + bool bounced = false; + vec2 nsum = vec2(0.0); int nh = 0; // the surface normal: 16 rays of 5 px around the last free position (0063fd80) + for (int k = 0; k < 16; ++k) { + const float a = float(k) * (6.2831855 / 16.0); + const vec2 d = vec2(-sin(a), cos(a)) * 5.0; + for (int s = 1; s <= 5; ++s) if (solid_at(ivec2(floor(pos + d * (float(s) / 5.0)))) || body_hit(pos + d * (float(s) / 5.0))) { nsum += d; ++nh; break; } + } + touchedWorld = true; + trigVel = (nh > 0 && dot(nsum, nsum) > 0.0) ? vel - normalize(nsum) * 2.0 * dot(normalize(nsum), vel) : -vel; + if (bl > 0) { + const vec2 v = vel * 60.0; + if (nh > 0 && dot(nsum, nsum) > 0.0) { + const vec2 n = normalize(nsum); + const vec2 r = v - n * 2.0 * dot(n, v); + if (dot(r, v) > 0.75 || (f16 & 4) != 0 || ((p.d.z >> 28) & 1) != 0) { vel = r * t15.w / 60.0; bounced = true; } + } + if (!bounced && (f16 & 2) != 0) { + const float a = rnd01(hash3(seed, uint(i), 97u)) * 0.2 - 0.1; + const vec2 rv = -vel * t15.w; + vel = vec2(cos(a) * rv.x - sin(a) * rv.y, sin(a) * rv.x + cos(a) * rv.y); + bounced = true; + } + } + if (bounced && (f16 & 2048) != 0) { uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(pos, -8.0, float(id)); } // bounce_fx_file: the host loads it at the bounce (00c073b0; event -8, the projectile's slot) + if (bounced) { pos += vel; break; } // (the position of the frame: the last free one + the new velocity x dt) + vel = vec2(0.0); + if ((t1.w & TF_COLLISION_DIE) != 0) { alive = false; hitWorld = true; } + else if ((f16 & 8) != 0) stuck = true; + break; + } + pos = np; + if (!inert && !stuck && (t1.w & TF_PHYSICS) == 0 && (tflags & 8) == 0 && ((tflags & 4) == 0 || age % max(fti(tb + 11).z & 0xFFFF, 1) == 0)) { + int hk; // the herds decide who is hurt: everything except the herd of the shooter (unless friendly fire); never the player with dont_hit_player + gAhead = pos + stepVel / float(steps); + if (victim_at(pos, shooter, herd, shot_flags(t1.w, p.d.z), age, hk)) { // (the shot's friendly_fire modifier) + float speedScale = 1.0; // damage_scaled_by_speed: speed / mInitialSpeed (or damage_scale_max_speed when > 0) + if ((f16 & 128) != 0) { const float s0 = t17.y > 0.0 ? t17.y / 60.0 : p.e.y; speedScale = s0 > 0.0 ? length(vel) / s0 : 1.0; } + const float cm = crit_mult((p.d.z >> 12) & 4095, t1.w, shooter, hk, seed); + const float hitDmg = hit_damage(hk, p.b.x * speedScale, tb, p.f) * boost_of(t1.w, hk) * cm; + if (cm > 1.0) note_crit(pos, hk); + note_block(hk, p.b.x, hitDmg); + atomicAdd(ents[hk].x.x, int(hitDmg * 1000.0)); + if (ents[hk].st.x == 4) body_damage(hk, pos, hitDmg); // a prop with a liquid inventory leaks + if (ents[hk].st.x == 4 && ents[hk].ex7.w > 0.5 && t16f.w > 0.0) { uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(gAhead, -10000.0 - float(hk), uintBitsToFloat(packHalf2x16(vel * t16f.w * ftf(tb + 9).y / 10.0))); } // and the impulse on its body + { // 00c091f0: knockback scalar = ProjectileComponent.knockback_force x speed (pixels per second) x the projectile's VelocityComponent mass; the force vector = velocity x ragdoll_force_multiplier; + // then the victim's gate (00ab9a90): abs(scalar) against minimum_knockback_force before the division by its mass, upward y, never on a killing hit + const vec4 t9 = ftf(tb + 9); + const int vk = ents[hk].st.x; + const vec2 fv = vel * 60.0 * t9.z; + const float kfh = t9.x + p.f.z; // ProjectileComponent knockback_force + the shot's knockback_force modifier + const float sc = kfh * length(vel * 60.0) * t9.y; + if (kfh != 0.0 && (vk == 1 || vk == 3) && length(fv) > 0.0 && ents[hk].hp.x - hitDmg > 0.0 && abs(sc) >= ents[hk].ex11.x && (ents[hk].ex8.x & 96) == 0 && ents[hk].ex8.y > 0) { + const float mass = intBitsToFloat(ents[hk].rg2.w); + vec2 imp = normalize(fv) * sc; + if (mass != 0.0) imp /= mass; + imp.y = -abs(imp.y); + ents[hk].ex11.yz = imp / 60.0; + } + } + note_attacker(hk, shooter); + ents[hk].ex19.yz = vec2(float(type + 1), float(pc.frame & 0xFFFFF)); // the projectile of the hit (script_damage_received's projectile_thats_responsible) + atomicOr(ents[hk].ex3.z, 1); // hit by a projectile + if ((t1.w & TF_HIT_EFFECTS) != 0) { uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(float(hk), float(shooter), -1.0, float(1 + type)); } // the hit effects of the projectile: the host applies them (victim slot, shooter slot + 1, type + 1) + if (hk != 0) { // the packet of 00c091f0: the impulse = velocity (px/s) x ragdoll_force_multiplier (the blood of the hit and the ragdoll take it), its RAGDOLL_FX + const vec2 imp = vel * 60.0 * ftf(tb + 9).z; + atomicExchange(ents[hk].aux.x, int(imp.x * 1000.0)); atomicExchange(ents[hk].aux.y, int(imp.y * 1000.0)); + atomicAnd(ents[hk].ex3.z, ~0xF000); atomicOr(ents[hk].ex3.z, ((fti(tb + 11).y >> 4) & 15) << 12); + } + if (spec >= 0) fire_triggers(spec, fired, 3, age, pos, vel, seed, team, shooter, herd); // the impact (00c091f0) + if ((tflags & 4) == 0) { alive = false; hitEntity = true; } // (a penetrating projectile flies on) + } + } + } + if (alive && t1.w < 0 && length(vel) < float(fti(tb + 9).w >> 20 & 4095) / 256.0) alive = false; // DieIfSpeedBelowComponent of a particle entity + // 00c370c0: the count N of liquid cells (not powder) in the 3x3 around the new integer position (float coordinates truncate toward zero), taken only when that position changed. + // Every candidate counts, then (displace_liquid on, 75 % of the draws) its cell becomes a particle that flies off against the motion (00717670: rotated, 0.1 x); the velocity keeps + // 1 - liquid_drag * dt * N of itself (VelocityComponent liquid_drag, unclamped), and N above liquid_death_threshold kills the projectile (liquid_death_threshold, default 0: off). + if (alive && any(notEqual(ivec2(pos), ivec2(start)))) { + int n = 0; + const ivec2 q = ivec2(pos); + for (int dy = -1; dy <= 1; ++dy) for (int dx = -1; dx <= 1; ++dx) if (liquid_nonsand(q + ivec2(dx, dy))) ++n; + if (n > 0 && fti(tb + 8).y != 0) { + for (int k = 0; k < 9; ++k) { + const ivec2 c = q + ivec2(k % 3 - 1, k / 3 - 1); + if (liquid_at_pos(vec2(c) + 0.5) == 0u || (hash3(seed, uint(k), 81u) % 101u) >= 75u) continue; + const float ang = 0.6 * float(hash3(seed, uint(k), 82u) & 0x7FFFu) / 32767.0 - 0.3; + displace_cell(c, -(mat2(cos(ang), sin(ang), -sin(ang), cos(ang)) * (0.1 * vel))); + } + } + vel *= 1.0 - ftf(tb + 8).x * (1.0 / 60.0) * float(n); + // (liquid_death_threshold of the VelocityComponent defaults to 0: no death by the count; die_on_liquid_collision is the collision with a liquid cell) + if ((t1.w & TF_DIE_IN_LIQUID) != 0 && liquid_nonsand(ivec2(pos))) { alive = false; hitWorld = true; } + } + if (alive && !inert && (f16 & 16) != 0 && (t1.w & TF_PHYSICS) == 0 && dot(vel, vel) * 3600.0 <= t16f.y * t16f.y) { // die_on_low_velocity: the death of a hit (00c08b90) + if ((t1.w & TF_COLLISION_DIE) != 0) alive = false; else if ((f16 & 8) != 0) stuck = true; + } + bool expired = false; + if (alive && !inert && !stuck && age >= int(p.b.y)) { alive = false; expired = true; } // lifetime out (the inert entity has no ProjectileComponent, so no lifetime) + if (!alive && fti(tb + 17).w != 0) { // on_death_emit_particle: on_death_emit_particle_count real particles of the material + const int dp = fti(tb + 17).w, dmat = (dp & 0xFFFF) - 1, dn = min(dp >> 16, 64); + for (int k = 0; k < dn; ++k) { + const vec2 ev = (vec2(rnd01(hash3(seed, uint(k), 93u)), rnd01(hash3(seed, uint(k), 94u))) - 0.5) * 2.0 / 60.0 * 30.0; + emit_material_particle(pos, ev, 30.0, PART_GRAV, mats[3u * uint(dmat)].x, dmat); + } + } + if (!alive && (hitWorld || hitEntity) && (f16 & 1024) != 0) { uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(pos, -6.0, float(type)); } // on_collision_spawn_entity: the host loads spawn_entity there (00c08b90) + // triggers (see fire_triggers): a hit on the world fires the hit-world and waiting timers in the reflected direction (also when the projectile bounces and lives on); the end of the + // lifetime fires the waiting timers; the removal of the projectile, whatever ended it, fires the death triggers + if (touchedWorld && spec >= 0) fire_triggers(spec, fired, 2, age, pos, trigVel, seed, team, shooter, herd); + if (stuck && !wasStuck && spec >= 0) fire_triggers(spec, fired, 5, age, pos, vel, seed, team, shooter, herd); // on_collision_remove_projectile: the ProjectileComponent goes, its removal (00c0e3e0, slot 15 of the system) casts the death triggers + if (!alive) { + if (spec >= 0) { + if (expired) fire_triggers(spec, fired, 4, age, pos, vel, seed, team, shooter, herd); + fire_triggers(spec, fired, 5, age, pos, vel, seed, team, shooter, herd); + } + if ((t1.w & TF_EXPLODE) != 0) { + explode(pos, p.c.x, p.c.y, type, team, shooter, herd, (p.d.z >> 12) & 4095, seed); + if (((p.d.z >> 24) & 15) != 0) { uint xi = uint(atomicAdd(xhead.x, 1)); if (xi < 512u) xe[xi] = vec4(pos, -7.0, float((p.d.z >> 24) & 15)); } // lightning_count: that many electricity.xml at the blast (event kind -7) + if (t3.y >= 0) boom_fx(t3.y, pos, seed, p.c.x); + } // (a projectile that does not explode leaves nothing of its own when it dies: no sparks - wall pieces absorbed by Spells To Power vanish into their red poofs only) + } + if (alive && p.f.w > 0.5) { // the shot's trail_material: real particles of the material along the path (00c0b440: an emitter with count = amount, velocity +- sqrt(amount) x 0.5) + const int tm = int(p.f.w), tmat = (tm & 4095) - 1, amount = tm >> 12; + const float tv = sqrt(float(amount)) * 0.5 / 60.0; + for (int k = 0; k < min(amount, 8); ++k) { + const vec2 ep = mix(start, pos, rnd01(hash3(seed, uint(k), 71u))); + const vec2 ev = (vec2(rnd01(hash3(seed, uint(k), 72u)), rnd01(hash3(seed, uint(k), 73u))) - 0.5) * 2.0 * tv; + emit_material_particle(ep, ev, 20.0, PART_GRAV, mats[3u * uint(tmat)].x, tmat); + } + } + if (alive) { + // particle emitters (cosmetic) + for (int e = 0; e < t3.w; ++e) { + int eb = EMIT_BASE + (t3.z + e) * EMIT_STRIDE; + ivec4 e0 = fti(eb), e2 = fti(eb + 2); + vec4 e0f = ftf(eb), e1 = ftf(eb + 1), e3 = ftf(eb + 3); + const int iv2 = max(e2.y & 0xFF, 2); // the mean emission interval in half frames (the min .. max interval of 00bc75e0 is random; 1..2 frames emits 2 times in 3 frames, not every frame) + const float away = float(e2.y >> 16) / 60.0; // velocity_always_away_from_center, cells per frame + const uint attr = uint(e2.y >> 8) & 0xFFu; // attractor_force + { const int ph = age + int(id) + e * 3; if ((ph * 2) / iv2 == (ph * 2 - 2) / iv2) continue; } + if (age < (e2.z >> 16)) continue; // delay_frames (summon_portal.xml's burst comes at frame 118) + const int c0 = e2.z & 0xFFFF; + int n = c0 + int(rnd01(hash3(seed, uint(e), 51u)) * float(e2.w - c0 + 1)); + for (int k = 0; k < n && k < 64; ++k) { // (the giga black hole's emitters make 40 - 60 a frame) + float tt = (e0.y & 1) != 0 ? rnd01(hash3(seed, uint(e * 64 + k), 52u)) : 1.0; + vec2 ep = mix(start, pos, tt); + ep += (vec2(rnd01(hash3(seed, uint(e * 64 + k), 59u)), rnd01(hash3(seed, uint(e * 64 + k), 60u))) - 0.5) * 2.0 * e3.zw; // x/y_pos_offset ranges + { // area_circle_radius (00bc75e0): a point of the disc, radius sqrt(u) between min and max (quarter pixels in bits 8-19 / 20-31 of the flags) + const float rmax = float((uint(e0.y) >> 8) & 0xFFFu) * 0.25, rmin = float((uint(e0.y) >> 20) & 0xFFFu) * 0.25; + if (rmax > 0.0) { const float rr = sqrt(rnd01(hash3(seed, uint(e * 64 + k), 61u))) * (rmax - rmin) + rmin, aa = rnd01(hash3(seed, uint(e * 64 + k), 62u)) * 6.2831853; ep += vec2(cos(aa), sin(aa)) * rr; } + } + vec2 ev = vec2(mix(e1.x, e1.y, rnd01(hash3(seed, uint(e * 64 + k), 53u))), mix(e1.z, e1.w, rnd01(hash3(seed, uint(e * 64 + k), 54u)))); + ev += (vec2(rnd01(hash3(seed, uint(k), 55u)), rnd01(hash3(seed, uint(k), 56u))) - 0.5) * 2.0 * e3.x; + if (away != 0.0) { const vec2 dd = ep - pos; const float dl = length(dd); if (dl > 1e-4) ev += dd / dl * away; } // (00bc75e0: along the way out from the emitter) + float life = max(2.0, mix(e0f.z, e0f.w, rnd01(hash3(seed, uint(e * 64 + k), 57u)))); + if ((e0.y & 4) != 0) { // create_real_particles (00bc75e0 route +0x60): the material is created in the grid at the position (velocity and lifetime are ignored) through the cell + // factory: into an empty cell, or over a gas or liquid one unless emit_only_if_there_is_space (+0x67, flag 64) + const ivec2 q = ivec2(floor(ep)) - ivec2(pc.view_x, pc.view_y); + const int mat = int(ft[eb + 3].y); + if (mat > 0 && q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh) { + const int ci = q.y * pc.gw + q.x; + const uint cur = cells[ci], cm = cell_mat(cur); + const bool free = cm == 0u || ((e0.y & 64) == 0 && (mflags(cm) & (F_GAS | F_LIQCELL)) != 0u && (mflags(cm) & F_STATIC) == 0u); + if (free && atomicCompSwap(cells[ci], cur, uint(mat) | (((hash3(seed, uint(e * 8 + k), 58u) >> 16) & 0xFFu) << 16)) == cur) { ext[ci] = vec2(0.0); birth[ci] = birth_frame(); } + } + } + else if ((e0.y & 16) == 0 || cell_free_at(ep)) { // (bit 3: collide_with_grid 0, bit 4: cosmetic_force_create 0, bit 5: a material that does not glow - blended with its alpha) + const bool blended = (e0.y & 32) != 0; + const int ps = emit_particle_g(ep, ev, life, uf(ft[eb + 2].x), uint(e0.x) & 0xFFFFFFu, (e0.y & 2) != 0, !blended, (e0.y & 8) == 0); + if (ps >= 0 && blended) fparts[ps].c.z = 255u - ((uint(e0.x) >> 24) & 0xFFu); + if (ps >= 0 && (attr != 0u || (e0.y & 128) != 0)) fparts[ps].c.z |= (id & 0x7FFu) << 8 | attr << 19 | ((e0.y & 128) != 0 ? 0x8000000u : 0u); // the attractor (this projectile, its force) and draw_as_long, for fx_particles.comp + } + } + } + if (t4.w > 0.0) add_light(pos, min(t4.w, 36.0) * 0.55, 0.32, t4.xyz); + draw_sprite(pos, vel, age, t1, t5, t6, spin, (t1.w & TF_PHYSICS) == 0 && spin < 0.0 ? -spin : 1.0); + } + if ((t1.w & TF_PHYSICS) != 0) { if (length(vel) < 0.01) omega *= 0.8; spin += omega; omega *= 0.99; } + p.e = vec4(spin, omega, p.e.z, p.e.w); // (the shooter slot + 1 and its herd stay with the projectile) + if (alive && !inert) vel *= bm_drag(pos); // 00c06c60: after the move the velocity is multiplied by the biome's projectile_drag_coeff (MOIST 0.965) + p.a = vec4(pos, vel); + p.c.z = float(age); p.c.w = float(bounces); + if (!alive && (fti(tb + 11).y & 1) != 0) { uint xi = uint(atomicAdd(xhead.x, 2)); if (xi + 1u < 512u) { xe[xi] = vec4(pos, -5.0, float(id)); xe[xi + 1u] = vec4(float(p.d.w), 0.0, 0.0, 0.0); } } // the twin's projectile died (event kind -5) + p.d.z = fired | (team << 8) | (p.d.z & ~0x3FF) | (p.d.z & 0x400) | (stuck ? 0x800 : 0); // bit 11: stuck without its ProjectileComponent (on_collision_remove_projectile); bits 12-23 crit, 24-27 lightning_count, 28 bounce at any angle stay + p.d.w = alive ? p.d.w : 0; + fprojs[id] = p; +} diff --git a/shaders/projectiles.comp b/shaders/projectiles.comp new file mode 100644 index 0000000..02486e0 --- /dev/null +++ b/shaders/projectiles.comp @@ -0,0 +1,75 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 64) in; +#include "common.glsl" + +// Projectiles shot by creatures: move in one-cell steps, die in solid cells or when they hit the player, drawn as a 3x3 blob into the entity layer. +struct Ent { vec4 a, box, mv, ai; ivec4 st; vec4 hp; ivec4 x; ivec4 rg; ivec4 rg2; ivec4 aux; vec4 ex; vec4 ex2; ivec4 ex3; vec4 ex4; vec4 ex5; vec4 ex6; vec4 ex7; ivec4 ex8; vec4 ex9; vec4 ex10; vec4 ex11; vec4 ex12, ex13, ex14, ex15, ex16, ex17, ex18, ex19; }; +struct Proj { vec4 a; vec4 b; vec4 c; }; // pos, vel / damage, life, gravity, colour bits / owner (0 creature, 1 player) +layout(std430, set = 0, binding = 21) buffer EntLayer { uint entlayer[]; }; +layout(std430, set = 0, binding = 22) buffer Ents { Ent ents[]; }; +layout(std430, set = 0, binding = 25) buffer Projs { Proj projs[]; }; +layout(std430, set = 0, binding = 23) readonly buffer Ctrl { ivec4 inp; ivec4 inp2; ivec4 inp3; vec4 outp; vec4 outp2; } ctrl; + +bool solid_at(ivec2 w) { + ivec2 p = w - ivec2(pc.view_x, pc.view_y); + if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return true; + uint m = cell_mat(cells[p.y * pc.gw + p.x]); + return m != 0u && (mflags(m) & (F_STATIC | F_SAND)) != 0u; +} + +void main() { + uint id = gl_GlobalInvocationID.x; + if (id >= 512u) return; + Proj p = projs[id]; + if (p.b.y <= 0.0) return; + if (pc.pass_ == 1) { // between ticks: only draw + ivec2 w0 = ivec2(floor(p.a.xy)) - ivec2(pc.view_x, pc.view_y); + for (int y = -1; y <= 1; ++y) + for (int x = -1; x <= 1; ++x) { + ivec2 q = w0 + ivec2(x, y); + if (q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh) entlayer[q.y * pc.gw + q.x] = floatBitsToUint(p.b.w); + } + return; + } + vec2 pos = p.a.xy, vel = p.a.zw; + vel.y += p.b.z; + int n = max(int(ceil(length(vel))), 1); + bool dead = false; + vec4 pl = ents[0].a, plbox = ents[0].box; + bool player_alive = (ents[0].st.w & 8) == 0 && ents[0].st.x == 3; + for (int i = 0; i < n && !dead; ++i) { + pos += vel / float(n); + if (solid_at(ivec2(floor(pos)))) { dead = true; break; } + if (p.c.x > 0.5) { // the player's bolt: hits the first living creature whose box contains it + for (int k = 1; k < ctrl.inp.w; ++k) { + if (ents[k].st.x == 0 || ents[k].st.x == 3 || (ents[k].st.w & 8) != 0) continue; + vec2 dk = pos - ents[k].a.xy; + if (dk.x >= ents[k].box.x - 1.0 && dk.x <= ents[k].box.y + 1.0 && dk.y >= ents[k].box.z - 1.0 && dk.y <= ents[k].box.w + 1.0) { + atomicAdd(ents[k].x.x, int(p.b.x * ents[k].ex9.x * 1000.0)); + dead = true; + break; + } + } + if (dead) break; + continue; + } + vec2 d = pos - pl.xy; + if (player_alive && d.x >= plbox.x - 1.0 && d.x <= plbox.y + 1.0 && d.y >= plbox.z - 1.0 && d.y <= plbox.w + 1.0) { + atomicAdd(ents[0].x.x, int(p.b.x * ents[0].ex9.x * 1000.0)); + dead = true; + } + } + float life = dead ? 0.0 : p.b.y - 1.0; + projs[id].a = vec4(pos, vel); + projs[id].b = vec4(p.b.x, life, p.b.z, p.b.w); + if (life > 0.0) { // draw + uint col = floatBitsToUint(p.b.w); + ivec2 w = ivec2(floor(pos)) - ivec2(pc.view_x, pc.view_y); + for (int y = -1; y <= 1; ++y) + for (int x = -1; x <= 1; ++x) { + ivec2 q = w + ivec2(x, y); + if (q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh) entlayer[q.y * pc.gw + q.x] = col; + } + } +} diff --git a/shaders/react.comp b/shaders/react.comp new file mode 100644 index 0000000..d1d9ffc --- /dev/null +++ b/shaders/react.comp @@ -0,0 +1,127 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 8, local_size_y = 8) in; +#include "common.glsl" + +// Material reactions (liquids mixing, acid, water on lava, ...). The grid is cut into disjoint pairs of neighbouring cells; pc.pass_ +// (0..3) selects the pairing: 0/1 horizontal pairs starting at even/odd columns, 2/3 vertical pairs. Both cells of a pair evaluate the +// same reaction (symmetric hash) and each applies its own side, so no cell is written twice. Four passes visit every adjacency once per frame. +layout(std430, set = 0, binding = 28) readonly buffer RHead { uint rhead[]; }; // [a * n + b] = (offset << 8) | count +layout(std430, set = 0, binding = 23) buffer Ctrl { ivec4 inp; ivec4 inp2; ivec4 inp3; vec4 outp; vec4 outp2; vec4 view; vec4 hand0; ivec4 hand1; vec4 hand2; ivec4 burnN; ivec4 burnEv[256]; ivec4 reactN; ivec4 reactEv[64]; } ctrl; // reactEv: entities to spawn (x, y, entity index + 1) +layout(std430, set = 0, binding = 29) readonly buffer RRec { uint rrec[]; }; // 5 words per entry: outs (a | b << 16, 0xFFFF = same), probability / 65536 + +void main() { + ivec2 p = ivec2(gl_GlobalInvocationID.xy); + if (p.x >= pc.gw || p.y >= pc.gh) return; + int ph = pc.pass_ & 3; + ivec2 q; + if (ph < 2) q = p + ivec2(((p.x & 1) == ph) ? 1 : -1, 0); + else q = p + ivec2(0, ((p.y & 1) == (ph - 2)) ? 1 : -1); + if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) return; + int i = p.y * pc.gw + p.x, j = q.y * pc.gw + q.x; + uint ci = cells[i], cj = cells[j]; + uint mi = cell_mat(ci), mj = cell_mat(cj); + if (mi == 0u && mj == 0u) return; + uint n = mats[mats.length() - 1u].w; + uint h = rhead[mi * n + mj]; + uint cnt = h & 255u; + if (cnt == 0u) return; + uint lo = uint(min(i, j)), hi = uint(max(i, j)); + uint rr = hash3(lo, hi, uint(pc.frame * 4 + ph) ^ 0x51u); + uint k = 0u; // the record of the pair table: R % count with the roll R that also gates the attempt (0070eaf0) + // 0070eaf0: every cell that has a reaction threshold (CellData + 0x22c: the largest probability of a reaction it is the first input of; air is no cell and never starts one) tries once per + // update with ONE random neighbour of four, rolls R (0..10000) against min(that threshold, the record's probability) and the record must accept the direction: the pair gets the chance of + // each of its two cells separately (rec / 4 for a material next to air, not rec / 2). + { + const bool iLo = i < j; + const uint mA = iLo ? mi : mj, mB = iLo ? mj : mi; + const ivec2 pA = iLo ? p : q, pB = iLo ? q : p; + const uint hA = rhead[mA * n + mB], hB = rhead[mB * n + mA]; + const uint eA = ((hA >> 8) + k % max(hA & 255u, 1u)) * 5u, eB = ((hB >> 8) + k % max(hB & 255u, 1u)) * 5u; + const uint pwA = rrec[eA + 1u], pwB = rrec[eB + 1u]; + const ivec2 dAB = pB - pA; + const uint relA = dAB.y < 0 ? 1u : dAB.y > 0 ? 2u : dAB.x < 0 ? 3u : 4u, relB = relA == 1u ? 2u : relA == 2u ? 1u : relA == 3u ? 4u : 3u; + const uint dirA = (pwA >> 16) & 7u, dirB = (pwB >> 16) & 7u; + // each cell has its own roll R = next15 % 10001: the material threshold (CellData + 0x22c, positive) and the record's threshold both accept R <= threshold, the record is R % count + const uint RA = (hash(rr ^ 0xA2u) & 0x7FFFu) % 10001u, RB = (hash(rr ^ 0xB2u) & 0x7FFFu) % 10001u; + const uint kA = RA % max(hA & 255u, 1u), kB = RB % max(hB & 255u, 1u); + const uint eA2 = ((hA >> 8) + kA) * 5u, eB2 = ((hB >> 8) + kB) * 5u; + const uint matA = rhead[n * n + mA], matB = rhead[n * n + mB]; + const bool fireA = mA != 0u && (hA & 255u) != 0u && matA > 0u && RA <= matA && RA <= (rrec[eA2 + 1u] & 0xFFFFu) && (((rrec[eA2 + 1u] >> 16) & 7u) == 0u || ((rrec[eA2 + 1u] >> 16) & 7u) == relA) && (hash(rr ^ 0xA1u) & 3u) == 0u; + const bool fireB = mB != 0u && (hB & 255u) != 0u && matB > 0u && RB <= matB && RB <= (rrec[eB2 + 1u] & 0xFFFFu) && (((rrec[eB2 + 1u] >> 16) & 7u) == 0u || ((rrec[eB2 + 1u] >> 16) & 7u) == relB) && (hash(rr ^ 0xB1u) & 3u) == 0u; + if (!fireA && !fireB) return; + k = fireA ? kA : kB; + } + uint e = ((h >> 8) + k % cnt) * 5u; + uint outs = rrec[e]; + uint third = rrec[e + 2u]; + uint blob = rrec[e + 3u]; + uint entityIdx = rrec[e + 4u] & 0x7FFFFFFFu; + bool second = (mi == mj) && (i > j); // equal materials: the lower cell takes the first output + uint o = second ? (outs >> 16) : (outs & 0xFFFFu); + const bool firstIsI0 = (rrec[e + 4u] & 0x80000000u) == 0u; // (the record is the mirrored one when the first input is the second material of the pair) + const uint reqLife = blob >> 20; + if (reqLife > 0u && birth_age(birth[firstIsI0 ? i : j]) < reqLife) return; // req_lifetime: the first cell must be old enough + if (((blob >> 19) & 1u) != 0u && firstIsI0 && !second) { // destroy_horizontally_lonely_pixels: the first output becomes air when both horizontal neighbours are empty (both probed) + const bool le = p.x > 0 && cell_mat(cells[i - 1]) == 0u, ri = p.x + 1 < pc.gw && cell_mat(cells[i + 1]) == 0u; + if (le && ri) o = 0u; + } + // three inputs (0070ebf0): a third cell next to the first input's cell (a cardinal neighbour chosen at random; the second cell's place is mirrored to the other side) + // must hold the third material; the thread of the first cell writes its output + if ((third & 0xFFFFu) != 0xFFFFu) { + const bool firstIsI = firstIsI0; + const ivec2 pf = firstIsI ? p : q, ps = firstIsI ? q : p; + const ivec2 dirs[4] = ivec2[4](ivec2(0, -1), ivec2(-1, 0), ivec2(1, 0), ivec2(0, 1)); + ivec2 p3 = pf + dirs[(rr >> 20) & 3u]; + if (p3 == ps) p3 = 2 * pf - ps; + if (p3.x < 0 || p3.y < 0 || p3.x >= pc.gw || p3.y >= pc.gh) return; + const int k3 = p3.y * pc.gw + p3.x; + const uint c3 = cells[k3]; + if (cell_mat(c3) != (third & 0xFFFFu)) return; + const uint o3 = (third >> 16) & 0xFFFFu; + if (firstIsI && o3 != 0xFFFFu && o3 != cell_mat(c3)) { atomicCompSwap(cells[k3], c3, o3 | (c3 & 0x00FF0000u)); birth[k3] = birth_frame(); } + } + if (o == 0xFFFFu || o == mi) return; // this side stays as it is + cells[i] = o | (ci & 0x00FF0000u); // keeps the colour variation, clears burning and direction bits + ext[i] = vec2(0.0); + birth[i] = birth_frame(); + if (entityIdx != 0u && firstIsI0 && !second) { // the reaction loads an entity at the position of the first input (the host spawns it) + const int ei = atomicAdd(ctrl.reactN.x, 1); + if (ei < 64) ctrl.reactEv[ei] = ivec4(p.x + pc.view_x, p.y + pc.view_y, int(entityIdx), 0); + else atomicAdd(ctrl.reactN.x, -1); + } + // blob_radius with blob_restrict_to_input_material (0071ab50 conditional mode): trunc(r * 2 pi + 0.5) rays of unit steps from the cell; a cell of the input material converts, + // one that already holds the output lets the ray go on, anything else ends it (a flood of the connected input material up to the radius) + { + const uint rad = second ? ((blob >> 8) & 255u) : (blob & 255u); + const bool restricted = ((blob >> (second ? 17 : 16)) & 1u) != 0u; + if (rad > 0u && restricted) { + const int nr = int(float(rad) * 6.2831853 + 0.5); + for (int ray = 0; ray < nr; ++ray) { + const float a = float(ray) * 6.2831853 / float(nr); + const vec2 dir = vec2(cos(a), sin(a)); + for (int st = 1; st <= int(rad); ++st) { + const ivec2 t = p + ivec2(trunc(dir * float(st))); + if (t.x < 0 || t.y < 0 || t.x >= pc.gw || t.y >= pc.gh) break; + const int ti = t.y * pc.gw + t.x; + const uint tc = cells[ti], tm = cell_mat(tc); + if (tm == o) continue; + if (tm != mi) break; + if (atomicCompSwap(cells[ti], tc, o | (tc & 0x00FF0000u)) == tc) { ext[ti] = vec2(0.0); birth[ti] = birth_frame(); } // a converted cell starts afresh (0070d0b0) + } + } + } else if (rad > 0u) { // direct mode (0071ab50, restriction explicitly off): every loaded cell of the disk dx^2 + dy^2 < r^2 takes the output, whatever it holds + const int ri = int(rad); + for (int dy = -ri; dy <= ri; ++dy) + for (int dx = -ri; dx <= ri; ++dx) { + if (dx * dx + dy * dy >= ri * ri) continue; + const ivec2 t = p + ivec2(dx, dy); + if (t.x < 0 || t.y < 0 || t.x >= pc.gw || t.y >= pc.gh) continue; + const int ti = t.y * pc.gw + t.x; + const uint tc = cells[ti]; + if (cell_mat(tc) == o) continue; + if (atomicCompSwap(cells[ti], tc, o | (tc & 0x00FF0000u)) == tc) { ext[ti] = vec2(0.0); birth[ti] = birth_frame(); } + } + } + } +} diff --git a/shaders/render.comp b/shaders/render.comp new file mode 100644 index 0000000..c6fb974 --- /dev/null +++ b/shaders/render.comp @@ -0,0 +1,457 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 8, local_size_y = 8) in; +#include "common.glsl" +#include "areas.glsl" + +vec4 unpack_argb(uint c) { + return vec4(float((c >> 16) & 0xFFu), float((c >> 8) & 0xFFu), float(c & 0xFFu), float(c >> 24)) / 255.0; +} + +layout(std430, set = 0, binding = 13) readonly buffer TexData { uint texdata[]; }; // ARGB texels of all material textures +layout(std430, set = 0, binding = 14) readonly buffer TexInfo { uvec4 texinfo[]; }; // per material: offset, width, height, 0 = none + +layout(std430, set = 0, binding = 12) readonly buffer SceneMat { uint scene_mat[]; }; // pixel scenes: material + 1 +layout(std430, set = 0, binding = 15) readonly buffer BgTile { uint bgtile[]; }; // biome background tile (ARGB) +layout(std430, set = 0, binding = 16) readonly buffer SceneVis { uint scene_vis[]; }; // scene visual colours (ARGB) +layout(std430, set = 0, binding = 17) readonly buffer SceneBg { uint scene_bg[]; }; // scene backgrounds (ARGB) + +layout(std430, set = 0, binding = 8) readonly buffer Tables { uint perm[]; }; // [0,512) simplex: the gradient of the procedural biomes decides where the background wall is +int ffloor(float v) { return int(floor(v)); } +uint ps(uint i) { return perm[i]; } +#include "procgrad.glsl" +layout(std430, set = 0, binding = 21) readonly buffer EntLayer { uint entlayer[]; }; +struct Ent { vec4 a, box, mv, ai; ivec4 st; vec4 hp; ivec4 x; ivec4 rg; ivec4 rg2; ivec4 aux; vec4 ex; vec4 ex2; ivec4 ex3; vec4 ex4; vec4 ex5; vec4 ex6; vec4 ex7; ivec4 ex8; vec4 ex9; vec4 ex10; vec4 ex11; vec4 ex12, ex13, ex14, ex15, ex16, ex17, ex18, ex19; }; +layout(std430, set = 0, binding = 22) readonly buffer Ents { Ent ents[]; }; +layout(std430, set = 0, binding = 39) readonly buffer EntPrev { vec4 prevpos[]; }; // positions before the last tick // entity sprites drawn this frame (ARGB per window cell) + +layout(std430, set = 0, binding = 26) readonly buffer Overlay { uint overlay[]; }; // CPU-drawn HUD (ARGB, output pixels) +struct FLight { vec4 a; vec4 b; }; // x, y, radius (world cells), strength / rgb +layout(std430, set = 0, binding = 23) readonly buffer Ctrl { ivec4 inp; ivec4 inp2; ivec4 inp3; vec4 outp; vec4 outp2; vec4 view; vec4 hand0; ivec4 hand1; vec4 hand2; } ctrl; +layout(std430, set = 0, binding = 38) readonly buffer FxLights { uint lcount; uint lpad[3]; FLight lights[512]; }; // lights of projectiles and explosions this frame + +layout(std430, set = 0, binding = 44) readonly buffer Sky { uint sky[]; }; // src/world/sky.h + +layout(std430, set = 0, binding = 47) readonly buffer GlowA { vec4 glowA[]; }; // the blurred glow of the materials at half resolution (glow.comp) +layout(std430, set = 0, binding = 49) readonly buffer Fog { uint fog[]; }; +layout(std430, set = 0, binding = 50) readonly buffer Vis { vec4 fogvis[]; }; // line-of-sight visibility per 4x4 block of the window (fog.comp) // the explored map, one byte per 8x8 world cells (fog.comp) +layout(std430, set = 0, binding = 57) readonly buffer LitCells { uvec2 litc[]; }; // the light of the entities per cell (light_cells.comp sums lent once per cell) +layout(std430, set = 0, binding = 45) readonly buffer Lit { uvec4 lhead; vec4 lent[]; }; // the lights near the window: count, then (x, y, radius, strength), (r, g, b, 0) per light + +uvec4 sky_img(int i) { return uvec4(sky[4 + 3 * i], sky[5 + 3 * i], sky[6 + 3 * i], 0u); } +vec4 sky_texel(int i, int x, int y) { // ARGB texel of image i (wrapping in x, clamped in y; transparent outside) + uvec4 h = sky_img(i); + if (y < 0 || y >= int(h.z)) return vec4(0.0); + x = ((x % int(h.y)) + int(h.y)) % int(h.y); + return unpack_argb(sky[h.x + uint(y) * h.y + uint(x)]); +} +vec3 sky_table(float tod, int row) { // parallax_colors.bmp: the colour of a row over the day (x = time of day) + return sky_texel(0, int(fract(tod) * 511.0), row).rgb; +} +// The sky behind the terrain (the parallax of 00888c80 / 008897e0 / 00889cb0, docs/BACKLOG.md 22): the colour of layer k is row 20 k of parallax_colors.bmp at +// x = time of day (lerped); the images are masks tinted by it. Layer 0 (weather_background, white) fills the screen with row 0; 1 (parallax_background x row +// 20, alpha gradient_sky_alpha) and 2 (parallax_sunset_line x row 40, alpha the sunset alpha) are repeated from the top of the screen; then in the order 3, 10, 7, +// 4, 9, 6, 5, 8 the image of layer k repeats horizontally from x = -(camera x / depth) + rec.x, its top at y = camera y / depth - rec.y - parallax_offset_y +// (virtual pixels from the top of the screen, at least screen height - image height), tinted by its row and its alpha. Depths 50, 30, 25, 25, 40, 20, 35, 45 +// (layers 3..10); rec.x / rec.y / alphas are the host's (sky words 48..: cloud 1 x, cloud 1 y, cloud 2 y, mountains 1 y, mountains 2 y, storm y, storm alpha, +// gradient alpha, sunset alpha, parallax_offset_y). (The rain particles, layer 11, are the GameEmitRainParticles of init.lua.) +vec4 sky_tex(int i, vec2 t) { // image i at texel t, repeating in x, transparent outside in y + const uvec4 h = sky_img(i); + if (h.y == 0u || t.y < 0.0 || t.y >= float(h.z)) return vec4(0.0); + const int x = ((int(floor(t.x)) % int(h.y)) + int(h.y)) % int(h.y); + return unpack_argb(sky[h.x + uint(int(t.y)) * h.y + uint(x)]); +} +vec4 sky_tex_wrap(int i, vec2 t) { // repeating in both directions + const uvec4 h = sky_img(i); + if (h.y == 0u || h.z == 0u) return vec4(0.0); + return sky_tex(i, vec2(t.x, float(((int(floor(t.y)) % int(h.z)) + int(h.z)) % int(h.z)))); +} +vec3 sky_row(float tod, int row) { // 008897e0: index (w - 1) x time, lerped to the next column + const uvec4 h = sky_img(0); + const float f = (float(h.y) - 1.0) * fract(tod); + const int i0 = int(floor(f)); + const vec3 a = sky_texel(0, i0, row).rgb, b = sky_texel(0, min(i0 + 1, int(h.y) - 1), row).rgb; + return mix(a, b, f - float(i0)); +} +float skp(int k) { return uintBitsToFloat(sky[48u + uint(k)]); } +// TODO(to-do #18): layer 0 shader variant, the biome source of gradient / sunset alpha, the camera origin +vec3 sky_colour(vec2 w, vec2 cam, float tod, float tsec, vec2 scr) { + const vec2 camTL = cam - vec2(213.5, 121.0); // (the virtual screen: 427 x 242 virtual pixels, 1 per world pixel) + const vec2 sv = w - camTL; + vec3 col = sky_row(tod, 0); + { const float ga = skp(7); + if (ga > 0.0) { const vec4 c = sky_tex_wrap(1, sv); col = mix(col, c.rgb * sky_row(tod, 20), c.a * ga); } } + { const float sa = skp(8); + if (sa > 0.0) { const vec4 c = sky_tex_wrap(2, sv); col = mix(col, c.rgb * sky_row(tod, 40), c.a * sa); } } + const int order[8] = int[8](3, 10, 7, 4, 9, 6, 5, 8); + const float depth[11] = float[11](1.0, 1.0, 1.0, 50.0, 30.0, 25.0, 25.0, 40.0, 20.0, 35.0, 45.0); + for (int o = 0; o < 8; ++o) { + const int k = order[o]; + float rx = 0.0, ry = 0.0, a = 1.0; + if (k == 3) { rx = skp(0); ry = skp(1); } + else if (k == 4) ry = skp(2); + else if (k == 5 || k == 6) ry = skp(3); + else if (k == 7) ry = skp(4); + else { ry = skp(5); a = skp(6); } + if (a <= 0.0) continue; + const uvec4 h = sky_img(k); + const float top = max(camTL.y / depth[k] - ry - skp(9), 242.0 - float(h.z)); + const vec4 c = sky_tex(k, vec2(sv.x - camTL.x / depth[k] + rx, sv.y - top)); + if (c.a > 0.0) col = mix(col, c.rgb * sky_row(tod, 20 * k), c.a * a); + } + return col; +} + +// Lighting of the foreground (cells and sprites), after post_final.frag: the lights of the entities (LightComponent, cubic falloff), the glow of the materials, the light of +// the sky (the sky exposure of the cell: open sky, the depth below the first solid cell, the sides of an opening; times the colour of the time of day), gamma, then the +// fog of war (unexplored cells are black unless the sky lights them) and the baseline 0.35 - sky ambient of the explored map. The background (sky, cave walls) is not lit. +// sky[3]: bit 0 lighting on (F4), bit 2 fog of war off (F6), bit 3 glow off (F7). +const int FOG_TW = 1152, FOG_TH = 896, FOG_OX = 18432, FOG_OY = 14336; // the fog map of fog.comp: 32 x 32 pixel texels from the world origin, the clear amount (255 - fog) +float fog_clear(ivec2 t) { + if (t.x < 0 || t.y < 0 || t.x >= FOG_TW || t.y >= FOG_TH) return 0.0; + return float(min(fog[t.y * FOG_TW + t.x], 255u)) / 255.0; +} +vec2 gCpf = vec2(0.0); // window position of the pixel in cells, at sub-cell precision +float bm_dust_r(vec2 w) { // the biome modifier dust_amount of the map tile under w (biome_mods.glsl's table, read through perm) + const uint n = mats[mats.length() - 1u].w; + const uint fb = 1024u + 9u * n + 132u + n; + const int bw = int(perm[fb]), bh = int(perm[fb + 1u]); + const ivec2 t = ivec2(floor(w / 512.0)) + ivec2(35, 14); + if (bw <= 0 || t.x < 0 || t.y < 0 || t.x >= bw || t.y >= bh) return 0.0; + return uintBitsToFloat(perm[fb + 2u + 8192u + 1u + 64u * 6u + 1u + 16u * 3u + 8u * 3272u + 4u * uint(t.y * bw + t.x) + 3u]); +} +float perlin_tex(vec2 uv) { // tex_perlin_noise (data/shaders/noise_perlin.png, sky image 11): bilinear, repeating; uv in texture units (1 = the image) + const uvec4 h = sky_img(11); + if (h.y == 0u || h.z == 0u) return 0.5; + const vec2 q = uv * vec2(float(h.y), float(h.z)) - 0.5; + const ivec2 e = ivec2(floor(q)); + const vec2 g = q - vec2(e); + const int W = int(h.y), H = int(h.z); + float t[4]; + for (int k = 0; k < 4; ++k) { + const int x = (((e.x + (k & 1)) % W) + W) % W, y = (((e.y + (k >> 1)) % H) + H) % H; + t[k] = float((sky[h.x + uint(y) * h.y + uint(x)] >> 16) & 255u) / 255.0; + } + return mix(mix(t[0], t[1], g.x), mix(t[2], t[3], g.x), g.y); +} +float sky_cell(ivec2 b) { // one block of the exposure grid (sky words: [7] = its offset, then the first block column / row, then the values) + const uint o = sky[60]; + const ivec2 b0 = ivec2(int(sky[o]), int(sky[o + 1u])); + const ivec2 q = clamp(b - b0, ivec2(0), ivec2(1024 / 64 + 1, 576 / 64 + 1)); + return uintBitsToFloat(sky[o + 2u + uint(q.y * (1024 / 64 + 2) + q.x)]); +} +float sky_bilinear(vec2 w) { // texel centres at the block centres + const vec2 q = w / 64.0 - 0.5; + const ivec2 e = ivec2(floor(q)); + const vec2 g = q - vec2(e); + return mix(mix(sky_cell(e), sky_cell(e + ivec2(1, 0)), g.x), mix(sky_cell(e + ivec2(0, 1)), sky_cell(e + ivec2(1, 1)), g.x), g.y); +} +float sky_amount(vec2 w) { // post_final.frag HIQ: (centre + 0.25 x the four neighbours) x 0.5 + return (sky_bilinear(w) + 0.25 * (sky_bilinear(w - vec2(64.0, 0.0)) + sky_bilinear(w + vec2(64.0, 0.0)) + sky_bilinear(w - vec2(0.0, 64.0)) + sky_bilinear(w + vec2(0.0, 64.0)))) * 0.5; +} +float fog_bilinear(vec2 w) { // the fog map between the texel centres (32 n + 16), as a linear-filtered tex_fog + const vec2 q = w / 32.0 - 0.5; + const ivec2 e = ivec2(floor(q)); + const vec2 g = q - vec2(e); + return mix(mix(fog_clear(e), fog_clear(e + ivec2(1, 0)), g.x), mix(fog_clear(e + ivec2(0, 1)), fog_clear(e + ivec2(1, 1)), g.x), g.y); +} +float explored(ivec2 cp, ivec2 wp) { // 0 fog .. 1 clear: the fog map read bilinearly between the texel centres (32 n + 16), as post_final.frag samples tex_fog + if ((sky[3] & 4u) != 0u) return 1.0; + const vec2 ww = gCpf + vec2(float(pc.view_x), float(pc.view_y)); + const vec2 w = ww + vec2(FOG_OX, FOG_OY); + // post_final.frag HIQ: the centre plus four samples at +-0.25 texel diagonally and four at 0.75 texel on the axes, / 9 (tex_fog: FOG_TEX_SIZE 64 at zoom 1, 32 px texels) + const float clr = (fog_bilinear(w) + fog_bilinear(w + vec2(8.0, -8.0)) + fog_bilinear(w + vec2(-8.0, -8.0)) + fog_bilinear(w + vec2(-8.0, 8.0)) + fog_bilinear(w + vec2(8.0, 8.0)) + + fog_bilinear(w - vec2(24.0, 0.0)) + fog_bilinear(w + vec2(24.0, 0.0)) + fog_bilinear(w - vec2(0.0, 24.0)) + fog_bilinear(w + vec2(0.0, 24.0))) * 0.1111111; + // the holes of the frame (main.cpp: fog_of_war_hole sprites): post_final.frag fog x (1 - the hole alpha); the disc of explosion_flare_fog_of_war_hole.png ~ exp(-4.5 t^2) + const uint n = mats[mats.length() - 1u].w; + const uint hb = 1024u + 9u * n + 132u + n + 2u + 8192u; + const int nh = min(int(uintBitsToFloat(perm[hb])), 64); + const uint ib = hb + 1u + 64u * 6u + 1u + 16u * 3u; + float hole = 0.0; + for (int k = 0; k < nh; ++k) { // the hole sprite's alpha (its animation frame by the age), bilinear (smooth_filtering), scaled about its offset + const uint o = hb + 1u + uint(k) * 6u; + const vec2 hp = vec2(uintBitsToFloat(perm[o]), uintBitsToFloat(perm[o + 1u])); + const float sc = max(uintBitsToFloat(perm[o + 2u]), 0.01); + const int img = int(uintBitsToFloat(perm[o + 4u])); + float a = 0.0; + if (img >= 0 && img < 8) { + const uint ia = ib + uint(img) * 3272u; + const float iw = uintBitsToFloat(perm[ia]), ih = uintBitsToFloat(perm[ia + 1u]); + const vec2 sp = (ww - hp) / sc + vec2(uintBitsToFloat(perm[ia + 2u]), uintBitsToFloat(perm[ia + 3u])) - 0.5; + if (sp.x < -1.0 || sp.y < -1.0 || sp.x > iw || sp.y > ih) continue; + const uint nf = max(perm[ia + 4u] & 255u, 1u), wt = max(perm[ia + 4u] >> 8, 1u); + const uint fr = (uint(uintBitsToFloat(perm[o + 5u])) / wt) % nf; + const ivec2 s0 = ivec2(floor(sp)); + const vec2 f = sp - vec2(s0); + for (int q = 0; q < 4; ++q) { + const ivec2 s = s0 + ivec2(q & 1, q >> 1); + if (s.x < 0 || s.y < 0 || s.x >= int(iw) || s.y >= int(ih)) continue; + a += float(perm[ia + 5u + fr * 1089u + uint(s.y * int(iw) + s.x)]) / 255.0 * ((q & 1) != 0 ? f.x : 1.0 - f.x) * ((q >> 1) != 0 ? f.y : 1.0 - f.y); + } + } else { // (no image: a soft disc of 16 px x the scale) + const float t = distance(ww, hp) / (16.0 * sc); + a = t < 1.0 ? exp(-4.5 * t * t) : 0.0; + } + hole = max(hole, uintBitsToFloat(perm[o + 3u]) * a); + } + return 1.0 - (1.0 - clr) * (1.0 - min(hole, 1.0)); +} +#define FPAL_TABLE perm +// ---- the fire palettes of the cell blitting (main.cpp loads them after the tables; 00722490 the lookup, 00724580 the use) ---- +uint fpal_word(uint k) { return FPAL_TABLE[1024u + 9u * mats[mats.length() - 1u].w + k]; } +vec3 fpal_rgb(uint w) { return vec3(float((w >> 16) & 255u), float((w >> 8) & 255u), float(w & 255u)) / 255.0; } +vec3 fire_lookup(uint at, uint row, uint w, float t) { // the row of the image at t x width: the first / last pixel outside, else the two neighbours mixed + const float x = float(w) * t; + const uint r0 = at + row * w; + if (x >= float(w)) return fpal_rgb(fpal_word(r0 + w - 1u)); + if (x <= 0.0) return fpal_rgb(fpal_word(r0)); + const uint i0 = uint(floor(x)); + if (float(i0) == x || i0 + 1u >= w) return fpal_rgb(fpal_word(r0 + i0)); + return mix(fpal_rgb(fpal_word(r0 + i0)), fpal_rgb(fpal_word(r0 + i0 + 1u)), x - float(i0)); +} +uint fire_row(uint m) { return min(fpal_word(132u + m), 5u); } // CellData +0x68 fire_colors_index +float fire_age(uint b) { return float(birth_age(b)) + float(hash3(b & BIRTH_MASK, 77u, 5u) % 21u); } // 00709ef0: the flame's frame is the creation frame - uniform(0, 20) +vec3 fire_colour(uint m, uint b) { const float a = fire_age(b) / 40.0; return fire_lookup(0u, fire_row(m), 8u, a * a); } // colors_fire.png at (age / 40)^2 +vec3 fire_glow_colour(uint m, uint b) { const float a = fire_age(b) / 40.0; return fire_lookup(48u, fire_row(m), 8u, fire_age(b) < 15.0 ? a : a * a); } // colors_fire_glow.png (young: age / 40) +vec3 burning_colour(uint m, uint h) { return fpal_rgb(fpal_word(96u + fire_row(m) * 6u + h % 6u)); } // colors_lava.bmp: a random pixel of the row +vec3 glow_at(ivec2 cp) { // bilinear in the half-resolution glow + if ((sky[3] & 8u) != 0u) return vec3(0.0); + const int hw = (pc.gw + 1) / 2, hh = (pc.gh + 1) / 2; + const vec2 q = (vec2(cp) + 0.5) * 0.5 - 0.5; + const ivec2 q0 = ivec2(floor(q)); + const vec2 f = q - vec2(q0); + vec3 r = vec3(0.0); + for (int k = 0; k < 4; ++k) { + const ivec2 s = clamp(q0 + ivec2(k & 1, k >> 1), ivec2(0), ivec2(hw - 1, hh - 1)); + r += glowA[s.y * hw + s.x].rgb * (((k & 1) != 0 ? f.x : 1.0 - f.x) * ((k >> 1) != 0 ? f.y : 1.0 - f.y)); + } + return max(vec3(0.0), r - 0.008); // (post_final.frag) +} +float gFog = 1.0; +float gSky = 0.0, gLum = 0.0; // (the squared sky amount and the luminosity of the lights of the last lighting() call: the dust) +vec3 gGlow = vec3(0.0); +vec3 lighting(ivec2 cp, ivec2 wp, uint emissive) { + gFog = 1.0; gGlow = vec3(0.0); gSky = 1.0; gLum = 1.0; + if ((sky[3] & 1u) == 0u) return vec3(1.0); + gGlow = glow_at(cp); + if (emissive != 0u) { gFog = explored(cp, wp); return vec3(1.0); } + const uvec2 lc = litc[cp.y * pc.gw + cp.x]; // (the sum over the lights near the window, light_cells.comp: a cell's light is the same for all its pixels) + vec3 L = vec3(unpackHalf2x16(lc.x), unpackHalf2x16(lc.y).x); + // post_final.frag: lights = pow(tex_lights x 0.8, 1.5) + glow; the sky: sky_light_color x sky_ambient_amount, the exposure grid (tex_skylight, 64-pixel blocks of the + // coarse map, main.cpp) read bilinearly with the HIQ blur (0.5 centre + 0.125 x the four neighbours one block away); lights = max(lights, sky), at most 1, gamma 1 / 2.2; + // then the fog of war: s = the sky amount faded out between y 250 and 350, fog = min(1, max(2 x 1.4 (0.6, 0.5, 0.45) max(0, 1 - fog - sqrt(s)), s)), + // lights x fog + max(0.35 - s, 0) x fog + vec3 lights = pow(max(L * 0.8, vec3(0.0)), vec3(1.5)) + gGlow; + float amount = sky_amount(vec2(wp) + 0.5); + amount *= amount; // (post_final.frag: sky_ambient_amount *= sky_ambient_amount after the sample) + const float tod = uintBitsToFloat(sky[0]); + const vec3 topc = sky_table(tod, 4); + const float f = smoothstep(0.12, 0.5, dot(topc, vec3(0.299, 0.587, 0.114))); + // TODO(to-do #17): sky_light_color from the PostFx keyframes (00890330) + const vec3 skyc = mix(vec3(0.12, 0.16, 0.30), vec3(1.0), f); // (sky_light_color: the engine's day keyframes are not traced yet; this follows the sky's brightness) + lights = min(max(lights, skyc * amount), vec3(1.0)); + lights = pow(lights, vec3(1.0 / 2.2)); + const float vis = explored(cp, wp); + const float s = amount * (1.0 - clamp((float(wp.y) - 250.0) / 100.0, 0.0, 1.0)); + vec3 fog = 1.4 * vec3(0.6, 0.5, 0.45) * max(0.0, 1.0 - (1.0 - vis) - sqrt(s)); + fog = min(vec3(1.0), max(2.0 * fog, vec3(s))); + L = lights * fog + max(0.35 - s, 0.0) * fog; + gSky = amount; gLum = sqrt(min(1.0, dot(L, vec3(0.299, 0.587, 0.114)))); + gFog = max(fog.r, max(fog.g, fog.b)); + return min(L, vec3(1.35)); +} + +// The picture is made at the game's own resolution, one pixel per world cell, then scaled up to the window (Noita draws its world at a fixed internal +// resolution: the window size changes only the final scaling). pass_ 1, the world pass: one thread per cell of the view, the colour of the cell (background, +// material, lights, fog of war, entity layer, projectile lights, grading, glow, blindness) into wcell. pass_ 0, the output pass: one thread per window pixel, +// the cell under it (camera shake and the camera between two ticks at sub-cell precision), the player at its sub-cell position, the low-health vignette, the HUD. +layout(std430, set = 0, binding = 60) writeonly buffer ClipRaw { uint clipraw[]; }; // the window picture without the HUD (the replay recorder) +layout(std430, set = 0, binding = 59) buffer WorldCells { uvec4 wcell[]; }; // per cell: the colour (rgba8), the light Lv (half r g, half b + the fog amount) +float gScreenY = 0.0; + +vec3 world_cell(ivec2 cp, out vec3 Lv) { + uint c = cells[cp.y * pc.gw + cp.x]; + + float t = gScreenY; // (the screen row of the cell centre, for the background gradient) + vec3 bg = mix(vec3(0.05, 0.05, 0.09), vec3(0.02, 0.02, 0.04), t); + + ivec2 wp0 = cp + ivec2(pc.view_x, pc.view_y); // world cell + const int bai = area_of_cell(wp0); + bool bgOk = true, bgDrawn = false; + { // a procedural (surface) biome has no background wall: the sky and the surface caves of the hills show none + const uint trec = topo_rec(wp0); + if (trec != 0xFFFFFFFFu && (atab[trec] & 1u) != 0u) bgOk = false; + } + if (bgOk && bai >= 0 && arec(bai, 12) > 0) { + // the biome's background wall, tiled in world space + const int bw = arec(bai, 12), bh = arec(bai, 13); + uint tx = uint(((wp0.x % bw) + bw) % bw), ty = uint(((wp0.y % bh) + bh) % bh); + vec4 bc = unpack_argb(bgtile[uint(arec(bai, 11)) + ty * uint(bw) + tx]); + bg = mix(bg, bc.rgb, bc.a); + } + uint sidx = 0xFFFFFFFFu; + for (int a = 0; a < int(atab[0]); ++a) { // the first area whose scene has a material here; the first scene background + int si = scene_index(a, wp0); + if (si < 0) continue; + if (sidx == 0xFFFFFFFFu && scene_mat[si] != 0u) sidx = uint(si); + uint sb = scene_bg[si]; + if (sb != 0u) { vec4 bc = unpack_argb(sb); bg = mix(bg, bc.rgb, bc.a); bgDrawn = bgDrawn || bc.a > 0.01; } + } + + const uint trecSky = topo_rec(wp0); + bool skyPix = false; + if (sky[2] != 0u && trecSky != 0xFFFFFFFFu && (atab[trecSky] & 0x1000000u) != 0u && !bgDrawn && ((atab[trecSky] & 0x2000000u) == 0u || cp.y < int(sky[64 + cp.x])) && cell_mat(c) == 0u) { // (the scene biomes show the sky wherever no scene background is drawn; the procedural ones (hills) have caves with no wall behind: there the column must be open) // open sky: no wall behind, nothing solid above in the window + const vec2 cam = vec2(float(pc.view_x) + float(pc.gw) * 0.5, float(pc.view_y) + float(pc.gh) * 0.5); + skyPix = true; + bg = sky_colour(vec2(wp0), cam, uintBitsToFloat(sky[0]), uintBitsToFloat(sky[1]), vec2(0.0, float(wp0.y - pc.view_y) - float(pc.gh) * 0.5)); + } + if ((sky[3] & 2u) != 0u && cell_mat(c) == 0u) { // F5: the sky mask: green = the sky is drawn, red = a solid cell above in the window column (the row of that cell is yellow), blue = not an outdoor biome, magenta = a scene background is drawn + const int top = int(sky[64 + cp.x]); + vec3 dbg = vec3(0.0); + if (!(trecSky != 0xFFFFFFFFu && (atab[trecSky] & 0x1000000u) != 0u)) dbg = vec3(0.0, 0.0, 0.8); + else if (bgDrawn) dbg = vec3(0.8, 0.0, 0.8); + else if (wp0.y >= 150) dbg = vec3(0.8, 0.8, 0.0); + else if ((atab[trecSky] & 0x2000000u) != 0u && cp.y >= top) dbg = vec3(0.8, 0.0, 0.0); + else dbg = vec3(0.0, 0.7, 0.0); + bg = mix(bg, dbg, 0.5); + } + { // the weather fog (post_final.frag): the background mixes toward FOG_BACKGROUND 0.7 (FOG_BACKGROUND_NIGHT 0.2 by night_amount) by fog_amount_background = + // WorldStateComponent.fog (00c42d30 eases it toward max(fog_target, fog_target_extra); init.lua's weather sets fog_target_extra 0.3 .. 0.85 when it rains or snows) + const float wfog = uintBitsToFloat(sky[61]); + if (wfog > 0.0) { + const float tod = uintBitsToFloat(sky[0]); + const float night = clamp((0.55 - abs(fract(tod) - 0.57) - 0.4) * 10.0, 0.0, 1.0); + bg = mix(bg, vec3(mix(0.7, 0.2, night)), wfog); + } + } + if (sky[63] != 0u && !skyPix) bg = vec3(0.022, 0.02, 0.028); // the spell test room: a dark background + vec3 rgb = bg; + uint m = cell_mat(c); + gCpf = vec2(cp) + 0.5; + Lv = lighting(cp, wp0, m != 0u && ((mats[3u * m + 2u].z >> 17) & 1u) != 0u ? 1u : 0u); // fire and lava light themselves + rgb = skyPix ? bg : bg * gFog; // (the fog of war darkens the background too; the sky lights it) + if (m != 0u) { + vec4 col = unpack_argb(mats[3u * m].x); + float shade = 1.0 + (float((c >> 16) & 0xFFu) / 255.0 - 0.5) * 0.18; // per-cell variation + uvec4 ti = texinfo[m]; + if (ti.y != 0u) { // the game's material texture, tiled in world space (one texel per cell) + ivec2 wp = cp + ivec2(pc.view_x, pc.view_y); + uint tx = uint(((wp.x % int(ti.y)) + int(ti.y)) % int(ti.y)); + uint ty = uint(((wp.y % int(ti.z)) + int(ti.z)) % int(ti.z)); + vec4 tc = unpack_argb(texdata[ti.x + ty * ti.y + tx]); + col = vec4(tc.rgb, col.a); + shade = 1.0; + } + if ((mflags(m) & F_FIRE) != 0u) { col = vec4(fire_colour(m, birth[cp.y * pc.gw + cp.x]), 1.0); shade = 1.0; } // a flame by its age (00724580: colors_fire.png, opaque) + if (sidx != 0xFFFFFFFFu && scene_mat[sidx] == m + 1u && scene_vis[sidx] != 0u) { // pixel-scene visual image // pixel-scene visual image + vec4 vc = unpack_argb(scene_vis[sidx]); + col = vec4(vc.rgb, col.a); + shade = 1.0; + } + if ((c & 0x80000000u) != 0u && (mflags(m) & F_STATIC) != 0u) col.rgb = mix(col.rgb, vec3(0.42, 0.03, 0.03), 0.6); // stained with blood (fx_particles.comp) + if ((c & (FROZEN_BIT | BURN_BIT)) == FROZEN_BIT && (mflags(m) & F_STATIC) != 0u && (mflags(m) & F_FIRE) == 0u) col.rgb = mix(col.rgb, vec3(0.72, 0.88, 1.0), 0.55); // the frozen stain (edits.comp mode 12; the icy look is an approximation of the engine's stain colour) + if ((mflags(m) & (F_STATIC | F_LIQCELL)) == F_STATIC && ext[cp.y * pc.gw + cp.x].x >= 8.0) // a solid that burnt: 0070e060 takes one off every channel per eight of its burn age (burn.comp, ext.x), down to 0 + col.rgb = max(col.rgb - vec3(floor(ext[cp.y * pc.gw + cp.x].x / 8.0) / 255.0), vec3(0.0)); + if ((mflags(m) & F_STATIC) != 0u && (c & BURN_BIT) == 0u && ((c >> 28) & 7u) != 0u && max(col.r, max(col.g, col.b)) > 18.5 / 255.0) col.rgb = max(col.rgb * (1.0 - float((c >> 28) & 7u) / 7.0), vec3(19.0 / 255.0)); // burnt by explosions (0070e760: every channel x (1 - s), at least 0x13; the level in bits 28-30, explode_rays.comp) + if ((c & BURN_BIT) != 0u) { // a burning cell: a random pixel of colors_lava.bmp every frame (00724580), opaque + col = vec4(burning_colour(m, hash3(uint(cp.x + pc.view_x), uint(cp.y + pc.view_y), uint(pc.frame))), 1.0); + shade = 1.0; + } + vec3 fgc = col.rgb * shade * Lv; + { // the dust (post_final.frag): the biome modifier dust_amount (tex_fog .g) hazes the lit foreground toward FOG_FOREGROUND 0.6 (night or underground 0.2): + // amount = mix(dust, fog_amount_foreground (the weather fog, sky word 61), sky) x max(sky, dust x luminosity x min(1, 2 perlin^2)); night_amount = clamp((0.55 - |tod - 0.57| - 0.4) x 10, 0, 1) + const vec2 wq = vec2(cp + ivec2(pc.view_x, pc.view_y)); + const float dust = bm_dust_r(wq), wfog = uintBitsToFloat(sky[61]); + if (dust > 0.0 || (wfog > 0.0 && gSky > 0.0)) { + const float tod = uintBitsToFloat(sky[0]); + const float night = clamp((0.55 - abs(fract(tod) - 0.57) - 0.4) * 10.0, 0.0, 1.0); + const float pn = perlin_tex(wq * 0.0004 + vec2(0.0, uintBitsToFloat(sky[1]) * 0.005)); // noise_perlin2 = texture2D(tex_perlin_noise, world_pos * 0.0004 + (0, noise_time * 0.005)) + const float amt = mix(dust, wfog, gSky) * max(gSky, dust * gLum * min(1.0, pn * pn * 2.0)); + fgc = mix(fgc, vec3(mix(0.6, 0.2, max(night, 1.0 - gSky))), amt); + } + } + rgb = mix(bg, fgc, col.a); + } + uint ec = entlayer[cp.y * pc.gw + cp.x]; + const bool playerPix = (ec >> 24) == 254u; // the player is drawn below, at its sub-cell position (the sprite layer itself has whole-cell resolution) + if (playerPix) ec = 0u; + if ((ec >> 24) >= 1u && (ec >> 24) <= 3u) rgb += unpack_argb(ec | 0xFF000000u).rgb * gFog; // (the fog of war hides them: post_final multiplies the light buffer they go to by fog_of_war) // additive light (alpha 1: additive projectiles and animations, 2: particles, 3: additive sprites such as glowing eyes) + else if ((ec >> 24) != 0u) { vec4 e4 = unpack_argb(ec); rgb = mix(rgb, e4.rgb * Lv, e4.a); } + return rgb; +} + +vec3 post_cell(vec3 rgb, ivec2 cp, vec3 Lv) { + { // light of projectiles, flashes and explosions: additive glow + vec2 wpos = vec2(cp + ivec2(pc.view_x, pc.view_y)) + 0.5; // (the cell centre: the world is drawn at one pixel per cell) + for (uint i = 0u; i < min(lcount, 512u); ++i) { + float d = distance(wpos, lights[i].a.xy) / lights[i].a.z; + if (d < 1.0) rgb += lights[i].b.rgb * ((1.0 - d) * (1.0 - d) * lights[i].a.w) * gFog; + } + } + { // the color grading of the biome modifiers (post_final.frag: mix toward the grey by .a, then times .rgb; main.cpp moves it toward the camera's biome; the last 4 words of Tables) + const uint n = mats[mats.length() - 1u].w; + const uint gi = 1024u + 9u * n + 132u + n + 2u + 8192u + 1u + 64u * 6u + 1u + 16u * 3u + 8u * 3272u + 8192u * 4u + 2u * n; + const vec4 cg = vec4(uintBitsToFloat(perm[gi]), uintBitsToFloat(perm[gi + 1u]), uintBitsToFloat(perm[gi + 2u]), uintBitsToFloat(perm[gi + 3u])); + if (cg != vec4(1.0, 1.0, 1.0, 0.0) && cg != vec4(0.0)) { rgb = mix(rgb, vec3((rgb.r + rgb.g + rgb.b) * 0.3333), cg.a); rgb *= cg.rgb; } + } + if ((sky[3] & 1u) != 0u && (gGlow.r + gGlow.g + gGlow.b) > 0.002) { // the glow, blended like post_final.frag: max(color + glow * 0.6 - 0.6 * lights, screen(color, glow)) + const vec3 g = gGlow * min(vec3(1.0), 2.8 * vec3(0.6, 0.5, 0.45) * gFog); // (post_final.frag: fog_of_war = min(1, 2 x 1.4 x (0.6, 0.5, 0.45) x (1 - fog)): the glow keeps its strength until the fog is thick) + rgb = max(rgb + g * 0.6 - 0.6 * Lv, clamp(rgb + g - rgb * g, 0.0, 1.0)); + } + rgb *= 1.0 - clamp(ctrl.hand2.w, 0.0, 1.0); // BLINDNESS (00b015d0 case 9): a black screen over the world, below the HUD + return rgb; +} + +void main() { + // camera shake of explosions: the whole picture (not the HUD) is offset by a random amount + const float scl = float(pc.scale) / 16.0; // output pixels per cell (fixed point, 1/16) + float amp = float(lpad[0]) * 0.001 * scl; + ivec2 so = amp > 0.5 ? ivec2((vec2(float(hash3(uint(pc.frame), 1u, 77u) & 255u), float(hash3(uint(pc.frame), 2u, 77u) & 255u)) / 255.0 - 0.5) * 2.0 * amp) : ivec2(0); + so += ivec2(round(ctrl.view.xy * scl)); // the camera is drawn between its last two ticks, at sub-cell precision (host: view.xy = displayed camera - centre of the window, in cells) + const ivec2 vis = ivec2((vec2(pc.gw, pc.gh) * scl - vec2(pc.ow, pc.oh)) * 0.5); // the window is the centre of the simulated grid (zoomed) + if (pc.pass_ == 1) { // the world pass: the cells from one before the window's first to past its last (main.cpp dispatches ow / scl + 4 by oh / scl + 4) + const ivec2 cp = ivec2(floor(vec2(so + vis) / scl)) - 1 + ivec2(gl_GlobalInvocationID.xy); + if (cp.x < 0 || cp.y < 0 || cp.x >= pc.gw || cp.y >= pc.gh) return; + gScreenY = ((float(cp.y) + 0.5) * scl - float(so.y + vis.y)) / float(pc.oh); + vec3 Lv; + vec3 rgb = world_cell(cp, Lv); + rgb = post_cell(rgb, cp, Lv); + wcell[cp.y * pc.gw + cp.x] = uvec4(packUnorm4x8(vec4(clamp(rgb, 0.0, 1.0), 1.0)), packHalf2x16(Lv.rg), packHalf2x16(vec2(Lv.b, gFog)), 0u); + return; + } + ivec2 p = ivec2(gl_GlobalInvocationID.xy); + if (p.x >= pc.ow || p.y >= pc.oh) return; + ivec2 cp = clamp(ivec2(floor(vec2(p + so + vis) / scl)), ivec2(0), ivec2(pc.gw - 1, pc.gh - 1)); + vec3 rgb = unpackUnorm4x8(wcell[cp.y * pc.gw + cp.x].x).rgb; // (only the colour: the light is read for the player's pixels) + if (ctrl.inp2.y != 0 && (ents[0].st.w & 8) == 0) { + vec2 P = ents[0].a.xy; + const vec2 pv = prevpos[0].xy; + if (distance(pv, P) < 10.0) P = mix(pv, P, ctrl.view.z); + const ivec2 cq = clamp(ivec2(floor(vec2(p + so + vis) / scl - (P - floor(P)))), ivec2(0), ivec2(pc.gw - 1, pc.gh - 1)); + const uint pe = entlayer[cq.y * pc.gw + cq.x]; + if ((pe >> 24) == 254u) { // the player's pixel: lit like the cell under it, then the cell's post effects + const uvec4 wc = wcell[cp.y * pc.gw + cp.x]; + const vec3 Lv = vec3(unpackHalf2x16(wc.y), unpackHalf2x16(wc.z).x); + gFog = unpackHalf2x16(wc.z).y; gGlow = glow_at(cp); + rgb = post_cell(unpack_argb(pe | 0xFF000000u).rgb * Lv, cp, Lv); + } + } + // TODO(to-do #20): damage_flash_interpolation, additive_overlay_color / overlay_color + { // the low health indicator (post_final.frag): + LOW_HEALTH_INDICATOR_COLOR (0.7, 0.1, 0) x (1.3 x the distance from the centre)^4 x low_health_indicator_alpha (sky word 62) + const float lh = uintBitsToFloat(sky[62]); + if (lh > 0.0) { + float a = length(vec2(p) / vec2(float(pc.ow), float(pc.oh)) - 0.5) * 1.3; + a *= a; a *= a; + rgb += vec3(0.7, 0.1, 0.0) * a * lh; + } + } + { // the picture without the HUD for the replay recorder (the game's GIFs have no UI): clip_half.comp / main.cpp copy it + const uvec3 q0 = uvec3(clamp(rgb, 0.0, 1.0) * 255.0 + 0.5); + clipraw[p.y * pc.ow + p.x] = 0xFF000000u | (q0.r << 16) | (q0.g << 8) | q0.b; + } + const uint ovTile = uint((p.y >> 5) * ((pc.ow + 31) >> 5) + (p.x >> 5)); // the HUD overlay: a mask word per 32 x 32 tile, then 1024 words per tile (main.cpp kOvlTiles) + const uint ov = overlay[ovTile] != 0u ? overlay[3600u + ovTile * 1024u + uint((p.y & 31) * 32 + (p.x & 31))] : 0u; + if ((ov >> 24) != 0u) { vec4 o4 = unpack_argb(ov); rgb = mix(rgb, o4.rgb, o4.a); } + uvec3 q = uvec3(clamp(rgb, 0.0, 1.0) * 255.0 + 0.5); + pixels[p.y * pc.ow + p.x] = 0xFF000000u | (q.r << 16) | (q.g << 8) | q.b; // BGRA8 in memory +} diff --git a/shaders/rigid_burn.comp b/shaders/rigid_burn.comp new file mode 100644 index 0000000..4a8c3b0 --- /dev/null +++ b/shaders/rigid_burn.comp @@ -0,0 +1,123 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 256) in; +#include "rigid_common.glsl" + +// Fire inside the rigid bodies of a burnable material (docs/FIRE_FRACTURE_AND_LIQUIDS.md, DISPLACEMENT_AND_FIRE.md, CEILING_ATTACHMENT_AND_CONTACT_GEOMETRY.md). +// One workgroup per body, a thread per group of pixels. A pixel has a state word in `burn`: bit 31 burning, bit 30 burnt away (reported to the host, which removes it from +// the body and splits it), bits 16-23 fade, bits 0-15 the burn age. A burning pixel does what a burning solid cell does: +// * common burning (0070fb10): flames into (x-1..x+1, y-1) with generates_flames percent, smoke, requires_oxygen fade (restored by an accepted flame, else -1 per update, +// burning stops below 1), and one spread attempt: r % 101 <= fade, then ONE of the four neighbours, then the ignition draw r % 101 < 100 - autoignition_temperature; +// * then the age grows, and on every 32nd frame a pixel with fire_hp != -1 and age > fire_hp is removed. +// A pixel that is not burning catches fire from the fire sources in its cell and the four around it with the probabilities of burn.comp. Water puts it out. +layout(std430, set = 0, binding = 41) buffer Burn { uint burn[]; }; +layout(std430, set = 0, binding = 23) buffer Ctrl { ivec4 inp; ivec4 inp2; ivec4 inp3; vec4 outp; vec4 outp2; vec4 view; vec4 hand0; ivec4 hand1; vec4 hand2; ivec4 burnN; ivec4 burnEv[256]; } ctrl; + +const uint BURNING = 0x80000000u, BURNT = 0x40000000u; + +float rnd(uint a, uint b, uint c) { return float(hash3(a, b, c) & 0xFFFFu) / 65536.0; } +layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; }; // 1024 noise words, 4 per material (burning explosion), then 2 per material: flame and smoke material + 1 +uint flame_material(uint m, uint dflt) { const uint v = tbl[1024u + 4u * mats[mats.length() - 1u].w + 2u * m]; return v != 0u ? v - 1u : dflt; } +uint smoke_material(uint m, uint dflt) { const uint v = tbl[1024u + 4u * mats[mats.length() - 1u].w + 2u * m + 1u]; return v != 0u ? v - 1u : dflt; } + +void main() { + int bi = int(gl_WorkGroupID.x); + if (bi >= bhdr.x) return; + Body B = bodies[bi]; + if ((B.i.z & B_REMOVED) != 0 || B.i.y < 0) return; + const uint matp1 = uint(ents[B.i.y].ex5.z + 0.5); + if (matp1 == 0u) return; + const uint m = matp1 - 1u; + const uint mz = mats[3u * m + 2u].z, mw = mats[3u * m + 2u].w; + const int s = B.i.x; + const int w = shi(s, 0), h = shi(s, 1); + if (w * h > 4096) return; + const vec2 com = vec2(shf(s, 5), shf(s, 6)); + const float cs = cos(B.p.z), sn = sin(B.p.z); + const uvec4 sp = mats[mats.length() - 1u]; // fire, smoke, steam, count + const int ai = int(mw & 255u), tempFire = int((mw >> 8) & 255u), smokeP = int((mw >> 16) & 255u), flameP = int((mw >> 24) & 255u); + const int fireHp = int(mz << 16) >> 16; // signed 16 bits + const bool noBurnOut = fireHp == -1; + const bool needO2 = ((mz >> 19) & 1u) != 0u; + const float pIgnite = max(0.0, float(100 - ai)) / 101.0; + const uint base = uint(bi) * 4096u; + for (int px = int(gl_LocalInvocationID.x); px < w * h; px += 256) { + const ivec2 pp = ivec2(px % w, px / w); + if (!shape_solid(s, pp.x, pp.y)) continue; + uint st = burn[base + uint(px)]; + if ((st & BURNT) != 0u) continue; + const vec2 wp = B.p.xy + rot2(cs, sn, vec2(pp) + 0.5 - com); + const ivec2 wc = ivec2(floor(wp)); + const uint seed = uint(bi) * 4099u + uint(px); + if ((st & BURNING) == 0u) { + // fire sources in the pixel's cell and the four around it (cells are free under a body) + float keep = 1.0, bestFade = -1.0; + bool wet = false; + for (int d = 0; d < 5; ++d) { + const ivec2 q = wc + ivec2(d == 1 ? 1 : d == 2 ? -1 : 0, d == 3 ? 1 : d == 4 ? -1 : 0) - ivec2(pc.view_x, pc.view_y); + if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue; + const uint cw = cells[q.y * pc.gw + q.x], cm = cell_mat(cw); + if (cm == 0u) continue; + const uint nz = mats[3u * cm + 2u].z, nw = mats[3u * cm + 2u].w; + if (((nz >> 18) & 1u) != 0u) wet = true; + const bool nBurning = (cw & BURN_BIT) != 0u; + if (!(nBurning || ((nz >> 17) & 1u) != 0u)) continue; + int nfi = fade_of(birth[q.y * pc.gw + q.x]); + if (!nBurning && nfi == 0) nfi = int((nw >> 8) & 255u); + const float nf = float(nfi); + const float gate = nfi < 0 ? 0.0 : min(nf + 1.0, 101.0) / 101.0; + float pSrc = gate * 0.25 * pIgnite; + if ((mflags(cm) & F_FIRE) != 0u) pSrc = 1.0 - (1.0 - pSrc) * (1.0 - pSrc); // a fire cell attempts twice + keep *= 1.0 - min(pSrc, 1.0); + if (rnd(seed, uint(pc.frame), 20u + uint(d)) < pSrc && bestFade < 0.0) bestFade = nf; // the fade of the source that ignited it + } + if (!wet && bestFade >= 0.0 && rnd(seed, uint(pc.frame), 1u) > keep) burn[base + uint(px)] = BURNING | (uint(min(bestFade, 255.0)) << 16) | (st & 0xFFFFu); + continue; + } + // burning + uint age = st & 0xFFFFu; + int fade = int((st >> 16) & 255u); + bool wet = false; + for (int d = 0; d < 5 && !wet; ++d) { + const ivec2 q = wc + ivec2(d == 1 ? 1 : d == 2 ? -1 : 0, d == 3 ? 1 : d == 4 ? -1 : 0) - ivec2(pc.view_x, pc.view_y); + if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue; + const uint cm = cell_mat(cells[q.y * pc.gw + q.x]); + wet = cm != 0u && ((mats[3u * cm + 2u].z >> 18) & 1u) != 0u; + } + if (wet) { burn[base + uint(px)] = age; continue; } + bool flameAccepted = false; + if (flameP > 0 && int(rnd(seed, uint(pc.frame), 2u) * 101.0) < flameP) { + const ivec2 t = wc + ivec2(int(rnd(seed, uint(pc.frame), 3u) * 3.0) - 1, -1) - ivec2(pc.view_x, pc.view_y); + if (t.x >= 0 && t.y >= 0 && t.x < pc.gw && t.y < pc.gh) { + const int ti = t.y * pc.gw + t.x; + const uint cur = cells[ti]; + if (cell_mat(cur) == 0u) { // an empty place (the word may hold a leftover colour variation): the flame takes it + flameAccepted = atomicCompSwap(cells[ti], cur, flame_material(m, sp.x) | (hash3(seed, uint(pc.frame), 4u) & 0xFF0000u)) == cur; + if (flameAccepted) birth[ti] = with_fade(birth_frame(), int((mats[3u * flame_material(m, sp.x) + 2u].w >> 8) & 255u)); // (0070f8c0: a new flame has its material's own fade, temperature_of_fire; the intensity is a separate field) + } + } + } + if (needO2) fade = flameAccepted ? tempFire : fade - 1; + if (smokeP > 0 && int(rnd(seed, uint(pc.frame), 6u) * 101.0) < smokeP) { + const ivec2 t = wc + ivec2(int(rnd(seed, uint(pc.frame), 7u) * 3.0) - 1, -1) - ivec2(pc.view_x, pc.view_y); + if (t.x >= 0 && t.y >= 0 && t.x < pc.gw && t.y < pc.gh) atomicCompSwap(cells[t.y * pc.gw + t.x], 0u, smoke_material(m, sp.y) | (hash3(seed, uint(pc.frame), 8u) & 0xFF0000u)); + } + if (fade < 1) { burn[base + uint(px)] = age; continue; } // out of oxygen: the pixel stops burning + // one spread attempt towards one of the four neighbours inside the body + if (int(hash3(seed, uint(pc.frame), 9u) % 101u) <= fade) { + const uint dsel = hash3(seed, uint(pc.frame), 10u) & 3u; + const ivec2 np = pp + (dsel == 0u ? ivec2(0, -1) : dsel == 1u ? ivec2(-1, 0) : dsel == 2u ? ivec2(1, 0) : ivec2(0, 1)); + if (shape_solid(s, np.x, np.y) && int(hash3(seed, uint(pc.frame), 11u) % 101u) < 100 - ai) { + const uint ni = base + uint(np.y * w + np.x), old = burn[ni]; + if ((old & (BURNING | BURNT)) == 0u) atomicCompSwap(burn[ni], old, BURNING | (uint(fade) << 16) | (old & 0xFFFFu)); + } + } + age = (age + 1u) & 0xFFFFu; // 0070e060: a wrapping 16-bit age + if ((pc.frame & 31) == 0 && !noBurnOut && fireHp < int(age)) { + const int ei = atomicAdd(ctrl.burnN.x, 1); + if (ei < 256) { ctrl.burnEv[ei] = ivec4(bi, px, 0, 0); burn[base + uint(px)] = BURNT | age; continue; } + atomicAdd(ctrl.burnN.x, -1); + } + burn[base + uint(px)] = BURNING | (uint(fade) << 16) | age; + } +} diff --git a/shaders/rigid_commit.comp b/shaders/rigid_commit.comp new file mode 100644 index 0000000..67f0fa6 --- /dev/null +++ b/shaders/rigid_commit.comp @@ -0,0 +1,11 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 64) in; +#include "rigid_common.glsl" + +// Copies the sub-step result back into the body state. +void main() { + uint b = gl_GlobalInvocationID.x; + if (int(b) >= bhdr.x) return; + bodies[b] = bnext[b]; +} diff --git a/shaders/rigid_common.glsl b/shaders/rigid_common.glsl new file mode 100644 index 0000000..8fc74a2 --- /dev/null +++ b/shaders/rigid_common.glsl @@ -0,0 +1,71 @@ +// Shared declarations of the GPU rigid-body engine (rigid_solve.comp, rigid_commit.comp, rigid_frame.comp). +// The engine is the GPU port of src/physics/rigid.cpp: bodies are pixel masks, terrain is the live cell grid, contacts are aggregated +// into one manifold per pair and resolved with impulses. Bodies are solved Jacobi-style: every body reads the state of the others +// from the previous sub-step (`bodies`) and writes its own result to `bnext`; rigid_commit copies bnext back. +#include "common.glsl" + +struct Ent { + vec4 a, box, mv, ai; + ivec4 st; + vec4 hp; + ivec4 x, rg, rg2, aux; + vec4 ex; // detection cone (cos of half angle), eye offset y, melee enabled, 0 + vec4 ex2; // ranged attack: projectile speed, gravity + ivec4 ex3; // blood material + 1, blood colour, damage types taken since the last check (bit 1 projectile, 2 explosion, 4 fire, 8 melee), 0 + vec4 ex4; // air in the lungs (frames), air maximum (0 = does not need air), damage per frame without air, frames left on fire + vec4 ex5; // swimming: swim_idle_buoyancy_coeff, swim_up_buoyancy_coeff, swim_drag, swim_extra_horizontal_drag + vec4 ex6; // swim_down_buoyancy_coeff, frames not swimming, pixel of the last liquid count (x, y) + vec4 ex7; // blood_multiplier (creatures); liquid inventory of a body: reserve, material + 1, frames of emission left + ivec4 ex8; vec4 ex9; vec4 ex10; vec4 ex11; vec4 ex12, ex13, ex14, ex15, ex16, ex17, ex18, ex19; // damage event of a body: hit point relative to the body x 1000, damage x 1000 pending +}; +layout(std430, set = 0, binding = 22) buffer Ents { Ent ents[]; }; + +// shape table: 16 words per shape (w, h, mask offset, sample offset, sample count, com x, com y, mass, inertia, radius as float bits), +// followed by the pixel masks (one word per pixel) and the outline samples (two float words each). +layout(std430, set = 0, binding = 30) readonly buffer Shapes { uint sh[]; }; + +struct Body { + vec4 p; // centre of mass x, y, angle, angular velocity + vec4 v; // velocity x, y, restitution, friction + ivec4 i; // shape, entity slot, flags (1 asleep, 2 removed), quiet frames + vec4 q; // anchor x, y (the entity origin in image coordinates), density, probe counter + vec4 l; // x, y, angle at the last sleep check + vec4 n; // fragile: impact speed (cells per frame) above which it is damaged, 0 = unbreakable / nail point relative to the entity origin (x, y) / radius in which a surface holds the nail; flag B_NAILED + vec4 m; // collision group (bodies of one group do not collide), y = 1: passable for creatures and projectiles (corpses) + ivec4 jn; // joint partners (body indices, -1 = none) + vec4 ja0, ja1, ja2, ja3; // joint anchors (x, y on this body, x, y on the partner), relative to the centres of mass, in the bodies' own frames + vec4 jb; // break_force multiplier of each joint slot (0 = the joint does not break from force); l.w is the multiplier of the nail + vec4 d; // component values: buoyancy, linear damping, angular damping +}; +layout(std430, set = 0, binding = 31) buffer Bodies { ivec4 bhdr; Body bodies[]; }; // bhdr.x = body count +layout(std430, set = 0, binding = 32) buffer BNext { ivec4 nhdr; Body bnext[]; }; + +const int B_ASLEEP = 1, B_REMOVED = 2, B_NAILED = 4; // nailed: hangs on the wall until the cells around the nail are gone + +int shi(int s, int k) { return int(sh[s * 16 + k]); } +float shf(int s, int k) { return uintBitsToFloat(sh[s * 16 + k]); } +bool shape_solid(int s, int x, int y) { + int w = shi(s, 0), h = shi(s, 1); + return x >= 0 && y >= 0 && x < w && y < h && sh[shi(s, 2) + y * w + x] != 0u; +} +vec2 sample_of(int s, int k) { + int o = shi(s, 3) + 2 * k; + return vec2(uintBitsToFloat(sh[o]), uintBitsToFloat(sh[o + 1])); +} +vec2 rot2(float c, float s, vec2 v) { return vec2(c * v.x - s * v.y, s * v.x + c * v.y); } + +bool solid_at(ivec2 w) { + ivec2 p = w - ivec2(pc.view_x, pc.view_y); + if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false; // no ground outside the window: bodies there are frozen + uint m = cell_mat(cells[p.y * pc.gw + p.x]); + if (m == 0u) return false; + return (mflags(m) & (F_STATIC | F_SAND)) != 0u; +} +bool liquid_at(ivec2 w) { + ivec2 p = w - ivec2(pc.view_x, pc.view_y); + if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false; + uint m = cell_mat(cells[p.y * pc.gw + p.x]); + if (m == 0u) return false; + uint f = mflags(m); + return (f & F_LIQCELL) != 0u && (f & F_POWDER) == 0u; // liquid and not powder (a static liquid counts too) +} diff --git a/shaders/rigid_events.comp b/shaders/rigid_events.comp new file mode 100644 index 0000000..f3c90c1 --- /dev/null +++ b/shaders/rigid_events.comp @@ -0,0 +1,53 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 64) in; +#include "rigid_common.glsl" + +// Explosions (from projectiles on the GPU, from props on the host) push the rigid bodies and wake them. +layout(std430, set = 0, binding = 37) buffer Xev { ivec4 xhead; vec4 xe[512]; }; + +void main() { + uint bi = gl_GlobalInvocationID.x; + if (int(bi) >= bhdr.x) return; + Body B = bodies[bi]; + if ((B.i.z & B_REMOVED) != 0) return; + int s = B.i.x; + float mass = shf(s, 7), radius = shf(s, 9); + bool touched = false; + for (int k = 0; k < min(xhead.x, 512); ++k) { + vec4 e = xe[k]; // x, y, radius, power + if (e.w < -0.5) { // an explosion with the force of 00657960: this entry and the next one (force min, force max) + vec4 f2 = xe[min(k + 1, 511)]; + ++k; + const float pixels = float(shi(s, 4) > 0 ? int(shf(s, 7)) : 1); + const float massKg = max(pixels * B.q.z * 0.01, 1.0e-4); + vec2 O = e.xy, C = B.p.xy; + float dist = length(C - O) * 0.1, Ru = e.z * 0.1; // physics units (10 pixels each) + if (dist > Ru) continue; + // the original needs the angular bin of the explosion to reach this body: approximated by a line of sight through the grid that no solid cell interrupts + bool blocked = false; + const int steps = int(dist * 10.0); + for (int q = 1; q < steps && !blocked; ++q) blocked = solid_at(ivec2(floor(mix(O, C, float(q) / float(steps))))); + if (blocked) continue; + float t = Ru == 0.0 ? 1.0 : 1.0 - dist / Ru; + float A = (f2.y - f2.x) * t + f2.x; + vec2 n = dist >= 1.19e-7 ? normalize(C - O) : vec2(0.0); + // F = n A (x mass if the body weighs less than 1), x 360 N, dv = F / m / 360 per frame in pixels: = n A min(1, m) / m + B.v.xy += n * A * min(1.0, massKg) / massKg; + const float inertiaKg = max(shf(s, 8) * B.q.z * 1.0e-4, 1.0e-6); + const float tau = float(hash3(bi, uint(xhead.x), 91u) & 0x7FFFu) / 32767.0 * 30.0 - 15.0; // the random torque of the original + B.p.w += tau / (3600.0 * inertiaKg); + touched = true; + continue; + } + vec2 d = B.p.xy - e.xy - vec2(0.0, 2.0); + float dist = length(d); + if (dist > e.z + radius) continue; + float f = e.w * max(0.0, 1.0 - dist / (e.z + radius)) / sqrt(max(1.0, mass * B.q.z) / 20.0); + vec2 n = dist > 1e-3 ? d / dist : vec2(0.0, -1.0); + B.v.xy += n * f; + B.p.w += (n.x > 0.0 ? 1.0 : -1.0) * f * 0.05; + touched = true; + } + if (touched) { B.i.z &= ~B_ASLEEP; B.i.w = 0; bodies[bi] = B; } +} diff --git a/shaders/rigid_frame.comp b/shaders/rigid_frame.comp new file mode 100644 index 0000000..e984643 --- /dev/null +++ b/shaders/rigid_frame.comp @@ -0,0 +1,67 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 64) in; +#include "rigid_common.glsl" + +// Once per frame: sleeping (resting bodies stop being simulated, sleeping ones re-check their support), and the entity's position and angle. +layout(std430, set = 0, binding = 27) buffer Edits { ivec4 ehead; vec4 ee[512]; }; // cell edits: the flames of a burning corpse + +void main() { + uint bi = gl_GlobalInvocationID.x; + if (int(bi) >= bhdr.x) return; + Body B = bodies[bi]; + if ((B.i.z & B_REMOVED) != 0) return; + int s = B.i.x; + int nS = shi(s, 4); + vec2 com = vec2(shf(s, 5), shf(s, 6)); + float c = cos(B.p.z), sn = sin(B.p.z); + if ((B.i.z & 8) != 0) { + // simulated on the CPU (src/phys): the sleep state comes from there + } else if ((B.i.z & B_ASLEEP) != 0) { + B.q.w += 1.0; + if (B.q.w >= 12.0) { // re-check the support now and then + B.q.w = 0.0; + bool supported = false; + for (int k = 0; k < nS && !supported; ++k) { + vec2 r = rot2(c, sn, sample_of(s, k)); + supported = solid_at(ivec2(floor(B.p.x + r.x), floor(B.p.y + r.y + 1.5))); + } + if (!supported) { B.i.z &= ~B_ASLEEP; B.i.w = 0; } + } + } else { + // 009be020: the sleep timer counts while the velocity stays under the tolerances (0.01 units per second linear, 2 degrees per second angular; pixels per frame at scale 10); a body + // sleeps when it has been quiet for half a second AND every body joined to it has too (the island's minimum timer) + const bool quiet = length(B.v.xy) < 0.01 * 10.0 / 60.0 && abs(B.p.w) < 0.034906585 / 60.0; + B.i.w = quiet ? B.i.w + 1 : 0; + if (B.i.w >= 30) { + bool island = true; + for (int k = 0; k < 4 && island; ++k) { + const int j = B.jn[k]; + if (j < 0) continue; + const Body C = bodies[j]; + if ((C.i.z & B_REMOVED) != 0 || (C.i.z & B_ASLEEP) != 0) continue; + island = C.i.w >= 30; + } + if (island) { B.i.z |= B_ASLEEP; B.v.xy = vec2(0.0); B.p.w = 0.0; B.i.w = 0; } + } + B.l = vec4(B.p.xy, B.p.z, 0.0); + } + if (B.m.z > 0.0) { // a burning corpse part: flames now and then, water puts it out (the pixels of burnable props burn in rigid_burn.comp) + B.m.z -= 1.0; + ivec2 cc = ivec2(floor(B.p.xy)) - ivec2(pc.view_x, pc.view_y); + if (cc.x >= 0 && cc.y >= 0 && cc.x < pc.gw && cc.y < pc.gh) { + uint cm = cell_mat(cells[cc.y * pc.gw + cc.x]); + if (cm != 0u && ((mats[3u * cm + 2u].z >> 18) & 1u) != 0u) B.m.z = 0.0; + } + if (B.m.z > 0.0 && (hash3(bi, uint(pc.frame), 17u) & 3u) == 0u) { + uint ei = uint(atomicAdd(ehead.x, 1)); + vec2 fp = B.p.xy + vec2(0.0, -1.0); + if (ei < 256u) { ee[2u * ei] = vec4(fp, 1.5, 0.0); ee[2u * ei + 1u] = vec4(1.0, float(mats[mats.length() - 1u].x), 0.45, 0.0); } + } + } + bodies[bi] = B; + if (B.i.y >= 0) { + vec2 d = rot2(c, sn, B.q.xy - com); + ents[B.i.y].a = vec4(B.p.xy + d, B.p.z, 0.0); + } +} diff --git a/shaders/rigid_raster.comp b/shaders/rigid_raster.comp new file mode 100644 index 0000000..548f1cc --- /dev/null +++ b/shaders/rigid_raster.comp @@ -0,0 +1,28 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 64) in; +#include "rigid_common.glsl" + +// Marks the cells covered by the pixel masks of the rigid bodies (those that are solid for creatures and projectiles) in a per-cell grid, so that +// creatures and projectiles test a cell in one read instead of looping over all bodies. One workgroup per body. +layout(std430, set = 0, binding = 40) buffer BodyGrid { uint bodyGrid[]; }; + +void main() { + int bi = int(gl_WorkGroupID.x), t = int(gl_LocalInvocationID.x); + if (bi >= bhdr.x) return; + Body b = bodies[bi]; + if ((b.i.z & 2) != 0 || b.m.y > 0.5) return; // removed, or a corpse part (passable) + int s = b.i.x; + float rad = shf(s, 9); + int n = int(2.0 * rad) + 2; + ivec2 lo = ivec2(floor(b.p.xy - vec2(rad))); + float cs = cos(b.p.z), sn = sin(b.p.z); + for (int k = t; k < n * n; k += 64) { + ivec2 w = lo + ivec2(k % n, k / n); + ivec2 q = w - ivec2(pc.view_x, pc.view_y); + if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue; + vec2 dd = vec2(w) + 0.5 - b.p.xy; + vec2 im = vec2(cs * dd.x + sn * dd.y, -sn * dd.x + cs * dd.y) + vec2(shf(s, 5), shf(s, 6)); + if (shape_solid(s, int(floor(im.x)), int(floor(im.y)))) bodyGrid[q.y * pc.gw + q.x] = (b.d.w > 0.5 ? uint(b.d.w + 0.5) : uint(b.i.y) + 1u) | ((b.i.z & 1024) != 0 ? 0x80000000u : 0u); // (bit 31: flag 1024, no platform: characters pass) // the entity slot + 1 (a part body of a PhysicsBody2 entity: the owner's, d.w; the engine's body user data is the entity) + } +} diff --git a/shaders/rigid_solve.comp b/shaders/rigid_solve.comp new file mode 100644 index 0000000..0b5d736 --- /dev/null +++ b/shaders/rigid_solve.comp @@ -0,0 +1,310 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 64) in; +#include "rigid_common.glsl" +layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; }; // after the noise words: per material friction and restitution of a rigid contact (6 words per material precede) + +// One sub-step of the rigid-body engine, one workgroup per body (a thread per outline sample). Port of PhysicsWorld::step. +shared vec4 sP, sV; // p, v of the body being solved +shared int accN, accPX, accPY, accNX, accNY, accD, accWet, woke; +shared float sImpact; // fastest approach speed into a contact during this sub-step (fragile bodies take damage from it) + +void contact_static(inout vec3 u, vec2 r, vec2 n, float e, float mu, float invM, float invI) { + vec2 vc = u.xy + u.z * vec2(-r.y, r.x); + float vn = dot(vc, n); + if (vn >= 0.0) return; + float rn = r.x * n.y - r.y * n.x; + float k = invM + rn * rn * invI; + float rest = -vn < 0.1667 ? 0.0 : e; // 009c1ec0: restitution only for vn < -1 unit/s (10 px/s = 0.1667 cells per frame, pixel scale 10) + float j = -(1.0 + rest) * vn / k; + u.xy += j * n * invM; + u.z += invI * (r.x * (j * n.y) - r.y * (j * n.x)); + vec2 wc = u.xy + u.z * vec2(-r.y, r.x); + vec2 t = vec2(-n.y, n.x); + float vt = dot(wc, t); + float rt = r.x * t.y - r.y * t.x; + float kt = invM + rt * rt * invI; + float jt = clamp(-vt / kt, -mu * j, mu * j); + u.xy += jt * t * invM; + u.z += invI * (r.x * (jt * t.y) - r.y * (jt * t.x)); +} + +vec2 snap_axis(vec2 g) { + if (abs(g.x) > 2.0 * abs(g.y)) g.y = 0.0; else if (abs(g.y) > 2.0 * abs(g.x)) g.x = 0.0; + return g; +} + +vec2 terrain_normal(ivec2 c) { + vec2 g = vec2(0.0); + for (int dy = -2; dy <= 2; ++dy) + for (int dx = -2; dx <= 2; ++dx) + if ((dx != 0 || dy != 0) && solid_at(c + ivec2(dx, dy))) { + float d = length(vec2(dx, dy)); + g -= vec2(dx, dy) / (d * d); + } + g = snap_axis(g); + float l = length(g); + return l < 1e-6 ? vec2(0.0, -1.0) : g / l; +} + +// A world point inside the mask of body C? Gives the outward normal of C's surface (world, pointing into the point's owner) and the depth. +bool mask_contact(vec2 wpnt, vec4 cp, int sc, out vec2 nw, out float depth) { + float qc = cos(cp.z), qs = sin(cp.z); + vec2 d = wpnt - cp.xy; + vec2 l = vec2(qc * d.x + qs * d.y, -qs * d.x + qc * d.y); + vec2 im = l + vec2(shf(sc, 5), shf(sc, 6)); + ivec2 ic = ivec2(floor(im)); + nw = vec2(0.0, -1.0); depth = 0.5; + if (!shape_solid(sc, ic.x, ic.y)) return false; + vec2 g = vec2(0.0); + for (int dy = -2; dy <= 2; ++dy) + for (int dx = -2; dx <= 2; ++dx) + if ((dx != 0 || dy != 0) && shape_solid(sc, ic.x + dx, ic.y + dy)) { + float dd = length(vec2(dx, dy)); + g -= vec2(dx, dy) / (dd * dd); + } + g = snap_axis(g); + float gl = length(g); + if (gl < 1e-6) { g = l; gl = length(g); if (gl < 1e-6) { g = vec2(0.0, -1.0); gl = 1.0; } } + g /= gl; + for (float t = 0.5; t <= 6.0; t += 0.5) { + depth = t; + if (!shape_solid(sc, int(floor(im.x + g.x * t)), int(floor(im.y + g.y * t)))) break; + } + nw = rot2(qc, qs, g); + return true; +} + +void add_contact(vec2 rel, vec2 n, float depth) { // rel: world point relative to the centre of mass of the body being solved + atomicAdd(accN, 1); + atomicAdd(accPX, int(round(rel.x * 256.0))); + atomicAdd(accPY, int(round(rel.y * 256.0))); + atomicAdd(accNX, int(round(n.x * 1024.0))); + atomicAdd(accNY, int(round(n.y * 1024.0))); + atomicMax(accD, int(depth * 256.0)); +} + +void main() { + int bi = int(gl_WorkGroupID.x); + int t = int(gl_LocalInvocationID.x); + if (bi >= bhdr.x) return; + Body B = bodies[bi]; + bool asleep = (B.i.z & B_ASLEEP) != 0; + vec2 wp0 = B.p.xy - vec2(pc.view_x, pc.view_y); + if ((B.i.z & B_REMOVED) != 0 || wp0.x < 4.0 || wp0.y < 4.0 || wp0.x > float(pc.gw - 4) || wp0.y > float(pc.gh - 4)) { // frozen outside the window + if (t == 0) bnext[bi] = B; + return; + } + if ((B.i.z & B_NAILED) != 0 && (B.i.z & B_REMOVED) == 0) { // hangs on the wall while there is terrain around the nail + vec2 org = B.p.xy + rot2(cos(B.p.z), sin(B.p.z), B.q.xy - vec2(shf(B.i.x, 5), shf(B.i.x, 6))); + ivec2 nc = ivec2(floor(org + B.n.yz)); + bool held = false; + int nr = int(B.n.w); + for (int dy = -nr; dy <= nr; ++dy) + for (int dx = -nr; dx <= nr; ++dx) held = held || (dx * dx + dy * dy <= nr * nr && solid_at(nc + ivec2(dx, dy))); // a surface near the nail point + if (held && B.l.w > 0.0) { // the joint breaks when its reaction force is above (mass * 160) * break_force (docs/IMPLEMENTATION_BLOCKERS.md): the nail holds the body still, so the + // reaction is the whole acceleration it would have had: gravity plus the velocity change the impulses of this frame gave it (explosions, kicks), in cells per frame^2 (x 360 = m/s^2) + const vec2 a = B.v.xy + vec2(0.0, 0.1667); + const float lim = 160.0 * B.l.w / 360.0; + if (dot(a, a) > lim * lim) held = false; + } + if (held) { B.v.xy = vec2(0.0); B.p.w = 0.0; if (t == 0) bnext[bi] = B; return; } // (the impulses of the frame went into the wall) + B.i.z &= ~(B_NAILED | B_ASLEEP); // the wall is gone or the joint broke: it falls + B.i.w = 0; + asleep = false; + } + int s = B.i.x; + int nS = shi(s, 4); + float mass = shf(s, 7), inertia = shf(s, 8), radius = shf(s, 9); + float invM = 1.0 / (mass * B.q.z), invI = 1.0 / (inertia * B.q.z); + const float dt = 0.5; + if (t == 0) { sP = B.p; sV = B.v; accWet = 0; woke = 0; accN = 0; accPX = 0; accPY = 0; accNX = 0; accNY = 0; accD = 0; sImpact = 0.0; } + barrier(); + + if (!asleep) { + // buoyancy and drag in liquids + // 00bda390 / 00bde0f0 (docs/LIQUID_PHYSICS_COMPLETE_GUIDE.md section 8): ONE probe at (int(x), int(y) + 8) decides; in a liquid (not a powder) the force is -b m (v + g gravity_scale) at the centre, + // b = the component's buoyancy (PhysicsBody2Component constructor 00672980: 0.7). dv/dt = g (1 - b) - b v, no submerged fraction, no density, no extra rotation damping. + float c = cos(sP.z), sn = sin(sP.z); + if (t == 0) { // the probe sits at the ENTITY ORIGIN (the image point B.q, not the centre of mass) plus 8 in y, float coordinates truncating toward zero + const vec2 org = sP.xy + rot2(c, sn, B.q.xy - vec2(shf(s, 5), shf(s, 6))); + if (liquid_at(ivec2(int(org.x), int(org.y) + 8))) accWet = 1; + } + barrier(); + if (t == 0) { + const float buoy = B.d.x * float(accWet); // the component's buoyancy + vec4 P = sP, V = sV; + const float h = dt / 60.0; // 009be020: v = (v + h (g + F / m)) / (1 + h linear_damping), the damping acts on the gravity just added + V.y += 0.1667 * dt * (1.0 - buoy); + V.xy *= (1.0 - buoy * dt / 60.0) / (1.0 + h * B.d.y); + P.w /= 1.0 + h * B.d.z; + float sp = length(V.xy); + if (sp > 2500.0 / 60.0) V.xy *= (2500.0 / 60.0) / sp; // the initial cap: 250 physics units per second (x 10 pixels) + P.w = clamp(P.w, -1.5707963, 1.5707963); // 009be020: |h omega| <= pi / 2 + vec2 stepv = V.xy * dt; + if (length(stepv) > 20.0) stepv *= 20.0 / length(stepv); // after the constraints a translation of at most 2 physics units (20 pixels) per step + P.xy += stepv; + P.z += P.w * dt; + sP = P; sV = V; + } + barrier(); + // terrain contacts: outline samples inside solid cells form one aggregated manifold + c = cos(sP.z); sn = sin(sP.z); + if (t < nS) { + vec2 r = rot2(c, sn, sample_of(s, t)); + vec2 pnt = sP.xy + r; + ivec2 cell = ivec2(floor(pnt)); + if (solid_at(cell)) { + vec2 nm = terrain_normal(cell); + float depth = 0.5; + for (float tt = 0.5; tt <= 6.0; tt += 0.5) { + depth = tt; + if (!solid_at(ivec2(floor(pnt + nm * tt)))) break; + } + add_contact(r, nm, depth); + } + } + barrier(); + if (t == 0 && accN > 0) { + vec2 cp = vec2(float(accPX), float(accPY)) / (256.0 * float(accN)); + vec2 an = vec2(float(accNX), float(accNY)) / 1024.0; + float al = length(an); + if (al > 1e-6) { + vec3 u = vec3(sV.xy, sP.w); + sImpact = max(sImpact, -dot(u.xy + u.z * vec2(-cp.y, cp.x), an / al)); + // the contacted terrain material: its own solid_friction / solid_restitution (009c18c0: friction sqrt(fA fB), restitution max) + float tf = 0.3, tr = 0.2; + { + const ivec2 cc = ivec2(floor(sP.xy + cp)); + for (int k = 0; k < 9; ++k) { + const ivec2 q = cc + ivec2(k % 3 - 1, k / 3 - 1) - ivec2(pc.view_x, pc.view_y); + if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue; + const uint tm = cell_mat(cells[q.y * pc.gw + q.x]); + if (tm != 0u && (mflags(tm) & F_STATIC) != 0u) { + const uint tb = 1024u + 6u * mats[mats.length() - 1u].w + 2u * tm; + tf = uintBitsToFloat(tbl[tb]); tr = uintBitsToFloat(tbl[tb + 1u]); + break; + } + } + } + for (int it = 0; it < 2; ++it) contact_static(u, cp, an / al, max(B.v.z, tr), sqrt(B.v.w * tf), invM, invI); + sV.xy = u.xy; sP.w = u.z; + float push = min(float(accD) / 256.0, 3.0) * 0.7; + sP.xy += an / al * push; + } + } + barrier(); + } + + // contacts with other bodies: my outline in their mask and theirs in mine + for (int j = 0; j < bhdr.x; ++j) { + if (j == bi) continue; + Body C = bodies[j]; + if ((C.i.z & B_REMOVED) != 0) continue; + if (B.m.x > 0.5 && B.m.x == C.m.x) continue; // parts of one corpse do not collide with each other + bool casleep = (C.i.z & B_ASLEEP) != 0; + if (asleep && casleep) continue; + int sc = C.i.x; + float rc = shf(sc, 9); + if (distance(sP.xy, C.p.xy) > radius + rc) continue; + if (t == 0) { accN = 0; accPX = 0; accPY = 0; accNX = 0; accNY = 0; accD = 0; } + barrier(); + vec4 myp = vec4(sP.xy, sP.z, 0.0); + float c = cos(sP.z), sn = sin(sP.z); + if (t < nS) { + vec2 w = sP.xy + rot2(c, sn, sample_of(s, t)); + vec2 nw; float depth; + if (mask_contact(w, C.p, sc, nw, depth)) add_contact(w - sP.xy, nw, depth); + } + int ncs = shi(sc, 4); + if (t < ncs) { + float cc = cos(C.p.z), cs = sin(C.p.z); + vec2 w = C.p.xy + rot2(cc, cs, sample_of(sc, t)); + vec2 nw; float depth; + if (mask_contact(w, myp, s, nw, depth)) add_contact(w - sP.xy, -nw, depth); + } + barrier(); + if (t == 0 && accN > 0) { + if (asleep && !casleep && length(C.v.xy) > 0.15) woke = 1; // a moving neighbour wakes a sleeping body; resting contacts do not + if (!asleep) { + float invMc = 1.0 / (shf(sc, 7) * C.q.z), invIc = 1.0 / (shf(sc, 8) * C.q.z); + float e = max(B.v.z, C.v.z), mu = sqrt(B.v.w * C.v.w); + vec2 ra = vec2(float(accPX), float(accPY)) / (256.0 * float(accN)); + vec2 rcv = sP.xy + ra - C.p.xy; + vec2 sn_ = vec2(float(accNX), float(accNY)) / 1024.0; + float al = length(sn_); + if (al > 1e-6) { + vec2 n = sn_ / al; + vec3 u = vec3(sV.xy, sP.w), uc = vec3(C.v.xy, C.p.w); // uc follows the partner's response virtually, only my side is applied + for (int it = 0; it < 2; ++it) { + vec2 va = u.xy + u.z * vec2(-ra.y, ra.x), vc = uc.xy + uc.z * vec2(-rcv.y, rcv.x); + float vn = dot(va - vc, n); + if (vn >= 0.0) break; + if (it == 0) sImpact = max(sImpact, -vn); + float ran = ra.x * n.y - ra.y * n.x, rcn = rcv.x * n.y - rcv.y * n.x; + float kk = invM + invMc + ran * ran * invI + rcn * rcn * invIc; + float jn = -(1.0 + (-vn < 0.1667 ? 0.0 : e)) * vn / kk; + u.xy += jn * n * invM; u.z += invI * (ra.x * (jn * n.y) - ra.y * (jn * n.x)); + uc.xy -= jn * n * invMc; uc.z -= invIc * (rcv.x * (jn * n.y) - rcv.y * (jn * n.x)); + vec2 wa = u.xy + u.z * vec2(-ra.y, ra.x), wc = uc.xy + uc.z * vec2(-rcv.y, rcv.x); + vec2 tg = vec2(-n.y, n.x); + float vt = dot(wa - wc, tg); + float rat = ra.x * tg.y - ra.y * tg.x, rct = rcv.x * tg.y - rcv.y * tg.x; + float kt = invM + invMc + rat * rat * invI + rct * rct * invIc; + float jt = clamp(-vt / kt, -mu * jn, mu * jn); + u.xy += jt * tg * invM; u.z += invI * (ra.x * (jt * tg.y) - ra.y * (jt * tg.x)); + uc.xy -= jt * tg * invMc; uc.z -= invIc * (rcv.x * (jt * tg.y) - rcv.y * (jt * tg.x)); + } + sV.xy = u.xy; sP.w = u.z; + float push = min(float(accD) / 256.0, 3.0) * 0.6; + sP.xy += n * push * invM / (invM + invMc); + } + } + } + barrier(); + } + + // pin joints (ragdolls): the two anchors are pulled together; every body applies its own side of the impulse + int brokenJoints = 0; + if (t == 0) { + for (int k = 0; k < 4; ++k) { + int j = B.jn[k]; + if (j < 0) continue; + Body C = bodies[j]; + if ((C.i.z & B_REMOVED) != 0) { brokenJoints |= 1 << k; continue; } // the partner is gone (cut away, burnt): the joint is dropped and the body wakes + if (asleep) { if ((C.i.z & B_ASLEEP) == 0 && length(C.v.xy) > 0.15) woke = 1; continue; } + vec4 ja = k == 0 ? B.ja0 : (k == 1 ? B.ja1 : (k == 2 ? B.ja2 : B.ja3)); + int sc = C.i.x; + float invMc = 1.0 / (shf(sc, 7) * C.q.z), invIc = 1.0 / (shf(sc, 8) * C.q.z); + vec2 ra = rot2(cos(sP.z), sin(sP.z), ja.xy), rb = rot2(cos(C.p.z), sin(C.p.z), ja.zw); + vec2 err = (C.p.xy + rb) - (sP.xy + ra); + vec2 va = sV.xy + sP.w * vec2(-ra.y, ra.x), vb = C.v.xy + C.p.w * vec2(-rb.y, rb.x); + vec2 r = (vb - va) + err * 0.35; // residual relative velocity of the anchors, with a pull toward each other + float m = invM + invMc; + mat2 K = mat2(m + invI * ra.y * ra.y + invIc * rb.y * rb.y, -invI * ra.x * ra.y - invIc * rb.x * rb.y, + -invI * ra.x * ra.y - invIc * rb.x * rb.y, m + invI * ra.x * ra.x + invIc * rb.x * rb.x); + vec2 J = inverse(K) * r; + const float jbk = k == 0 ? B.jb.x : (k == 1 ? B.jb.y : (k == 2 ? B.jb.z : B.jb.w)); + if (jbk != 0.0) { // force of the joint: impulse / dt, in newtons (mass 0.01 per pixel, 360 = cells/frame^2 -> m/s^2); limit = (m_a + m_b) * 160 * break_force, + // for the ragdoll kinds 0x2b67 / 0x2b68 (stored negative) max((m_a + m_b) * 400, 200) * break_force + const float M = (shf(s, 7) * B.q.z + shf(sc, 7) * C.q.z) * 0.01; + const float F = length(J) * 0.01 * 360.0 / dt, lim = jbk > 0.0 ? M * 160.0 * jbk : max(M * 400.0, 200.0) * -jbk; + if (F * F > lim * lim) brokenJoints |= 1 << k; + } + sV.xy += J * invM; + sP.w += invI * (ra.x * J.y - ra.y * J.x); + sP.xy += err * 0.15 * invM / m; // and a little of the error is corrected directly + woke = max(woke, 0); + } + } + barrier(); + if (t == 0) { + if (B.n.x > 0.0 && B.i.y >= 0 && sImpact > B.n.x) atomicAdd(ents[B.i.y].x.x, int((sImpact - B.n.x) * 750.0)); // fall / impact damage of fragile bodies + Body o = B; + o.p = sP; + o.v.xy = sV.xy; + for (int k = 0; k < 4; ++k) if ((brokenJoints & (1 << k)) != 0) { o.jn[k] = -1; woke = 1; } + if (woke != 0) { o.i.z &= ~B_ASLEEP; o.i.w = 0; } + bnext[bi] = o; + } +} diff --git a/shaders/simplex3.glsl b/shaders/simplex3.glsl new file mode 100644 index 0000000..a64b3e4 --- /dev/null +++ b/shaders/simplex3.glsl @@ -0,0 +1,23 @@ +// (needs the Tables buffer `tbl`: its first 512 words are the permutation table) +// 00872560: Gustavson's 3D simplex noise (skew 1/3, unskew 1/6, falloff 0.6, x 32) on the permutation table at the start of Tables (00fdf530 holds the same values) +const ivec3 G3[12] = ivec3[12](ivec3(1,1,0), ivec3(-1,1,0), ivec3(1,-1,0), ivec3(-1,-1,0), ivec3(1,0,1), ivec3(-1,0,1), ivec3(1,0,-1), ivec3(-1,0,-1), ivec3(0,1,1), ivec3(0,-1,1), ivec3(0,1,-1), ivec3(0,-1,-1)); +float corner3(int gi, vec3 d) { float t = 0.6 - dot(d, d); if (t < 0.0) return 0.0; t *= t; return t * t * dot(vec3(G3[gi]), d); } +float simplex3(vec3 v) { + const float F3 = 1.0 / 3.0, G = 1.0 / 6.0; + const float s = (v.x + v.y + v.z) * F3; + const ivec3 ijk = ivec3(floor(v + s)); + const float t = float(ijk.x + ijk.y + ijk.z) * G; + const vec3 d0 = v - (vec3(ijk) - t); + ivec3 o1, o2; + if (d0.x >= d0.y) { if (d0.y >= d0.z) { o1 = ivec3(1,0,0); o2 = ivec3(1,1,0); } else if (d0.x >= d0.z) { o1 = ivec3(1,0,0); o2 = ivec3(1,0,1); } else { o1 = ivec3(0,0,1); o2 = ivec3(1,0,1); } } + else { if (d0.y < d0.z) { o1 = ivec3(0,0,1); o2 = ivec3(0,1,1); } else if (d0.x < d0.z) { o1 = ivec3(0,1,0); o2 = ivec3(0,1,1); } else { o1 = ivec3(0,1,0); o2 = ivec3(1,1,0); } } + const vec3 d1 = d0 - vec3(o1) + G, d2 = d0 - vec3(o2) + 2.0 * G, d3 = d0 - 1.0 + 3.0 * G; + const int ii = ijk.x & 255, jj = ijk.y & 255, kk = ijk.z & 255; + #define P(n) int(tbl[uint(n)]) + const int g0 = P(ii + P(jj + P(kk))) % 12; + const int g1 = P(ii + o1.x + P(jj + o1.y + P(kk + o1.z))) % 12; + const int g2 = P(ii + o2.x + P(jj + o2.y + P(kk + o2.z))) % 12; + const int g3 = P(ii + 1 + P(jj + 1 + P(kk + 1))) % 12; + #undef P + return 32.0 * (corner3(g0, d0) + corner3(g1, d1) + corner3(g2, d2) + corner3(g3, d3)); +} diff --git a/shaders/sky_cols.comp b/shaders/sky_cols.comp new file mode 100644 index 0000000..ecb7f16 --- /dev/null +++ b/shaders/sky_cols.comp @@ -0,0 +1,18 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 64) in; +#include "common.glsl" + +// The sky is visible in a window column above its first solid cell (static or powder); this finds that cell for every column (the sky buffer: words 64 ...). +layout(std430, set = 0, binding = 44) buffer Sky { uint sky[]; }; + +void main() { + int x = int(gl_GlobalInvocationID.x); + if (x >= pc.gw) return; + int top = pc.gh; + for (int y = 0; y < pc.gh; ++y) { + uint m = cell_mat(cells[y * pc.gw + x]); + if (m != 0u && (mflags(m) & (F_STATIC | F_SAND)) != 0u) { top = y; break; } + } + sky[64 + x] = uint(top); +} diff --git a/shaders/sprites.comp b/shaders/sprites.comp new file mode 100644 index 0000000..e2dd7a5 --- /dev/null +++ b/shaders/sprites.comp @@ -0,0 +1,132 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 64) in; +#include "common.glsl" + +// Draws entity sprites into the per-cell entity layer. One workgroup per sprite instance. +// The sprite is placed at its entity current position (entities.comp moves them), mirrored when the entity faces left, and +// animated: mobile entities play their walk animation while moving and the idle one otherwise. +struct Ent { vec4 a, box, mv, ai; ivec4 st; vec4 hp; ivec4 x; ivec4 rg; ivec4 rg2; ivec4 aux; vec4 ex; vec4 ex2; ivec4 ex3; vec4 ex4; vec4 ex5; vec4 ex6; vec4 ex7; ivec4 ex8; vec4 ex9; vec4 ex10; vec4 ex11; vec4 ex12, ex13, ex14, ex15, ex16, ex17, ex18, ex19; }; +layout(std430, set = 0, binding = 19) readonly buffer EntTexels { uint enttex[]; }; +layout(std430, set = 0, binding = 20) readonly buffer EntInst { ivec4 entinst[]; }; // 4 per instance, see main.cpp +layout(std430, set = 0, binding = 21) buffer EntLayer { uint entlayer[]; }; +layout(std430, set = 0, binding = 22) readonly buffer Ents { Ent ents[]; }; +layout(std430, set = 0, binding = 23) readonly buffer Ctrl { ivec4 inp; ivec4 inp2; ivec4 inp3; vec4 outp; vec4 outp2; vec4 view; vec4 hand0; ivec4 hand1; vec4 hand2; } ctrl; +layout(std430, set = 0, binding = 39) readonly buffer EntPrev { vec4 prevpos[]; }; // positions before the last tick +layout(std430, set = 0, binding = 54) buffer EntDepth { uint entdepth[]; }; +// the rigid bodies (rigid_common.glsl) and the burn state of their pixels (rigid_burn.comp: bits 0-15 the burn age): a burnt pixel is charred +struct SBody { vec4 p; vec4 v; ivec4 i; vec4 q; vec4 l; vec4 n; vec4 m; ivec4 jn; vec4 ja0, ja1, ja2, ja3; vec4 jb; vec4 d; }; +layout(std430, set = 0, binding = 31) readonly buffer Bodies { ivec4 bhdr; SBody bodies[]; }; +layout(std430, set = 0, binding = 41) readonly buffer Burn { uint burnpx[]; }; +shared int sBody; +// the burning-cell palette (main.cpp puts colors_lava.bmp after the tables; render.comp / glow.comp read it the same way) +layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; }; +uint fpal_word(uint k) { return tbl[1024u + 9u * mats[mats.length() - 1u].w + k]; } +uint burning_texel(uint m, uint h) { // 00724580: a burning cell takes a random pixel of colors_lava.bmp (the row of its fire_colors_index) every frame; alpha 253 marks it for glow.comp + const uint row = min(fpal_word(132u + m), 5u); + return (fpal_word(96u + row * 6u + h % 6u) & 0x00FFFFFFu) | 0xFD000000u; +} // per cell: the depth key of the nearest opaque texel (cleared to ~0 every frame) + +// The depth of an instance: SpriteComponent z_index (larger = further back; the instance keeps z x 100), then the later instance in front of an earlier one of the same z +uint gKey = 0xFFFFFFFFu; +uint gFlash = 0u; // the hit flash (entities.comp hit_flash): 0, or the colour every opaque texel takes (white; red for a critical hit), keeping its alpha +float gFade = 0.0; // LifetimeComponent fade_sprites (00b9a010): the sprites' alpha is 1 - this (ex10.w of an entity that is not a creature; the host sets it) + + +// Two passes: 0 draws the ordinary texels, 1 the additive ones (alpha 3: the glow of emissive sprites). The glow is added to what is already in the cell (the creature under +// its eyes keeps its pixels); drawing both kinds in one dispatch let the writers of a cell race, which made the creature vanish behind its glow in some frames. +void put_texel(int idx, uint t) { + uint a = t >> 24; + if (a == 0u) return; + if (gFade > 0.0 && a >= 4u && a != 253u && a != 254u) { // (the alpha byte keeps clear of the markers 1-3, 253, 254) + a = uint(float(a) * (1.0 - gFade) + 0.5); + if (a < 4u) return; + a = min(a, 252u); + t = (t & 0x00FFFFFFu) | (a << 24); + } + const bool additive = a == 3u; + if (gFlash != 0u && !additive && a >= 4u) t = (t & 0xFF000000u) | gFlash; + if (pc.pass_ == 2) { if (!additive) atomicMin(entdepth[idx], gKey); return; } // depth pass + if (!additive && entdepth[idx] != gKey) return; // hidden by a nearer texel + if (additive != (pc.pass_ == 1)) return; + if (!additive) { entlayer[idx] = a == 1u ? (t | 0x02000000u) & 0xFEFFFFFFu : t; return; } + const uint cur = entlayer[idx]; + if (cur == 0u) { entlayer[idx] = t; return; } + const uvec3 s = uvec3((cur >> 16) & 255u, (cur >> 8) & 255u, cur & 255u) + uvec3((t >> 16) & 255u, (t >> 8) & 255u, t & 255u); + const uvec3 c = min(s, uvec3(255u)); + entlayer[idx] = (cur & 0xFF000000u) | (c.r << 16) | (c.g << 8) | c.b; +} + +void main() { + uint id = gl_WorkGroupID.x; + ivec4 h0 = entinst[4u * id], hi = entinst[4u * id + 1u], hw = entinst[4u * id + 2u]; + Ent e = ents[h0.x]; + gKey = (uint(clamp(h0.w + 32768, 0, 65535)) << 16) | (65535u - (id & 65535u)); + if ((e.st.w & (8 | 4096)) != 0) return; // dead, or its sprites switched off (4096: a creature, main.cpp on_components_enabled) + if (e.st.x != 4 && (e.ex8.x & 8192) != 0) return; + { // the hit flash: creatures, the player and physics items (a worm segment, kind 0, keeps its root slot in ex19.w) + const int fc = int(e.ex19.w + 0.5); + if (e.st.x != 0 && (fc & 7) >= 3) gFlash = (fc & 8) != 0 ? 0xFF0000u : 0xFFFFFFu; + } + gFade = (e.st.x == 1 || e.st.x == 2 || e.st.x == 3) ? 0.0 : clamp(e.ex10.w, 0.0, 1.0); // INVISIBILITY active (a prop's ex8.xy is the hit point of its leak) (00b015d0 case 0x51: its sprites are told not to draw) + { vec2 pv = prevpos[h0.x].xy; if (distance(pv, e.a.xy) < 10.0) e.a.xy = mix(pv, e.a.xy, ctrl.view.z); } // drawn between the last two ticks + const bool rigid = (e.st.w & (32 | 64)) != 0; // drawn rotated by the body angle in a.z (64: the part of a Verlet chain, angle in ex7.x) + bool mobile = e.st.x != 0 && !rigid; + bool moving = mobile && abs(e.a.z) > 0.08; + ivec4 an = moving ? hw : hi; // tex, w, h, frames | wait << 16 + int w = an.y, h = an.z, frames = an.w & 0xFFFF, wait = max(an.w >> 16, 1); + int frame = (pc.frame / wait + int(h0.x)) % max(frames, 1); + { // an action the host played (the attacks of the AnimalAI): its animation once from its start frame (row 4 of the instance: texture, frames | wait << 16, start frame, w | h << 16) + const ivec4 hx = entinst[4u * id + 3u]; + const bool loopA = hx.z < 0; // (a negative start: start + 1 negated, a movement animation that loops: jump_up, jump_fall, swim_*, fly_*, burn) + const int af = max(hx.y & 0xFFFF, 1), aw = max(hx.y >> 16, 1), t = pc.frame - (loopA ? -hx.z - 1 : hx.z); + if (hx.x > 0 && t >= 0 && (loopA || t < af * aw) && (hx.w & 0xFFFF) > 0 && (hx.w >> 16) > 0) { w = hx.w & 0xFFFF; h = hx.w >> 16; an = ivec4(hx.x, w, h, hx.y); frames = af; wait = aw; frame = loopA ? (t / aw) % af : t / aw; } // (its own frame size: the zombie attacks in 19 x 20 frames, stands in 18 x 19; the sprite offset is the file's) + } + if (rigid) { + if (gl_LocalInvocationID.x == 0u) sBody = -1; + barrier(); + for (int k = int(gl_LocalInvocationID.x); k < min(bhdr.x, 1024); k += 64) if (bodies[k].i.y == int(h0.x) && (bodies[k].i.z & 2) == 0) sBody = k; + barrier(); + const int body = (e.st.w & 64) == 0 && w * h <= 4096 ? sBody : -1; + int rf = (pc.frame / wait + int(h0.x)) % max(frames, 1); + const float ang = (e.st.w & 64) != 0 ? e.ex7.x : e.a.z; + const float sx = (e.st.w & 64) != 0 && e.ex7.y > 0.0 ? e.ex7.y : 1.0; // a part stretched along its length (the segments of a limb: IKLimbComponent) + float c = cos(ang), s = sin(ang); + vec2 anchor = vec2(float(h0.y), float(h0.z)); + float r = length(max(anchor, vec2(float(w), float(h)) - anchor) * vec2(max(sx, 1.0), 1.0)) + 1.5; + int n = int(2.0 * r) + 2; + ivec2 lo = ivec2(floor(e.a.xy - vec2(r))) - ivec2(pc.view_x, pc.view_y); + int rbase = an.x + rf * w * h; + for (int i = int(gl_LocalInvocationID.x); i < n * n; i += 64) { + ivec2 p = lo + ivec2(i % n, i / n); + if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) continue; + vec2 d = vec2(p + ivec2(pc.view_x, pc.view_y)) + vec2(0.5) - e.a.xy; + ivec2 sp = ivec2(floor(anchor + vec2((c * d.x + s * d.y) / sx, -s * d.x + c * d.y))); + if (sp.x < 0 || sp.y < 0 || sp.x >= w || sp.y >= h) continue; + uint t = enttex[uint(rbase + sp.y * w + sp.x)]; + if (body >= 0 && (t >> 24) > 3u) { // 0070e060: a burnt solid loses one of every channel per eight of its burn age + const uint bst = burnpx[uint(body) * 4096u + uint(sp.y * w + sp.x)]; + const uint bm = uint(e.ex5.z + 0.5); // (the body's material + 1, as rigid_burn.comp reads it) + if ((bst & 0x80000000u) != 0u && bm > 0u) { put_texel(p.y * pc.gw + p.x, burning_texel(bm - 1u, hash3(uint(sp.x + sp.y * w), uint(body), uint(pc.frame)))); continue; } // a burning pixel: the fire colours + const uint dk = (bst & 0xFFFFu) >> 3; + if (dk > 0u) { const uvec3 c3 = uvec3((t >> 16) & 255u, (t >> 8) & 255u, t & 255u); const uvec3 r3 = uvec3(max(ivec3(c3) - int(dk), ivec3(0))); t = (t & 0xFF000000u) | (r3.r << 16) | (r3.g << 8) | r3.b; } + } + put_texel(p.y * pc.gw + p.x, t); // alpha 1 is the marker of additive light in render.comp: faint pixels (the halo of the portal) take alpha 2 + } + return; + } + bool flip = mobile && e.st.y < 0; + ivec2 pos = ivec2(floor(e.a.xy)); + int ox = flip ? (w - 1 - h0.y) : h0.y; + ivec2 origin = pos - ivec2(ox, h0.z) - ivec2(pc.view_x, pc.view_y); // window position of the sprite top-left + if (origin.x >= pc.gw || origin.y >= pc.gh || origin.x + w <= 0 || origin.y + h <= 0) return; + int base = an.x + frame * w * h; + for (int i = int(gl_LocalInvocationID.x); i < w * h; i += 64) { + int sxp = i % w; + ivec2 p = origin + ivec2(sxp, i / w); + if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) continue; + uint t = enttex[uint(base + (flip ? (i / w) * w + (w - 1 - sxp) : i))]; + if (h0.x == 0u && (t >> 24) == 255u) t = (t & 0x00FFFFFFu) | 0xFE000000u; // the player's pixels (alpha 254): render.comp draws them at the sub-cell position + put_texel(p.y * pc.gw + p.x, t); + } +} diff --git a/shaders/terrain.comp b/shaders/terrain.comp new file mode 100644 index 0000000..d6747ed --- /dev/null +++ b/shaders/terrain.comp @@ -0,0 +1,451 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 8, local_size_y = 8) in; +#include "common.glsl" +#include "areas.glsl" +#include "far.glsl" + +// Terrain generation on the GPU: GLSL port of the engine's wang-image -> cell path +// (docs/ENGINE_ANALYSIS.md section 18). One invocation per grid cell. +layout(std430, set = 0, binding = 6) readonly buffer Field { float field[]; }; // wang solidity plane +layout(std430, set = 0, binding = 7) readonly buffer MatPlane { uint matplane[]; }; // CellData index + 1, 0 = none +layout(std430, set = 0, binding = 8) readonly buffer Tables { uint perm[]; }; // [0,512) simplex, [512,1024) perlin +layout(std430, set = 0, binding = 9) readonly buffer MatParams { vec2 mparams[]; }; // per material: warp, threshold + +layout(std430, set = 0, binding = 10) readonly buffer CellsOld { uint cells_old[]; }; +layout(std430, set = 0, binding = 11) readonly buffer ExtOld { vec2 ext_old[]; }; +layout(std430, set = 0, binding = 52) readonly buffer BirthOld { uint birth_old[]; }; // the ages before the scroll + +layout(std430, set = 0, binding = 12) readonly buffer Scene { uint scene[]; }; // pixel scenes: 0 = none, else material + 1 + +layout(std430, set = 0, binding = 18) readonly buffer Override { uint ovr[]; }; // cells restored from the persistent store (0xFFFFFFFF = none) + +int ffloor(float v) { return int(floor(v)); } +float fade(float t) { return t * t * t * (t * (t * 6.0 - 15.0) + 10.0); } +float sstep(float t) { return (3.0 - t * 2.0) * t * t; } + +float sgrad(uint hsh, float x, float y) { + uint h = hsh & 7u; + float u = h < 4u ? x : y; + float v = h < 4u ? y : x; + return (((h & 1u) != 0u) ? -u : u) + (((h & 2u) != 0u) ? -2.0 * v : 2.0 * v); +} +uint ps(uint i) { return perm[i]; } +uint pp(uint i) { return perm[512u + i]; } + +float simplex2(float x, float y) { + const float F2 = 0.366025404, G2 = 0.211324865; + float s = (x + y) * F2; + int i = ffloor(x + s), j = ffloor(y + s); + float t = float(i + j) * G2; + float x0 = x - (float(i) - t), y0 = y - (float(j) - t); + int i1 = x0 > y0 ? 1 : 0, j1 = 1 - i1; + float x1 = x0 - float(i1) + G2, y1 = y0 - float(j1) + G2; + float x2 = x0 - 1.0 + 2.0 * G2, y2 = y0 - 1.0 + 2.0 * G2; + uint ii = uint(i) & 255u, jj = uint(j) & 255u; + float n0 = 0.0, n1 = 0.0, n2 = 0.0; + float t0 = 0.5 - x0 * x0 - y0 * y0; + if (t0 >= 0.0) { t0 *= t0; n0 = t0 * t0 * sgrad(ps(ii + ps(jj)), x0, y0); } + float t1 = 0.5 - x1 * x1 - y1 * y1; + if (t1 >= 0.0) { t1 *= t1; n1 = t1 * t1 * sgrad(ps(ii + uint(i1) + ps(jj + uint(j1))), x1, y1); } + float t2 = 0.5 - x2 * x2 - y2 * y2; + if (t2 >= 0.0) { t2 *= t2; n2 = t2 * t2 * sgrad(ps(ii + 1u + ps(jj + 1u)), x2, y2); } + return 40.0 * (n0 + n1 + n2); +} + +// 8 gradients of FUN_00872a20 +vec2 pgrad(uint g) { + const vec2 G[8] = vec2[8](vec2(-1, 0), vec2(1, 0), vec2(0, -1), vec2(0, 1), vec2(-1, -1), vec2(-1, 1), vec2(1, -1), vec2(1, 1)); + return G[g]; +} + +float perlin2(float x, float y) { + int ix = ffloor(x), iy = ffloor(y); + uint X = uint(ix) & 255u, Y = uint(iy) & 255u, X1 = uint(ix + 1) & 255u, Y1 = uint(iy + 1) & 255u; + float fx = x - float(ix), fy = y - float(iy); + uint pa = pp(X), pb = pp(X1); + float na = dot(pgrad(pp(pa + Y) & 7u), vec2(fx, fy)); + float nb = dot(pgrad(pp(pb + Y) & 7u), vec2(fx - 1.0, fy)); + float nc = dot(pgrad(pp(pa + Y1) & 7u), vec2(fx, fy - 1.0)); + float nd = dot(pgrad(pp(pb + Y1) & 7u), vec2(fx - 1.0, fy - 1.0)); + float v = fade(fy), u = fade(fx); + return mix(mix(na, nc, v), mix(nb, nd, v), u); +} + +float value2(float x, float y) { + int ix = ffloor(x), iy = ffloor(y); + uint X = uint(ix) & 255u, Y = uint(iy) & 255u, X1 = uint(ix + 1) & 255u, Y1 = uint(iy + 1) & 255u; + float fx = x - float(ix), fy = y - float(iy); + uint pa = pp(X), pb = pp(X1); + float v00 = float(pp(pa + Y)) / 255.0, v01 = float(pp(pa + Y1)) / 255.0; + float v10 = float(pp(pb + Y)) / 255.0, v11 = float(pp(pb + Y1)) / 255.0; + float v = fade(fy), u = fade(fx); + return (mix(mix(v00, v01, v), mix(v10, v11, v), u) - 0.5) * 2.0; +} + +int wrapi(int v, int n) { v = v % n; return v < 0 ? v + n : v; } +int gpw, gph, gfo, gmo; // the area of the cell being generated: plane size and offsets +float field_at(int x, int y) { return field[gfo + wrapi(y, gph) * gpw + wrapi(x, gpw)]; } + +// FUN_008712d0: nearest wang pixel (smoothstep(f) >= 0.5 rounds up); CellData index or -1 +int material_pixel(float px, float py) { + int ix = ffloor(px), iy = ffloor(py); + float fx = px - float(ix), fy = py - float(iy); + if (!(0.5 > sstep(fy))) ++iy; + if (!(0.5 > sstep(fx))) ++ix; + return int(matplane[gmo + wrapi(iy, gph) * gpw + wrapi(ix, gpw)]) - 1; +} + +// FUN_00870e60: smoothstep-weighted bilinear sample of the solidity plane +float bilinear(float px, float py) { + int ix = ffloor(px), iy = ffloor(py); + float sx = sstep(px - float(ix)), sy = sstep(py - float(iy)); + float top = mix(field_at(ix, iy), field_at(ix + 1, iy), sx); + float bot = mix(field_at(ix, iy + 1), field_at(ix + 1, iy + 1), sx); + return mix(top, bot, sy); +} + + +// ---- biome evaluator (FUN_0086d2a0) and its noise: 70-scaled simplex (FUN_00872300), 4-octave fbm (FUN_009296d0), polka dots (FUN_00872140) +#include "procgrad.glsl" +// FUN_0086fd40 / FUN_0086fe30: the cell hash (one value, and three more from the same product) +float cell_u(vec2 c) { + float cx = floor(c.x * 0.014084507), cy = floor(c.y * 0.014084507); + float a = c.x - cx * 71.0 + 26.0, b = c.y - cy * 71.0 + 161.0; + return b * b * (a * a); +} +float polka(float x, float y, float rlow, float rhigh, bool boxed, float prob) { + vec2 c = vec2(floor(x), floor(y)); + float u = cell_u(c); + if (fract(u * 0.0010131986) >= prob) return 0.0; + float fx = x - c.x, fy = y - c.y; + float o0 = fract(u * 0.0010513747), o1 = fract(u * 0.0015553312), o2 = fract(u * 0.0012450161); + float r = (rhigh - rlow) * o2 + rlow; + if (r <= 0.0) return 0.0; + float inv = 2.0 / r; + float dx = fx * inv - (inv - 1.0) + (inv - 2.0) * o0; + float dy = fy * inv - (inv - 1.0) + (inv - 2.0) * o1; + float d; + if (boxed) { d = dy * dy; d *= d; float e = dx * dx; d += e * e; if (d > 1.0) return 0.0; } + else { d = min(dx * dx + dy * dy, 1.0); } + float q = 1.0 - d; + return q * q * q; +} +// area a's component list: 20 words each (see AreaStore): [0] material, [1] flags, [2..3] min max, [4..5] add_perlin scale, [6..7] limit min max, [8..9] rare scale, +// [10..11] rare offset, [12..13] required min max, [14..15] polka radius low high, [16] polka probability +uint eval_materials_at(int base, int cnt, float v, ivec2 w) { + float fx = float(w.x), fy = float(w.y); + for (int k = 0; k < cnt; ++k) { + int o = base + 20 * k; + uint mat = atab[o]; + uint fl = atab[o + 1]; + if (mat == 0xFFFFFFFFu) continue; + if ((fl & 1u) != 0u && (fy < uintBitsToFloat(atab[o + 6]) || fy > uintBitsToFloat(atab[o + 7]))) continue; + float vv = v; + if ((fl & 2u) != 0u) vv += simplex70(fx * uintBitsToFloat(atab[o + 4]), fy * uintBitsToFloat(atab[o + 5])); + if (!(vv >= uintBitsToFloat(atab[o + 2]) && vv < uintBitsToFloat(atab[o + 3]))) continue; + if ((fl & 4u) == 0u) return mat; + float px = uintBitsToFloat(atab[o + 10]) + fx * uintBitsToFloat(atab[o + 8]); + float py = uintBitsToFloat(atab[o + 11]) + fy * uintBitsToFloat(atab[o + 9]); + float rmin = uintBitsToFloat(atab[o + 12]), rmax = uintBitsToFloat(atab[o + 13]); + if ((fl & 8u) != 0u) { + float n = (fl & 16u) != 0u ? fbm70(px, py) : simplex70(px, py); + if (rmin >= n || n > rmax) continue; + } + if ((fl & 32u) == 0u) return mat; + float n2 = polka(px, py, uintBitsToFloat(atab[o + 14]), uintBitsToFloat(atab[o + 15]), (fl & 64u) != 0u, uintBitsToFloat(atab[o + 16])); + if (rmin < n2 && n2 <= rmax) return mat; + } + return 0u; +} +uint eval_materials(int a, float v, ivec2 w) { return eval_materials_at(arec(a, 14), arec(a, 15), v, w); } + +// ---- procedural biomes (FUN_0087e110): gradient between the biome's y limits, inside perlin, noise-type blend; the BitmapCaves image is not generated (value 1 everywhere) + +float sin_big(float t) { // sin of a large float: reduce by 2*pi in two steps first (the GPU sin loses its accuracy for large arguments) + float k = floor(t * 0.15915494309189535 + 0.5); + float r = fma(-k, 6.28125, t); + r = fma(-k, 0.0019353071795864769, r); + return sin(r); +} +float iq_hash(float n) { return fract(sin_big(n) * 43758.547); } +float iq_mix(float n, float sx, float sy) { + float a = iq_hash(n), b = iq_hash(n + 1.0), c = iq_hash(n + 57.0), d = iq_hash(n + 58.0); + return mix(mix(a, b, sx), mix(c, d, sx), sy); +} +float iq_noise(float x, float y) { // FUN_00871630 + float ix = floor(x), iy = floor(y); + float fx = x - ix, fy = y - iy; + return iq_mix(iy * 57.0 + ix, (3.0 - 2.0 * fx) * fx * fx, (3.0 - 2.0 * fy) * fy * fy); +} +float iq_smooth_par(float f, int i) { return (i % 2 == 0) ? (3.0 - 2.0 * f) * f * f : f; } +float iq_noise2(float x, float y) { // FUN_00871850: the interpolation weight depends on the parity of the cell + float ix = floor(x), iy = floor(y); + float fx = x - ix, fy = y - iy; + int jx = int(ix), jy = int(iy); + float sx0 = iq_smooth_par(fx, jx), sx1 = iq_smooth_par(fx, jx + 1); + float sy0 = iq_smooth_par(fy, jy), sy1 = iq_smooth_par(fy, jy + 1); + return iq_mix(iy * 57.0 + ix, (sx1 - sx0) * fx + sx0, (sy1 - sy0) * fy + sy0); +} +float noise_t(int t, float x, float y) { // FUN_00873c00 (types 0-4 are not used by any biome of the game) + if (t == 5) return simplex70(x, y); + if (t == 6) return perlin2(x, y); + if (t == 7) return value2(x, y); + if (t == 8) return simplex2(x, y); + return 0.0; +} +float fbm_t(int t, float x, float y) { // FUN_00873cc0 + const vec4 M = vec4(0.84147, 0.5403, 0.5403, -0.84147); + const float K[3] = float[3](2.02, 2.33, 2.01); + float sum = noise_t(t, x, y) * 0.5, wgt = 0.25; + for (int o = 0; o < 3; ++o) { + float nx = (M.x * x + M.z * y) * K[o], ny = (M.y * x + M.w * y) * K[o]; + x = nx; y = ny; + sum += noise_t(t, x, y) * wgt; + wgt *= 0.5; + } + return sum / 0.9375; +} +float inside_perlin(uint rec, float X, float Y) { // FUN_0087e7a0 + uint fl = atab[rec]; + float x = X * 0.05243442 * tf(rec, 1) + 0.1 + tf(rec, 5); + float y = Y * 0.05243442 * tf(rec, 2) + 0.1 + tf(rec, 6); + if ((fl & 64u) != 0u) { float sd = float(atab[5]); x += sd * 1.312e-5; y += sd * 2.331e-5; } + int it = int((fl >> 12) & 15u); + float n = (fl & 2u) != 0u ? fbm_t(it, x, y) : noise_t(it, x, y); + float v = n; + if ((fl & 4u) != 0u) v = n * n; + float mn = tf(rec, 3), mx = tf(rec, 4); + if ((fl & 8u) != 0u) { if (!(mx > v)) v = mx; if (!(v > mn)) v = mn; } + if ((fl & 16u) != 0u) v = (v * 0.5 + 0.5) * (mx - mn) + mn; + return v; +} +float cave_at(uint base, int cw, int ch, int x, int y) { // the cave image: both axes wrap + x = ((x % cw) + cw) % cw; y = ((y % ch) + ch) % ch; + return field[base + uint(y * cw + x)]; +} +float cave_sample(uint base, int cw, int ch, float xf, float yf) { // FUN_00870e60 + int ix = int(floor(xf)), iy = int(floor(yf)); + float fx = xf - float(ix), fy = yf - float(iy); + float sx = (3.0 - fx * 2.0) * (fx * fx), sy = (3.0 - fy * 2.0) * (fy * fy); + float a = cave_at(base, cw, ch, ix, iy), b = cave_at(base, cw, ch, ix + 1, iy), c = cave_at(base, cw, ch, ix, iy + 1), d = cave_at(base, cw, ch, ix + 1, iy + 1); + float top = (b - a) * sx + a, bot = (d - c) * sx + c; + return (bot - top) * sy + top; +} +float proc_value(ivec2 w, uint rec) { + float X = float(w.x), Y = float(w.y); + float v = 1.0; + if (atab[rec + 18u] != 0xFFFFFFFFu) { // the cave image of the biome (see proc_biome.cpp) + uint cw = atab[rec + 19u] & 0xFFFFu, chh = atab[rec + 19u] >> 16; + float px = (X * 0.49162514 + 2560.0) * 0.1, py = (2560.0 * 6.86035959282328e-07 + Y * 0.49162514) * 0.1; + v = cave_sample(atab[rec + 18u], int(cw), int(chh), px, py); + if (1.0 > v) v = v + simplex70(px, py) * ((1.0 - v) * 0.495); + if (0.0 >= v) return 0.0; + } + v = proc_gradient(w, rec) * v; + float s8 = 0.0, s18 = 0.0, s1c = 0.0; + float mulp = tf(rec, 9); + uint fl = atab[rec]; + if (v > 0.51 && mulp != 0.0) { s18 = (v - 0.5) * 2.0; s8 = inside_perlin(rec, X, Y); } + if (v > 0.85) { + s1c = tf(rec, 7); + float nz; + if (((fl >> 8) & 15u) == 3u) { // SIN_CAPPED_SIMPLEX + float amp = iq_noise(X * 0.02, Y * 0.021) * 0.05 + 0.05; + float t1 = sin(Y * 0.0009765625) * 1024.0 * 0.5 * 0.06 * amp, t2 = cos(X * 0.0009765625) * 1024.0 * 0.5 * 0.06 * amp; + nz = float(simplex70(t2, t1)) * v; + } else { // the default noise type + float amp = iq_noise2(X * 0.025, Y * 0.025) * 0.05 + 0.05; + nz = simplex2(X * 0.01 * amp, Y * 0.01 * amp) * v; + } + v = v + (nz - v) * ((v - 0.85) / 0.1); + } + if ((fl & 32u) != 0u) s8 = ((v * s8) - s8) * (Y * 0.001) + s8; + return mulp * s18 * s8 + tf(rec, 8) * v + s1c; +} + +// Probe mode (shift_mode 2, docs: the teleport destination test): the world cells listed in probe[] get their cell word: from the live window when inside it +// (flag 4, flag 2 when the cell is falling: ext.x > 0), else generated as the window would generate them (flag 1 = an area is loaded there). +layout(std430, set = 0, binding = 53) buffer Probe { uvec4 phead; ivec4 probe[]; }; +bool g_unloaded = false; + +uint gen_cell(ivec2 w) { + uint hh = hash3(uint(w.x), uint(w.y), 77u); + uint var = (hh >> 8) & 0xFFu; + // LoadPixelScene images override the terrain (the scenes of every loaded area: a scene may reach into a neighbour) + bool forceSolid = false; + int whiteTile = -1; // the biome-map tile whose biome the white pixels of the scene use (the scene's origin) + for (int a = 0; a < int(atab[0]); ++a) { + int si = scene_index(a, w); + if (si < 0) continue; + uint sv = scene[si]; + if (sv >= 0xE000u) { forceSolid = true; whiteTile = sv == 0xFFFFu ? -1 : int(sv - 0xE000u); break; } + if (sv != 0u) { return (sv == 1u ? 0u : ((sv - 1u) | (var << 16))); } + } + const int ai = area_of_cell(w); + if (ai < 0) { g_unloaded = true; return uint(pc.m_rock) | (var << 16); } // no area loaded here + if (forceSolid) { // a white scene pixel: solid, the material from the of the biome with the wang value 1 + float xw = float(w.x), yw = float(w.y); + float n3 = value2(xw * 0.035, yw * 0.07) * 15.5; + float s = simplex2((xw + n3) * 0.0489275, (yw + n3) * 0.0489275); + const float wv = max(0.5, s * (0.0625 * 5.35 * 0.95) + 1.0); + uint fm; + const uint wrec = whiteTile >= 0 ? tile_rec(whiteTile % int(atab[2]), whiteTile / int(atab[2])) : 0xFFFFFFFFu; + if (wrec != 0xFFFFFFFFu && atab[wrec + 22u] != 0u) fm = eval_materials_at(int(atab[wrec + 21u]), int(atab[wrec + 22u]), wv, w); // the biome of the scene origin + else fm = eval_materials(ai, wv, w); + return fm == 0u ? 0u : (fm | (var << 16)); + } + { // a procedural biome (no wang template): the value function, then the list (FUN_0087d0e0, type 0) + const uint trec = topo_rec(w); + if (trec != 0xFFFFFFFFu && (atab[trec] & 1u) != 0u) { + const uint pm = eval_materials(ai, proc_value(w, trec), w); + return pm == 0u ? 0u : (pm | (var << 16)); + } + } + gpw = arec(ai, 2); gph = arec(ai, 3); gfo = arec(ai, 4); gmo = arec(ai, 5); + ivec2 o = w - ivec2(arec(ai, 0), arec(ai, 1)); // cell relative to the centre of wang pixel (0,0) + float px = (float(o.x) + 0.5) * 0.1, py = (float(o.y) + 0.5) * 0.1; + + if (py < -0.5) { return 0u; } // open sky above the area + // the last wang pixel is 5 cells short of the right / bottom edge of the area's tiles: those cells continue the edge pixel (a 13 cell wall of rock stood there) + px = clamp(px, -0.5, float(gpw) - 0.5001); py = clamp(py, -0.5, float(gph) - 0.5001); + + float warp = 1.0, thr = 0.5; + int m0 = material_pixel(px, py); + if (m0 > 0) { vec2 mp = mparams[m0]; warp = mp.x; thr = mp.y; } + + float qx = px, qy = py; + if (warp > 0.0) { + float xf = float(o.x), yf = float(o.y); + float n1 = simplex2(xf * 0.13715, yf * 0.13717); + float a = n1 * 0.45 + 0.1; + float xs = xf * 0.11111111, ys = yf * 0.11111111; + float n2 = perlin2(ys, xs); + float t = ((1.0 - a) * 0.33 + 0.111) * warp; + float n3 = value2(xs, ys); + qx = px + n3 * (a * warp); + qy = py + n2 * t; + } + float f = bilinear(qx, qy); + if (f < thr) { return 0u; } + + int m1 = material_pixel(qx, qy); + uint mat; + if (m1 > 0) { + mat = uint(m1); + } else { + // the biome's : FUN_0087d010 value, then the first matching component (none = air) + float xw = float(w.x), yw = float(w.y); + float n = value2(xw * 0.035, yw * 0.07); + float n3 = n * 15.5; + float h = (f - 0.5) * 0.5; + float s = simplex2((xw + n3) * 0.0489275, (yw + n3) * 0.0489275); + float v = max(0.5, s * (h * h * 5.35 * 0.95) + f); + mat = eval_materials(ai, v, w); + } + return mat == 0u ? 0u : (mat | (var << 16)); +} + +// The world rules of the cells that enter the window: the header in the tables before the 4 colour-grading words (main.cpp kWorldRules): [0] EVERYTHING_TO_GOLD, +// [1] gold, [2] gold_static; the cuts in binding 55: [0] count, then from word 4, 12 words per GameCutThroughWorldVertical (x, y_min, y_max, radius, edge, the engine's +// loaded region at the call: x0, y0, x1, y1, seed), every cut of the run. A generated cell (one that never was in the window) inside a cut's region takes the cut (007392d0: off = floor(3 sin(0.02 y) +// +- 0.5), air for left < x <= right, the static stainable cells within edge darkened by (1 - sqrt(d / edge)) x (0.4 + 0.3 rand), as the burn level of bits 28-30); +// with EVERYTHING_TO_GOLD every non-air cell is gold (the static ones gold_static), as the engine's redefined materials (00704f70) +const uint kWorldRules = 4u; +layout(std430, set = 0, binding = 55) readonly buffer CutRules { uint cuts[]; }; +uint cut_hash(uint a, uint b) { uint h = a * 0x9E3779B1u ^ (b + 0x7F4A7C15u); h ^= h >> 15; h *= 0x2C1B3C6Du; h ^= h >> 12; h *= 0x297A2D39u; h ^= h >> 15; return h; } +uint rules_word(uint c, ivec2 w, bool fresh) { + const uint wb = uint(perm.length()) - 4u - kWorldRules; + if (fresh) { + const uint n = min(cuts[0], (uint(cuts.length()) - 4u) / 12u); + for (uint k = 0u; k < n; ++k) { + const uint b = 4u + 12u * k; + const int x = int(cuts[b]), y0 = int(cuts[b + 1u]), y1 = int(cuts[b + 2u]), r = int(cuts[b + 3u]), edge = int(cuts[b + 4u]); + if (w.y < y0 || w.y >= y1 || w.x < x - r - edge || w.x >= x + r + edge) continue; + if (w.x < int(cuts[b + 5u]) || w.y < int(cuts[b + 6u]) || w.x >= int(cuts[b + 7u]) || w.y >= int(cuts[b + 8u])) continue; + const float v = sin(float(w.y) * 0.02) * 3.0; + const int off = int(floor(v + (v < 0.0 ? -0.5 : 0.5))); + const int left = x - r + off, right = x + r + off; + if (w.x > left && w.x <= right) { c = 0u; continue; } + const uint m = cell_mat(c); + if (edge <= 0 || m == 0u || (mflags(m) & F_STATIC) == 0u || (mflags(m) & F_NOSTAIN) != 0u || (c & BURN_BIT) != 0u) continue; + const float rnd = 0.4 + 0.3 * float(cut_hash(uint(w.y), cuts[b + 9u]) & 0xFFFFu) / 65536.0; + const float d = float(w.x <= left ? left - w.x : w.x - right); + const float s = max(0.0, (1.0 - sqrt(d / float(edge))) * rnd); + const float keep = (1.0 - float((c >> 28) & 7u) / 7.0) * (1.0 - s); + const uint L = uint(clamp(int(round((1.0 - keep) * 7.0)), 0, 7)); + c = (c & ~(7u << 28)) | (L << 28); + } + } + if (perm[wb] != 0u) { + const uint m = cell_mat(c); + if (m != 0u && m != perm[wb + 1u] && m != perm[wb + 2u]) c = ((mflags(m) & F_STATIC) != 0u ? perm[wb + 2u] : perm[wb + 1u]) | (c & (0xFFu << 16)); + } + return c; +} +// The spell test room (main.cpp --spelltest; cut rules words 1..3): every generated cell is rock, but the hall (x0 <= x < x1, y0 <= y < y1) is air inside 16 cells of +// temple brick +uint spell_room(uint c, ivec2 w) { + if (cuts[1] == 0u) return c; + const ivec2 a = ivec2(int(cuts[2] << 16) >> 16, int(cuts[2]) >> 16), b = ivec2(int(cuts[3] << 16) >> 16, int(cuts[3]) >> 16); + const uint var = (cut_hash(uint(w.x), uint(w.y)) >> 8) & 0xFFu; + if (w.x >= a.x && w.x < b.x && w.y >= a.y && w.y < b.y) return 0u; + if (w.x >= a.x - 16 && w.x < b.x + 16 && w.y >= a.y - 16 && w.y < b.y + 16) return (cuts[1] >> 16) | (var << 16); + return (cuts[1] & 0xFFFFu) | (var << 16); +} +void world_rules(int i, ivec2 w, bool fresh) { const uint c = rules_word(cells[i], w, fresh); if (c != cells[i]) { cells[i] = c; ext[i] = vec2(0.0); } } + +void main() { + if (pc.shift_mode == 3) { // far blocks (far.glsl): job z generates its block's cells into its slot - a store-backed block keeps the cells the store had + const int j = int(gl_GlobalInvocationID.z); + if (j >= farmap[kFarJobCount]) return; + const int slot = farmap[kFarJobList + 4 * j], bx = farmap[kFarJobList + 4 * j + 1], by = farmap[kFarJobList + 4 * j + 2], kept = farmap[kFarJobList + 4 * j + 3]; + const ivec2 w = ivec2(bx * 64, by * 64) + ivec2(gl_GlobalInvocationID.xy); + const int fi = slot * 4096 + (w.y & 63) * 64 + (w.x & 63); + if (kept != 0 && farcells[fi] != 0xFFFFFFFFu) return; + farcells[fi] = rules_word(spell_room(gen_cell(w), w), w, true); + return; + } + if (pc.shift_mode == 2) { + const uint q = gl_GlobalInvocationID.y * gl_NumWorkGroups.x * 8u + gl_GlobalInvocationID.x; + if (q >= phead.x) return; + const ivec2 w = probe[q].xy; + const ivec2 lp = w - ivec2(pc.view_x, pc.view_y); + if (lp.x >= 0 && lp.y >= 0 && lp.x < pc.gw && lp.y < pc.gh) { + const int li = lp.y * pc.gw + lp.x; + probe[q].z = int(cells[li]); + probe[q].w = 1 | 4 | (ext[li].x > 0.0 ? 2 : 0); + return; + } + g_unloaded = false; + probe[q].z = int(rules_word(spell_room(gen_cell(w), w), w, true)); + probe[q].w = g_unloaded ? 0 : 1; + return; + } + ivec2 p = ivec2(gl_GlobalInvocationID.xy); + if (p.x >= pc.gw || p.y >= pc.gh) return; + int i = p.y * pc.gw + p.x; + if (pc.shift_mode == 1) { // scrolling: reuse the simulated overlap, generate only the exposed strips + ivec2 src = p + ivec2(pc.shift_x, pc.shift_y); + if (src.x >= 0 && src.y >= 0 && src.x < pc.gw && src.y < pc.gh) { + cells[i] = cells_old[src.y * pc.gw + src.x]; + ext[i] = ext_old[src.y * pc.gw + src.x]; + birth[i] = birth_old[src.y * pc.gw + src.x]; // a cell that stays keeps its age (0070d1e0: the age belongs to the cell) + return; + } + } + birth[i] = birth_frame(); + if (ovr[i] != 0xFFFFFFFFu) { // a cell that comes back from the store (only its word is kept): a burning cell or a flame gets the fade a new cell of its material has + // (0070f450: the material's temperature_of_fire), so a fire that went out of the window goes on when it comes back instead of dying at its next update + const uint oc = ovr[i]; + cells[i] = oc; ext[i] = vec2(0.0); + const uint om = cell_mat(oc); + if (om != 0u && ((oc & BURN_BIT) != 0u || (mflags(om) & F_FIRE) != 0u)) birth[i] = with_fade(birth_frame(), int((mats[3u * om + 2u].w >> 8) & 255u)); + world_rules(i, p + ivec2(pc.view_x, pc.view_y), false); + return; + } + ivec2 w = p + ivec2(pc.view_x, pc.view_y); // world cell + ext[i] = vec2(0.0); + cells[i] = spell_room(gen_cell(w), w); + world_rules(i, w, true); +} diff --git a/shaders/vines.comp b/shaders/vines.comp new file mode 100644 index 0000000..e45a16f --- /dev/null +++ b/shaders/vines.comp @@ -0,0 +1,224 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +layout(local_size_x = 64) in; +#include "common.glsl" + +// VerletPhysicsComponent chains (the vines and ropes of the caves): one invocation per chain root. The root entity has ex.w = 777, ex.x = number of points, ex.y = the +// first part entity (the parts are consecutive entity slots, one per point; the point is the part's position, its previous position is kept in a.zw), ex.z = resting +// distance, mv = (velocity dampening, stiffness, gravity (0/1), wind (0/1)). The first point hangs on the surface above the root; the others are simulated with Verlet +// integration (the previous position gives the velocity), a distance constraint to the neighbour and the live cell grid as the collision. +struct Ent { vec4 a, box, mv, ai; ivec4 st; vec4 hp; ivec4 x; ivec4 rg; ivec4 rg2; ivec4 aux; vec4 ex; vec4 ex2; ivec4 ex3; vec4 ex4; vec4 ex5; vec4 ex6; vec4 ex7; ivec4 ex8; vec4 ex9; vec4 ex10; vec4 ex11; vec4 ex12, ex13, ex14, ex15, ex16, ex17, ex18, ex19; }; +layout(std430, set = 0, binding = 22) buffer Ents { Ent ents[]; }; +layout(std430, set = 0, binding = 23) readonly buffer Ctrl { ivec4 inp; ivec4 inp2; ivec4 inp3; vec4 outp; vec4 outp2; vec4 view; vec4 hand0; ivec4 hand1; vec4 hand2; } ctrl; + +bool solid_at(ivec2 w) { + ivec2 p = w - ivec2(pc.view_x, pc.view_y); + if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false; + uint m = cell_mat(cells[p.y * pc.gw + p.x]); + return m != 0u && (mflags(m) & (F_STATIC | F_SAND)) != 0u; +} + +bool cell_present(ivec2 w) { + ivec2 p = w - ivec2(pc.view_x, pc.view_y); + return p.x >= 0 && p.y >= 0 && p.x < pc.gw && p.y < pc.gh && cell_mat(cells[p.y * pc.gw + p.x]) != 0u; +} +bool blocking_at(ivec2 w) { // a non-null cell whose type is solid (virtual +2c = 1; CLiquidCell 0070b0b0: a static powder - the rocks are liquid_static liquid_sand cells; CSolidCell) + ivec2 p = w - ivec2(pc.view_x, pc.view_y); + if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false; + uint m = cell_mat(cells[p.y * pc.gw + p.x]); + return m != 0u && (mflags(m) & F_STATIC) != 0u && ((mflags(m) & F_POWDER) != 0u || (mflags(m) & F_LIQCELL) == 0u); +} +bool stain_here(ivec2 w) { + ivec2 p = w - ivec2(pc.view_x, pc.view_y); + if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false; + uint m = cell_mat(cells[p.y * pc.gw + p.x]); + return m != 0u && (mflags(m) & F_STAINS) != 0u; +} +bool liquid_here(ivec2 w) { + ivec2 p = w - ivec2(pc.view_x, pc.view_y); + if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false; + uint m = cell_mat(cells[p.y * pc.gw + p.x]); + return m != 0u && (mflags(m) & F_LIQCELL) != 0u && (mflags(m) & F_POWDER) == 0u; +} + +bool in_window(vec2 w, float margin) { + vec2 p = w - vec2(pc.view_x, pc.view_y); + return p.x > margin && p.y > margin && p.x < float(pc.gw) - margin && p.y < float(pc.gh) - margin; +} + +void main() { + uint id = gl_GlobalInvocationID.x; + if (id >= uint(ctrl.inp.w)) return; + Ent root = ents[id]; + if (root.ex.w != 777.0) return; + const int n = int(root.ex.x), first = int(root.ex.y); + if ((root.st.w & 8) != 0) { // the chain was removed (its area is gone): so are its points + for (int k = 0; k < n; ++k) ents[uint(first + k)].st.w |= 8; + return; + } + const float rest = root.ex.z, damp = root.mv.x, stiff = root.mv.y; + if (!in_window(root.a.xy, 24.0)) return; + // a new chain waits 15 frames in the window before its surface search and its joints' first look at the cells: after a teleport the pixel scenes of the new area + // reach the cells a few frames after the terrain (the holy mountain's brick over the generated rock), and the ropes searched the bare rock, or remembered it and broke + if (root.hp.y == 0.0 && root.hp.z < 15.0) { ents[id].hp.z = root.hp.z + 1.0; return; } + vec2 anchor = root.a.xy; + const int jtype = int(root.ex4.x); // MoveToSurfaceOnCreateComponent: 0 the surface above, 1 VERLET_ROPE_ONE_JOINT, 2 VERLET_ROPE_TWO_JOINTS + if (root.hp.y == 0.0) { + if (jtype == 0 || jtype == 3) { // MoveToSurfaceOnCreate (3: the hair of a limb boss, its first point on the root entity the host moves): the first point goes to the surface above the spawn point (the cell below the first solid one) + ivec2 c = ivec2(floor(anchor)); + if (jtype == 0 && !solid_at(c)) { + for (int k = 1; k <= 24; ++k) if (solid_at(c - ivec2(0, k))) { anchor = vec2(anchor.x, float(c.y - k + 1)); break; } + } + ents[id].a.xy = anchor; + for (int k = 0; k < n; ++k) { // hang straight down from the anchor + vec2 p = anchor + vec2(0.0, float(k) * rest); + ents[uint(first + k)].a = vec4(p, p); + } + } else if (jtype == 3) { // VerletWorldJointComponent (load_verlet_rope_with_*_joint*): the joints are given (ex2, ex7.z), nothing is searched + } else { + // 00bc3490 / 00bc2fa0 (called with first-hit and the upward fan): each try casts ray_count rays of length lookup_radius from the spawn point at random angles within + // +-1.2566371 (0x3fa0d97c) of straight up, the first ray that hits gives the joint: the hit cell itself (offset_from_surface only moves the type-0 entity). + // TWO_JOINTS: both joints are searched again until the second is at least verlet_min_joint_distance from the first; after 11 tries the joints are dropped (the rope is free). + const float R = root.ex4.y, mind = root.ex4.w; + const int rays = max(int(root.ex6.x), 1); + vec2 A = vec2(0.0), B = vec2(0.0); + bool gotA = false, gotB = false; + for (int attempt = 0; attempt < 11 && !(jtype == 1 ? gotA : gotB); ++attempt) { + gotA = false; gotB = false; + for (int which = 0; which < 2; ++which) { + if (which == 1 && (jtype != 2 || !gotA)) continue; + vec2 pt = vec2(0.0); + bool hit = false; + for (int r = 0; r < rays && !hit; ++r) { + const float ang = float(hash3(id, uint(attempt * 8 + which * 4 + r), 4242u) & 0x7FFFu) / 32767.0 * 2.5132742 - 1.2566371; + const vec2 dir = vec2(sin(ang), -cos(ang)); + for (float t = 1.0; t <= R; t += 1.0) { + const ivec2 c = ivec2(floor(root.a.xy + dir * t)); + if (solid_at(c)) { pt = vec2(c); hit = true; break; } + } + } + if (!hit) break; + if (which == 0) { A = pt; gotA = true; } + else if (distance(pt, A) >= mind) { B = pt; gotB = true; } + } + } + if (jtype == 2 && !gotB) gotA = false; // (no pair: both joints go) + if (!gotA) { // 00bc3490: after 11 tries without its joint(s) the components go and the entity is killed (0044df60): no rope falls from the ceiling + ents[id].st.w |= 8; + for (int k = 0; k < n; ++k) ents[uint(first + k)].st.w |= 8; + return; + } + float flags0 = 0.0; + if (gotA) flags0 += 1.0; + if (gotB) flags0 += 2.0; + ents[id].ex2 = vec4(A, B); + ents[id].ex7.z = flags0; + } + if (jtype == 1 || jtype == 2) { + const int fl = int(ents[id].ex7.z); + const bool gotA = (fl & 1) != 0, gotB = (fl & 2) != 0; + const vec2 A = ents[id].ex2.xy, B = ents[id].ex2.zw; + const vec2 P0 = gotA ? A : (gotB ? B - vec2(0.0, float(n - 1) * rest) : root.a.xy); + ents[id].a.xy = P0; + const float L = float(n - 1) * rest; + for (int k = 0; k < n; ++k) { // 00c39fd0: the points start on a horizontal line from the entity position, resting_distance apart; the joints then move their points (first, last) + vec2 p = root.a.xy + vec2(float(k) * rest, 0.0); + if (k == 0 && gotA) p = A; + if (gotB) { // two joints: started where the rope settles (along the chord with the sag of its length) so that no point begins inside the rock beside the spawn point + const float d = max(distance(A, B), 1.0); + const float sag = L > d ? sqrt(3.0 * d * (L - d) / 8.0) : 0.0; + const float u = float(k) / float(max(n - 1, 1)); + p = mix(A, B, u) + vec2(0.0, 4.0 * sag * u * (1.0 - u)); + } + ents[uint(first + k)].a = vec4(p, p); + } + } + ents[id].hp.y = 1.0; + } + // the pins: ex7.z = which joints exist (1 first point, 2 last point; -1 none left); jtype 0 and a type 3 without data (a limb boss's hair) pin the first point + int pinFlags = ents[id].ex7.z < 0.0 ? 0 : (jtype == 0 || jtype == 3) && ents[id].ex7.z == 0.0 ? 1 : int(ents[id].ex7.z); + if (jtype == 1 || jtype == 2 || (jtype == 3 && ents[id].ex7.z > 0.0 && (pinFlags & 1) != 0)) anchor = ents[id].ex2.xy; + { // VerletWorldJointSystem (00c40b90): a joint remembers the cell at its world position on its first update (none there: it never breaks); when another cell or none is + // there later, it breaks: the component goes and the rope's damping becomes 0.97 (and a "joint_break" sound, not played here). ex6.z / ex6.w: the remembered material + // + 1 of the first / last point's joint, -1 for air, 0 not yet seen + bool broke = false; + for (int j = 0; j < 2; ++j) { + if ((pinFlags & (1 << j)) == 0) continue; + const vec2 jw = j == 0 ? anchor : ents[id].ex2.zw; + const ivec2 q = ivec2(floor(jw)) - ivec2(pc.view_x, pc.view_y); + if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) continue; + const uint m = cell_mat(cells[q.y * pc.gw + q.x]); + const float rec = j == 0 ? ents[id].ex6.z : ents[id].ex6.w; + if (rec == 0.0) { if (j == 0) ents[id].ex6.z = m == 0u ? -1.0 : float(m + 1u); else ents[id].ex6.w = m == 0u ? -1.0 : float(m + 1u); } + else if (rec > 0.0 && float(m + 1u) != rec) { pinFlags &= ~(1 << j); broke = true; } + } + if (broke) { ents[id].ex7.z = pinFlags == 0 ? -1.0 : float(pinFlags); ents[id].mv.x = 0.97; } + } + const bool pinA = (pinFlags & 1) != 0, pinB = (pinFlags & 2) != 0; + vec2 pts[40], prv[40]; + for (int k = 0; k < n && k < 40; ++k) { vec4 a = ents[uint(first + k)].a; pts[k] = a.xy; prv[k] = a.zw; } + const float t = float(ctrl.inp2.x); + float massOf[40]; // the random mass of every point between mass_min and mass_max (00c39fd0); it weights the relaxation, the forces are proportional to it + for (int k = 0; k < n && k < 40; ++k) massOf[k] = root.ex5.y + (root.ex5.z - root.ex5.y) * float(hash3(id, uint(k), 77u) & 0xFFFFu) / 65536.0; + const float T = root.ex5.x * t / 60.0; // wind_change_speed x time in seconds + // wind (docs/EXPLOSION_KNOCKBACK_AND_VINES.md section 3): WorldStateComponent.wind (wind_speed x dt) times the sky exposure of the first point (00746620: the 2048 x 2048 map of + // 64-pixel cells, 1 in the open and 0 a few cells under the biomes' ground; the earlier "x num_points" was the return of that lookup misread), set by the host in ex6.y + const float W = root.ex6.y; + const float Aw = sin(25.0 * T), Bw = sin(0.4 * T), Pw = 0.5 * (sin(T) + 1.0) + 0.5, Gw = 5.0 * abs(W) + 1.0; + for (int k = 0; k < n && k < 40; ++k) { + // 00c37d30: new = cur + (cur - prev) * damping + 0.5 F / m dt^2 (dt = 1/60); a point whose stored liquid material is set damps by 0.85 + const bool wet = ents[uint(first + k)].hp.x > 0.5; // the point carries a stored liquid material + vec2 v = (pts[k] - prv[k]) * (wet ? 0.85 : damp); + vec2 F = vec2(0.0); // force per unit mass + if (root.mv.z > 0.5) F.y += 600.0; // 00c379e0: mass x 600 + if (root.mv.w > 0.5) { // 00c37b10 + F.x += 360.0 * W * Pw + 100.0 * sin(5.0 * pts[k].x) * Aw; + F.y += 20.0 * sin(5.0 * pts[k].y) * Aw + 60.0 * sin(0.005 * pts[k].y) * Bw * Gw; + } + const vec2 delta = v + F * 0.5 / 3600.0; + vec2 cand = pts[k] + delta; + // the terrain (00c37d30): probe (truncated x, ceil y); a free cell takes the candidate (a liquid one only liquid_damping of the move); a blocking one halves the move once; if the midpoint still + // blocks, the cell above it decides: free = one pixel up, otherwise the point stays and its velocity is gone + vec2 np = cand; + if (blocking_at(ivec2(int(cand.x), int(ceil(cand.y))))) { + const vec2 mid = (pts[k] + cand) * 0.5; + if (!blocking_at(ivec2(int(mid.x), int(ceil(mid.y))))) np = mid; + else if (!blocking_at(ivec2(int(mid.x), int(ceil(mid.y)) - 1)) && cell_present(ivec2(int(mid.x), int(ceil(mid.y)) - 1))) { np = pts[k] + vec2(0.0, -1.0); prv[k] = np; pts[k] = np; continue; } + else { prv[k] = pts[k]; continue; } + } else if (liquid_here(ivec2(int(cand.x), int(ceil(cand.y))))) { + np = pts[k] + root.ex5.w * delta; + // 00c37d30: the liquid's material is sampled into the point on 6 of 10 points, else with a 1 in 30 draw (only liquids that stain) + if (stain_here(ivec2(int(cand.x), int(ceil(cand.y)))) && (k % 10 < 6 || hash3(id, uint(k), uint(pc.frame)) % 30u == 0u)) ents[uint(first + k)].hp.x = 1.0; + } + prv[k] = pts[k]; + pts[k] = np; + } + for (int it = 0; it < 2; ++it) // 00c38310 runs twice over the links: the chain is soft, it stretches under its own weight + for (int k = 0; k + 1 < n && k + 1 < 40; ++k) { // distance constraints; the joints' points move like the others here and are put back afterwards (00c3a610) + vec2 d = pts[k + 1] - pts[k]; + float l = length(d); + if (l < 1e-5) continue; + float diff = (l - rest) / max(l, 0.01) * stiff * 0.5; // 00c3a610 passes stiffness x 0.5 (0x3f000000) to 00c38310; links shorter than 0.01 count as 0.01 + const float wa = 1.0 / massOf[k], wb = 1.0 / massOf[k + 1], sa = wa / (wa + wb); // the share of the correction by the inverse masses + pts[k] += d * diff * sa; pts[k + 1] -= d * diff * (1.0 - sa); + } + if (pinA) pts[0] = anchor; // the world joints put their points back after the relaxation (00c3a610) + if (pinB) pts[n - 1] = ents[id].ex2.zw; + // the animation (00c385a0, after the relaxation when animation_target_offset is not 0 - the tentacler's tentacles, offset (0, 1)): every link pulls its far point toward + // near point + offset + the offset's direction turned by 90 degrees x sin((link + time) x animation_speed) x animation_amount, by animation_energy shared by the inverse masses + // (root ex17 = offset, amount, speed; ex18.x = energy; the time in seconds) + if (root.ex17.x != 0.0 || root.ex17.y != 0.0) { + const vec2 D = root.ex17.xy, u = normalize(D), perp = vec2(-u.y, u.x); + for (int k = 0; k + 1 < n && k + 1 < 40; ++k) { + const float w = sin((float(k) + t / 60.0) * root.ex17.w) * root.ex17.z; + const vec2 delta = pts[k] + D + perp * w - pts[k + 1]; + const float wa = 1.0 / massOf[k], wb = 1.0 / massOf[k + 1]; + pts[k + 1] += delta * (root.ex18.x * wb / (wa + wb)); + } + } + for (int k = 0; k < n && k < 40; ++k) { + ents[uint(first + k)].a = vec4(pts[k], prv[k]); + const vec2 d = k + 1 < n ? pts[k + 1] - pts[k] : pts[k] - pts[max(k - 1, 0)]; + ents[uint(first + k)].ex7.x = atan(d.y, d.x); // the horizontal part sprite is turned along the chain + } +} diff --git a/src/ai/pathfind.cpp b/src/ai/pathfind.cpp new file mode 100644 index 0000000..1e49829 --- /dev/null +++ b/src/ai/pathfind.cpp @@ -0,0 +1,362 @@ +#include "ai/pathfind.h" + +#include +#include +#include +#include + +#include "entity/herds.h" + +namespace ng::ai { + +void PathGrid::build(int worldX, int worldY, int pixelsW, int pixelsH, const std::function& classify) { + const int nox = (int)std::ceil(worldX / 4.0) * 4, noy = (int)std::ceil(worldY / 4.0) * 4; + const int nw = std::max(0, (worldX + pixelsW - nox + 3) / 4), nh = std::max(0, (worldY + pixelsH - noy + 3) / 4); + // the stuck marks of the nodes that stay + std::vector oldBlocked = std::move(blocked); + std::vector oldUntil = std::move(blockedUntil); + const int pox = ox, poy = oy, pw = w, ph = h; + ox = nox; oy = noy; w = nw; h = nh; + flags.assign((size_t)w * h, 0); + liquidCells.assign((size_t)w * h, 0); + markHerd.assign((size_t)w * h, -1); + markEnt.assign((size_t)w * h, 0); + playerArea.assign((size_t)w * h, 0); + clearance.assign((size_t)w * h, 0); + blocked.assign((size_t)w * h, 0); + blockedUntil.assign((size_t)w * h, 0); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + const int wx = ox + x * 4, wy = oy + y * 4; + int nSolid = 0, nLiquid = 0, nDanger[4] = {0, 0, 0, 0}; + for (int cy = 0; cy < 4; ++cy) + for (int cx = 0; cx < 4; ++cx) { + const uint8_t c = classify(wx + cx, wy + cy); + if (c & kNodeSolid) ++nSolid; else if (c & kNodeLiquid) ++nLiquid; + for (int b = 0; b < 4; ++b) if (c & (1 << b)) ++nDanger[b]; + } + uint8_t f = (uint8_t)((nSolid > 0 ? kNodeSolid : 0) | (nLiquid > 10 && nSolid == 0 ? kNodeLiquid : 0) | (nSolid > 14 ? kNodeFullSolid : 0)); + for (int b = 0; b < 4; ++b) if (nDanger[b] > 2) f |= (uint8_t)(1 << b); + flags[idx(x, y)] = f; + liquidCells[idx(x, y)] = (uint8_t)nLiquid; + const int px = (wx - pox) / 4, py = (wy - poy) / 4; + if (!oldBlocked.empty() && px >= 0 && py >= 0 && px < pw && py < ph && (wx - pox) % 4 == 0 && (wy - poy) % 4 == 0) { + blocked[idx(x, y)] = oldBlocked[(size_t)py * pw + px]; + blockedUntil[idx(x, y)] = oldUntil[(size_t)py * pw + px]; + } + } + // clearance (00860150): the Chebyshev distance in nodes to the nearest solid node, 0 for a solid one, at most 4 + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) { + if (flags[idx(x, y)] & kNodeSolid) { clearance[idx(x, y)] = 0; continue; } + int d = 4; + for (int r = 1; r < 4 && d == 4; ++r) { + for (int k = -r; k <= r && d == 4; ++k) + if (solid(x + k, y - r) || solid(x + k, y + r) || solid(x - r, y + k) || solid(x + r, y + k)) d = r; + } + clearance[idx(x, y)] = (uint8_t)d; + } +} + +void PathGrid::clear_marks() { + std::fill(markHerd.begin(), markHerd.end(), (int16_t)-1); + std::fill(markEnt.begin(), markEnt.end(), 0); + std::fill(playerArea.begin(), playerArea.end(), (uint8_t)0); +} + +void PathGrid::mark(float wx, float wy, int herd, int entity) { + const int nx = (int)std::floor((wx - ox) / 4.0f), ny = (int)std::floor((wy - oy) / 4.0f); + if (!valid(nx, ny)) return; + markHerd[(size_t)idx(nx, ny)] = (int16_t)(herd >= 0 && herd < 0x80 ? herd : -1); + markEnt[(size_t)idx(nx, ny)] = entity; +} + +void PathGrid::mark_player_area(float wx, float wy, float radius, float phase) { // 12 rays (every 30 degrees from the phase: (frame % 60) x a step) to the radius, stopped by a solid node + if (radius <= 0.0f) return; + for (int k = 0; k < 12; ++k) { + const float a = phase + (float)k * 0.5235988f; + const float ex = wx + std::cos(a) * radius, ey = wy + std::sin(a) * radius; + const int n = std::max(1, (int)(radius / 4.0f)); + for (int i = 0; i <= n; ++i) { + const int nx = (int)std::floor((wx + (ex - wx) * i / n - ox) / 4.0f), ny = (int)std::floor((wy + (ey - wy) * i / n - oy) / 4.0f); + if (!valid(nx, ny) || solid(nx, ny)) break; + playerArea[(size_t)idx(nx, ny)] = 1; + } + } +} + +void PathGrid::block(int nx, int ny, uint8_t dirs, int untilFrame) { + if (!valid(nx, ny)) return; + blocked[idx(nx, ny)] |= dirs; + blockedUntil[idx(nx, ny)] = std::max(blockedUntil[idx(nx, ny)], untilFrame); +} + +void PathGrid::forget_blocks(int frame) { + for (size_t i = 0; i < blocked.size(); ++i) if (blocked[i] && blockedUntil[i] <= frame) blocked[i] = 0; +} + +bool lob_velocity(float x0, float y0, float x1, float y1, float speed, float gravity, float& vx, float& vy) { + const float dx = x1 - x0, dy = y1 - y0; + const float dist = std::hypot(dx, dy); + if (speed <= 0.0f) return false; + const float t = std::max(dist / speed, 0.15f); // the flight time at the jump speed + vx = dx / t; + vy = (dy - 0.5f * gravity * t * t) / t; + return std::hypot(vx, vy) <= speed * 2.5f; +} + +namespace { + +struct Rec { int node; float g; int depth; int parent; bool jump; float jvx, jvy; }; + +// a node a walker can stand on (00860790): free, a solid node below, two free nodes above +bool standable(const PathGrid& g, int x, int y) { + return !g.solid(x, y) && g.solid(x, y + 1) && !g.solid(x, y - 1) && !g.solid(x, y - 2); +} +// 00860790 with its strict argument (00860930: the landings of jumps): each side is a wall, or standable at the same level or one node down - a one-node perch +// above a drop is no landing (the helper's neighbour order is partly unclear in the decompile: this footprint is the reading of docs/WEAK_ZOMBIE_AI_DIFFERENCES.md AI-08) +bool standable_strict(const PathGrid& g, int x, int y) { + if (!standable(g, x, y)) return false; + auto side = [&](int d) { return g.solid(x + d, y) || standable(g, x + d, y) || standable(g, x + d, y + 1); }; + return side(-1) && side(1); +} + +} // namespace + +namespace { +// one pass of 00862620 with the movement modes of p; budgetOut: it stopped at iterations_max with nodes still open (the engine would resume it later) +std::vector search_pass(const PathGrid& g, float sx, float sy, const SearchParams& p, bool& budgetOut) { + std::vector out; + budgetOut = false; + if (g.w <= 0 || g.h <= 0) return out; + int snx = (int)std::floor((sx - g.ox) / 4.0f), sny = (int)std::floor((sy - g.oy) / 4.0f); + if (!g.valid(snx, sny)) return out; + // 00861e00: the start projected like the engine. An embedded start looks left, right, then up, up to 12 pixels (3 nodes), for a free node; an unsupported + // walker's start is dropped down a ray up to 12 pixels to the node above the ground. (Only one node up before: a creature wedged under a table top beside its leg + // found no route at all, docs/WEAK_ZOMBIE_AI_DIFFERENCES.md AI-06) + if (g.solid(snx, sny)) { + bool moved = false; + for (int d = 1; d <= 3 && !moved; ++d) { + if (g.valid(snx - d, sny) && !g.solid(snx - d, sny)) { snx -= d; moved = true; } + else if (g.valid(snx + d, sny) && !g.solid(snx + d, sny)) { snx += d; moved = true; } + else if (g.valid(snx, sny - d) && !g.solid(snx, sny - d)) { sny -= d; moved = true; } + } + if (!moved) return out; + } else if (!p.can_fly && !g.solid(snx, sny + 1) && !(g.at(snx, sny + 1) & kNodeLiquid) && !(g.at(snx, sny) & kNodeLiquid)) { + for (int d = 1; d <= 3; ++d) { + if (!g.valid(snx, sny + d) || g.solid(snx, sny + d) || (g.at(snx, sny + d) & kNodeLiquid)) break; + sny += 1; + if (g.solid(snx, sny + 1)) break; + } + } + const int N = g.w * g.h; + std::vector best((size_t)N, 1e30f); + std::vector recs; + recs.reserve(4096); + std::vector recOf((size_t)N, -1); + using QE = std::pair; // f, rec + std::priority_queue, std::greater> open; + const bool directed = p.goal == Goal::Hunt || p.goal == Goal::Goto || p.goal == Goal::MoveNear; + const int gnx = (int)std::floor((p.gx - g.ox) / 4.0f), gny = (int)std::floor((p.gy - g.oy) / 4.0f); + auto heuristic = [&](int nx, int ny) -> float { // 00862620: (|dx| + |dy|) in pixels x 10 + if (!directed) return 0.0f; + return (float)(std::abs(nx - gnx) * 4 + std::abs(ny - gny) * 4) * 10.0f; + }; + const bool hasMarks = !g.markHerd.empty(); + auto avoided = [&](int nx, int ny) -> uint8_t { // the stuck directions of this creature at a node (00860c20), plus the shared ones of the grid + uint8_t d = g.blocked.empty() ? 0 : g.blocked[g.idx(nx, ny)]; + if (p.avoid && !p.avoid->empty()) { + const int64_t k = world_node_key((g.ox >> 2) + nx, (g.oy >> 2) + ny); + for (const auto& [ak, ad] : *p.avoid) if (ak == k) d |= ad; + } + return d; + }; + // RandomMove, Escaping and EscapingPrey step only into nodes no creature marks (their edge checks 00864a80 / 00864ea0 / 00865090) + const bool unmarkedOnly = hasMarks && (p.goal == Goal::RandomMove || p.goal == Goal::Away || p.goal == Goal::Escape); + auto isGoal = [&](int nx, int ny) -> bool { + // the node's centre; Goto (008649c0) and MoveNear (00864d40) test its corner + const bool corner = p.goal == Goal::Goto || p.goal == Goal::MoveNear; + const float wx = (float)(g.ox + nx * 4 + (corner ? 0 : 2)), wy = (float)(g.oy + ny * 4 + (corner ? 0 : 2)); + if (p.home_radius > 0.0f && p.goal != Goal::Goto && p.goal != Goal::MoveNear && p.goal != Goal::Charmed) { + const float d2 = (wx - p.home_x) * (wx - p.home_x) + (wy - p.home_y) * (wy - p.home_y); + if (d2 > p.home_radius * p.home_radius) return false; + if (p.goal == Goal::Escape && d2 > p.home_radius * p.home_radius * 0.2f) return false; + } + const int id = g.idx(nx, ny); + switch (p.goal) { + case Goal::Hunt: { // 00864bf0: a node marked by a hostile herd, or by the target entity + if (!hasMarks) return std::abs(nx - gnx) <= 1 && std::abs(ny - gny) <= 1; + const int hm = g.markHerd[(size_t)id]; + if (hm >= 0 && hm != p.own_herd && ng::herd_relation(p.own_herd, hm) < p.aggression) return true; + return p.target_entity != 0 && g.markEnt[(size_t)id] == p.target_entity; + } + case Goal::Escape: return (wx - p.gx) * (wx - p.gx) + (wy - p.gy) * (wy - p.gy) >= p.away_distance * p.away_distance && !g.marked(id); // 00864ed0 + case Goal::Charmed: return !g.playerArea.empty() && g.playerArea[(size_t)id] && (wx - sx) * (wx - sx) + (wy - sy) * (wy - sy) > 256.0f; // 00865480 + case Goal::Goto: return (wx - p.gx) * (wx - p.gx) + (wy - p.gy) * (wy - p.gy) < 8.0f; // 008649c0 (DAT_01053cc0 = 8: the squared distance, a radius of sqrt 8) + case Goal::MoveNear: return std::abs(p.gx - wx) < 8.0f && std::abs(p.gy - wy) < 256.0f; // 00864d40 + case Goal::Away: return (wx - p.gx) * (wx - p.gx) + (wy - p.gy) * (wy - p.gy) >= p.away_distance * p.away_distance && !g.marked(id); // 008650c0 + case Goal::Burning: { // 00865280: a liquid node of 16 liquid cells without danger, with the same above it and on both sides (to put the fire out) + auto wet = [&](int x, int y) { return g.valid(x, y) && (g.at(x, y) & kNodeLiquid) && !(g.at(x, y) & p.danger_mask & 0x0F) && g.liquidCells[g.idx(x, y)] == 16; }; + return wet(nx, ny) && wet(nx, ny - 1) && wet(nx - 1, ny) && wet(nx + 1, ny); + } + case Goal::RandomMove: return std::abs((int)wx - (int)sx) + std::abs((int)wy - (int)sy) > p.random_distance && !(g.at(nx, ny) & kNodeLiquid) && !g.marked(id); // 00864ab0 + default: return false; + } + }; + auto push = [&](int nx, int ny, int parent, float g2, int depth, bool jump, float jvx, float jvy) { + const int id = g.idx(nx, ny); + if (g2 >= best[id]) return; + if (unmarkedOnly && parent >= 0 && g.marked(id)) return; + best[id] = g2; + recs.push_back({id, g2, depth, parent, jump, jvx, jvy}); + recOf[id] = (int)recs.size() - 1; + open.push({g2 + heuristic(nx, ny), (int)recs.size() - 1}); + }; + push(snx, sny, -1, 0.0f, 0, false, 0, 0); + bool fanned = false; // (00862620 local_22: the fan of initial jumps only once a search) + int iterations = 0, found = -1; + // the node closest to the goal, for a directed search that runs out of iterations (the engine keeps the best partial path of a target) + int closest = -1; float closestH = 1e30f; + while (!open.empty() && iterations < p.iterations_max) { + const auto [f, ri] = open.top(); open.pop(); + const Rec r = recs[ri]; + if (r.g > best[r.node]) continue; + ++iterations; + if (p.work) ++*p.work; + const int cx = r.node % g.w, cy = r.node / g.w; + if (ri != 0 && isGoal(cx, cy)) { found = ri; break; } + if (directed) { const float h = heuristic(cx, cy); if (h < closestH) { closestH = h; closest = ri; } } + if (r.depth >= p.depth_max) continue; + const uint8_t cflags = g.at(cx, cy); + const bool cLiquid = (cflags & kNodeLiquid) != 0; + // jumps (00862620 -> 00860930): from a node on the ground every 5th depth, or at a ledge. The first such node of the search tries a fan of aim points every + // 4 pixels within +-initial_jump_max_distance_x / _y (only (0, 0) left out); every later one only the component's jump_trajectories, each mirrored too + // (00862620 generates them before the neighbours; on every 5th depth from a node with solid cells below it, or at a ledge: the parent a lateral neighbour and + // no ground beyond the node on the far side - not the full standable test, docs/WEAK_ZOMBIE_AI_DIFFERENCES.md AI-14) + const int parentNode = r.parent >= 0 ? recs[r.parent].node : -1; + const bool lateralParent = parentNode >= 0 && parentNode / g.w == cy && std::abs(parentNode % g.w - cx) == 1; + const bool ledge = lateralParent && !g.solid(cx + (cx - parentNode % g.w), cy + 1); + if (p.can_jump && !p.can_fly && g.solid(cx, cy + 1) && !g.solid(cx, cy) && (r.depth % 5 == 0 || ledge)) { + // 00860930: the lob from the node's corner aimed at (dx, dy) (0063ec90: T = lob x 60 x distance / jump_speed frames, v = (d - g T^2 / 2) / T per frame, + // here per second); 0063ee20 samples it at n = (|dx| + |vy t| + |g t^2 / 2|) / 4 points evenly in x up to the aim point (t = dx / vx); a solid or dangerous + // node on the way ends it with nothing; the first standable node it enters going down (a node below the one of the sample before) is the landing. + // A vertical aim (dx = 0) has no samples: no jump. Cost: the nodes sampled x 10000 + auto arc = [&](float dx, float dy, float lob) { + const float dist = std::hypot(dx, dy); + if (dist < 1.1920929e-7f || p.jump_speed <= 0.0f) return; + const float T = lob * 60.0f * (dist / p.jump_speed); // frames + if (!(T > 0.0f)) return; + const float vx = (dx * 60.0f) / T, vy = ((dy - p.gravity / 3600.0f * T * T * 0.5f) * 60.0f) / T; // px/s + const float t = vx != 0.0f ? dx / vx : 0.0f; + const int n = (int)((std::fabs(dx) + std::fabs(vy * t) + std::fabs(p.gravity * 0.5f * t * t)) * 0.25f); + if ((unsigned)(n - 1) >= 0x1ffu) return; + const float x0 = (float)(g.ox + cx * 4), y0 = (float)(g.oy + cy * 4); + const float sdx = dx / (float)n, sdt = vx != 0.0f ? sdx / vx : 0.0f; + int crossed = 0, prevY = cy; + for (int s = 1; s <= n; ++s) { + const float ts = sdt * (float)s; + const int ax = (int)std::floor(((float)(int)(x0 + sdx * (float)s) - (float)g.ox) / 4.0f); + const int ay = (int)std::floor(((float)(int)(y0 + vy * ts + p.gravity * 0.5f * ts * ts) - (float)g.oy) / 4.0f); + if (!g.valid(ax, ay)) continue; // (no node there: not counted) + ++crossed; + const uint8_t af = g.at(ax, ay); + if ((af & kNodeSolid) || (af & p.danger_mask & 0x0F)) return; + if (standable_strict(g, ax, ay) && ay > prevY) { + // 00862620 hands the landing to the ordinary relaxer 00863af0: its clearance and danger tests too (docs/WEAK_ZOMBIE_AI_DIFFERENCES.md AI-09) + if (g.clearance[g.idx(ax, ay)] < p.clearance_required) return; + push(ax, ay, ri, r.g + 10.0f + (float)crossed * 10000.0f, r.depth + 1, true, vx, vy); + return; + } + prevY = ay; + } + }; + if (!fanned) { + fanned = true; + for (float dx = -p.jump_max_x; dx <= p.jump_max_x; dx += 4.0f) + for (float dy = -p.jump_max_y; dy <= p.jump_max_y; dy += 4.0f) + if (dx != 0.0f || dy != 0.0f) arc(dx, dy, p.jump_lob); + } else + for (const auto& jt : p.jump_trajectories) { arc(jt[0], jt[1], jt[2]); arc(-jt[0], jt[1], jt[2]); } + } + // the four neighbours (00863af0) + static const int dx4[4] = {-1, 0, 0, 1}, dy4[4] = {0, 1, -1, 0}; + static const uint8_t dirBit[4] = {kBlockLeft, kBlockDown, kBlockUp, kBlockRight}; + for (int k = 0; k < 4; ++k) { + const int nx = cx + dx4[k], ny = cy + dy4[k]; + if (!g.valid(nx, ny)) continue; + if (avoided(cx, cy) & dirBit[k]) continue; // a direction this creature got stuck going (00860c20) + const uint8_t nf = g.at(nx, ny); + if (nf & kNodeSolid) continue; + if (nf & p.danger_mask & 0x0F) continue; + if (g.clearance[g.idx(nx, ny)] < p.clearance_required) continue; + float extra = 0.0f; + if (nf & kNodeLiquid) { // swimming on the surface (free node above): +10000, diving: +13000 + const bool surface = g.valid(nx, ny - 1) && !(g.at(nx, ny - 1) & (kNodeSolid | kNodeLiquid)); + if (surface && p.can_swim_on_surface) extra = 10000.0f; + else if (p.can_dive) extra = 13000.0f; + else continue; + } + else if (!p.can_fly) { + // a walker enters a node without ground under it only by falling into it from above (or moving along the surface of a liquid) + const bool supported = g.solid(nx, ny + 1) || (g.at(nx, ny + 1) & kNodeLiquid) != 0; + // (00863af0 lets a walker whose node holds solid cells - uneven ground, a slope - step sideways into an unsupported node; here the nodes walked + // through are free of solid cells, so the same step down a slope is a sideways step whose node below has ground under it) + const bool stepDown = dy4[k] == 0 && g.solid(cx, cy + 1) && g.valid(nx, ny + 2) && !g.solid(nx, ny + 1) && g.solid(nx, ny + 2); + if (!supported && dy4[k] != 1 && !cLiquid && !stepDown) continue; + if (!p.can_walk && supported) continue; + } + else { + const bool supported = g.solid(nx, ny + 1); + if (!supported) extra = p.cost_of_flying; + } + push(nx, ny, ri, r.g + 10.0f + extra, r.depth + 1, false, 0, 0); + } + } + if (found < 0) { + // 00861e00 publishes a path only on success (+0xa2): a search that ran out of nodes returns none. One stopped by our per-call budget (the engine yields at 1000 + // units of work and resumes the next frames) keeps the best partial route of a directed search, standing in for the resumed search + budgetOut = !open.empty() && iterations >= p.iterations_max; + if (!budgetOut || !directed || closest <= 0) return out; + found = closest; + } + std::vector chain; + for (int ri = found; ri > 0; ri = recs[ri].parent) chain.push_back(ri); + std::reverse(chain.begin(), chain.end()); + if (!chain.empty() && recs[chain.front()].jump) chain.insert(chain.begin(), 0); // (a jump from the start: the start node is its source entry) + for (int ri : chain) { + const Rec& r = recs[ri]; + out.push_back({g.ox + (r.node % g.w) * 4 + 2, g.oy + (r.node / g.w) * 4 + 2, r.jump, r.jvx, r.jvy, false}); // (a node covers its 4 x 4 cells: their centre) + } + for (size_t i = 0; i + 1 < out.size(); ++i) if (out[i + 1].jump) out[i].jumpFrom = true; // (00861e00's post-pass: the entry before a jump) + return out; +} +} // namespace + +// TODO(to-do #34): source flags of jump entries, strict landings, jump ends through the relaxer, per-creature stuck memory, a resumable search +std::vector search(const PathGrid& g, float sx, float sy, const SearchParams& p) { + // 00861e00: the movement modes in passes - walking / diving / surface swimming alone first (flight and jumps off); only when that found no route the flying pass, + // then the jumping one. A route of an earlier pass wins even when a jump would be cheaper (docs/WEAK_ZOMBIE_AI_DIFFERENCES.md AI-01) + bool budget = false; + std::vector partial; + if (p.can_walk || p.can_dive || p.can_swim_on_surface) { + SearchParams q = p; q.can_fly = false; q.can_jump = false; + std::vector r = search_pass(g, sx, sy, q, budget); + if (!r.empty() && !budget) return r; + if (budget) partial = std::move(r); + } + if (p.can_fly) { + SearchParams q = p; q.can_jump = false; + std::vector r = search_pass(g, sx, sy, q, budget); + if (!r.empty() && !budget) return r; + if (budget && partial.empty()) partial = std::move(r); + } + if (p.can_jump) { + SearchParams q = p; q.can_fly = false; + std::vector r = search_pass(g, sx, sy, q, budget); + if (!r.empty() && !budget) return r; + if (budget && partial.empty()) partial = std::move(r); + } + return partial; +} + +} // namespace ng::ai diff --git a/src/ai/pathfind.h b/src/ai/pathfind.h new file mode 100644 index 0000000..19635a5 --- /dev/null +++ b/src/ai/pathfind.h @@ -0,0 +1,94 @@ +#pragma once +// The pathfinding of the creatures (PathFindingSystem 00bca030, search 00862620 / 00863af0, jumps 00860930 / 00860790, the goal logics 00864bf0..00865480). +// The grid has a node every 4 world pixels (the engine's chunks hold 128 x 128 nodes of 32 bytes over 512 x 512 pixels, 00635060 / 00635470); here it covers the +// simulated window and is rebuilt from the cells. docs/PATHFINDING_AND_ANIMAL_AI.md. +#include +#include +#include +#include + +namespace ng::ai { + +// node flags (byte +0xf of the engine's node, 00864060): counted over the node's 4 x 4 cells, from the pixel bytes of 00863fd0 (the path type of a cell, its vtable +// slot 11, e.g. CLiquidCell 0070b0b0: 3 = a liquid that is not liquid_sand, 1 = a solid; the danger bits of the material, a burning cell) +enum : uint8_t { + kNodeDangerFire = 1, // more than 2 cells burning or of a danger_fire material + kNodeDangerPoison = 2, // more than 2 cells danger_poison + kNodeDangerRadioactive = 4, // more than 2 cells danger_radioactive + kNodeDangerWater = 8, // more than 2 cells danger_water + kNodeFullSolid = 0x10, // more than 14 solid cells + kNodeLiquid = 0x20, // more than 10 liquid cells and no solid one + kNodeSolid = 0x40, // any solid cell +}; +// blocked directions of a node (byte +0x18, 00860bb0 / 00860c20): set when a creature got stuck going that way +enum : uint8_t { kBlockLeft = 1, kBlockRight = 2, kBlockUp = 4, kBlockDown = 8 }; + +struct PathGrid { + int ox = 0, oy = 0; // world pixel of node (0, 0); a multiple of 4 + int w = 0, h = 0; // nodes + std::vector flags, clearance, blocked; + std::vector liquidCells; // the liquid cells of the node, 0..16 (byte 0 of the engine's 3-byte node record, +0x80000) + std::vector blockedUntil; // the frame the blocked directions are forgotten (mNextClearDontApproachListFrame) + // PathFindingGridMarkerSystem (00bcdc80, every frame): the node at an entity's position + marker_offset holds its herd (byte +0x10, -1 none) and entity (+0x14); + // the player's player_marker_radius marks the nodes along 12 rays from its node (byte +0xe bit 0x10, the Charmed logic's goal) + std::vector markHerd; + std::vector markEnt; + std::vector playerArea; + void clear_marks(); + void mark(float wx, float wy, int herd, int entity); + void mark_player_area(float wx, float wy, float radius, float phase); + bool marked(int i) const { return !markHerd.empty() && markHerd[(size_t)i] >= 0; } + bool valid(int nx, int ny) const { return nx >= 0 && ny >= 0 && nx < w && ny < h; } + int idx(int nx, int ny) const { return ny * w + nx; } + uint8_t at(int nx, int ny) const { return valid(nx, ny) ? flags[idx(nx, ny)] : (uint8_t)kNodeSolid; } // (outside the grid: a wall) + bool solid(int nx, int ny) const { return (at(nx, ny) & kNodeSolid) != 0; } + // Rebuilds the flags from a classifier of world pixels (returns the flags of one cell: kNodeSolid, kNodeLiquid and the danger bits), counted per node, then the clearance (00860150: distance in nodes to the nearest solid, 0..4) + void build(int worldX, int worldY, int pixelsW, int pixelsH, const std::function& classify); + // keeps the blocked directions of the overlapping area when the window moves + void block(int nx, int ny, uint8_t dirs, int untilFrame); + void forget_blocks(int frame); +}; + +// Hunt: PathFindingLogic_Default 00864bf0; Burning 00865280; Away: EscapingPrey 008650c0; Escape: Escaping 00864ed0; Charmed 00865480 +enum class Goal { None, Hunt, RandomMove, Goto, MoveNear, Burning, Away, Escape, Charmed }; + +struct SearchParams { + bool can_fly = false, can_walk = true, can_jump = false, can_dive = false, can_swim_on_surface = false; + int clearance_required = 0; // space_required / 4 (00bca030: input +0xcc) + float cost_of_flying = 20.0f; + int depth_max = 50; // nodes along the path (AnimalAI pathfinding_max_depth_has_target / _no_target) + int iterations_max = 1500; + float jump_max_x = 100.0f, jump_max_y = 80.0f; // initial_jump_max_distance_x / _y (pixels) + float jump_speed = 200.0f; // pixels per second + float jump_lob = 1.0f; // initial_jump_lob: the flight takes lob x 60 x distance / jump_speed frames (0063ec90) + std::vector> jump_trajectories; // PathFindingComponent jump_trajectories (dx, dy, lob; each tried mirrored too): the jumps after the first fan + float gravity = 350.0f; // pixels per second squared of the jumping body + uint8_t danger_mask = kNodeDangerFire | kNodeDangerPoison | kNodeDangerRadioactive; + Goal goal = Goal::None; + float gx = 0, gy = 0; // the goal point (Hunt: the target's marker, Goto / MoveNear: the point) + float away_distance = 100.0f; // Away / Escape: a node at least this far (pixels) from the goal point (input +0x64) + float home_x = 0, home_y = 0, home_radius = 0; // mHomePosition, max_distance_to_move_from_home (input +0x68 / +0x6c / +0x70): the goals of Hunt, RandomMove, + // Burning, Away and Escape lie within it (Escape: within sqrt(0.2) of it) + int own_herd = -1, aggression = 100; // Hunt: a node marked by another herd whose relation is below the aggression (input +0x78 / +0x7c) ... + int target_entity = 0; // ... or by this entity (input +0x88 / +0x8c) + int random_distance = 100; // RandomMove: a node further than this (manhattan, pixels) from the start (00864ab0) + int* work = nullptr; // when set: the nodes the search expanded are added to it (the host's per-frame budget, standing in for the engine's resumable search) + // the directions this creature got stuck going, by world node (the search input's own tree, 00860c20 / 00860bf0: not shared with other creatures); key = world_node_key + const std::vector>* avoid = nullptr; +}; +inline int64_t world_node_key(int wnx, int wny) { return ((int64_t)wnx << 32) ^ (uint32_t)wny; } // (a node of the world: floor(pixel / 4)) + +struct PathNode { + int x = 0, y = 0; // world pixel of the node's centre (a node covers the cells 4n .. 4n + 3; a world pixel is in node floor(p / 4)) + bool jump = false; // reached by a jump from the node before + float jvx = 0, jvy = 0; // the launch velocity of that jump (pixels per second) + bool jumpFrom = false; // the source of the next node's jump (00861e00 propagates a flag onto it; 00bca030 +0x169: reached within 2 px in x, the launch from there) +}; + +// A* from the node of (sx, sy): the path from the start to the first node the goal accepts (start excluded); empty when none was found +std::vector search(const PathGrid& g, float sx, float sy, const SearchParams& p); + +// the launch velocity of a lob from (x0, y0) to (x1, y1) at `speed` under `gravity` (0063ec90); false when out of reach +bool lob_velocity(float x0, float y0, float x1, float y1, float speed, float gravity, float& vx, float& vy); + +} // namespace ng::ai diff --git a/src/app/clip.h b/src/app/clip.h new file mode 100644 index 0000000..8e7ec38 --- /dev/null +++ b/src/app/clip.h @@ -0,0 +1,544 @@ +#pragma once +// Replay recorder: keeps the last N presented frames (compressed with Blosc2/Zstd in a worker thread) and writes them to disk on request: every frame as a PNG +// (real deflate), an animated GIF, a contact sheet and a text file with the view / player position of every frame, so that a bug that only shows in motion can be +// looked at afterwards. Capture: the host copies the final output image into a host-visible buffer each frame and hands it to push() one frame later. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace ng { + +struct ClipMeta { int frame = 0, view_x = 0, view_y = 0; double px = 0, py = 0; double time_ms = 0; float scale = 1, psx = 0, psy = 0; }; // scale: output pixels per cell, psx / psy: the player on the screen (output pixels) +struct ClipFrame { int w = 0, h = 0; ClipMeta meta; std::vector comp; }; // one recorded frame: BGRA w*h*4, compressed + +// ---------- PNG with a real (fixed Huffman + LZ77) deflate, thread safe ---------- +namespace clipimpl { + +inline uint32_t crc32(const uint8_t* p, size_t n, uint32_t crc = 0xFFFFFFFFu) { + static const auto table = [] { + struct T { uint32_t v[256]; } t; + for (uint32_t i = 0; i < 256; ++i) { uint32_t c = i; for (int k = 0; k < 8; ++k) c = (c & 1) ? 0xEDB88320u ^ (c >> 1) : c >> 1; t.v[i] = c; } + return t; + }(); + for (size_t i = 0; i < n; ++i) crc = table.v[(crc ^ p[i]) & 0xFF] ^ (crc >> 8); + return crc; +} + +struct BitWriter { + std::vector& out; + uint32_t acc = 0; int nb = 0; + explicit BitWriter(std::vector& o) : out(o) {} + void bits(uint32_t v, int n) { acc |= v << nb; nb += n; while (nb >= 8) { out.push_back((uint8_t)acc); acc >>= 8; nb -= 8; } } + void huff(uint32_t code, int len) { uint32_t r = 0; for (int i = 0; i < len; ++i) r |= ((code >> i) & 1u) << (len - 1 - i); bits(r, len); } // codes go out most significant bit first + void flush() { if (nb > 0) { out.push_back((uint8_t)acc); acc = 0; nb = 0; } } +}; + +inline void put_lit(BitWriter& w, int c) { + if (c < 144) w.huff(0x30 + c, 8); + else if (c < 256) w.huff(0x190 + c - 144, 9); + else if (c < 280) w.huff(c - 256, 7); + else w.huff(0xC0 + c - 280, 8); +} + +inline std::vector deflate_fixed(const std::vector& in) { + static const uint16_t lbase[29] = {3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258}; + static const uint8_t lext[29] = {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0}; + static const uint16_t dbase[30] = {1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577}; + static const uint8_t dext[30] = {0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13}; + std::vector out; + out.reserve(in.size() / 4 + 64); + out.push_back(0x78); out.push_back(0x01); + BitWriter w(out); + w.bits(1, 1); w.bits(1, 2); // final block, fixed Huffman + const size_t n = in.size(); + std::vector head(1 << 15, -1), prev(n, -1); + auto hash = [&](size_t i) { return (uint32_t)((in[i] * 506832829u ^ in[i + 1] * 2654435761u ^ in[i + 2] * 40503u) >> 4) & 0x7FFFu; }; + size_t i = 0; + while (i < n) { + int bestLen = 0, bestDist = 0; + if (i + 3 <= n) { + const uint32_t h = hash(i); + int32_t cand = head[h]; + for (int depth = 0; cand >= 0 && depth < 24 && i - (size_t)cand <= 32768; ++depth, cand = prev[(size_t)cand]) { + const size_t maxl = std::min(258, n - i); + size_t l = 0; + while (l < maxl && in[(size_t)cand + l] == in[i + l]) ++l; + if ((int)l > bestLen) { bestLen = (int)l; bestDist = (int)(i - (size_t)cand); if (l == maxl) break; } + } + prev[i] = head[h]; head[h] = (int32_t)i; + } + if (bestLen >= 3) { + int lc = 28; while (lbase[lc] > bestLen) --lc; + put_lit(w, 257 + lc); + if (lext[lc]) w.bits((uint32_t)(bestLen - lbase[lc]), lext[lc]); + int dc = 29; while (dbase[dc] > bestDist) --dc; + w.huff((uint32_t)dc, 5); + if (dext[dc]) w.bits((uint32_t)(bestDist - dbase[dc]), dext[dc]); + for (int k = 1; k < bestLen; ++k) { const size_t j = i + (size_t)k; if (j + 3 <= n) { const uint32_t h = hash(j); prev[j] = head[h]; head[h] = (int32_t)j; } } + i += (size_t)bestLen; + } else { put_lit(w, in[i]); ++i; } + } + put_lit(w, 256); + w.flush(); + uint32_t a = 1, b = 0; + for (uint8_t v : in) { a = (a + v) % 65521; b = (b + a) % 65521; } + const uint32_t ad = (b << 16) | a; + for (int s = 24; s >= 0; s -= 8) out.push_back((ad >> s) & 0xFF); + return out; +} + +inline bool write_png(const std::string& path, const uint8_t* rgb, int w, int h) { + std::vector raw; + raw.reserve((size_t)h * (w * 3 + 1)); + for (int y = 0; y < h; ++y) { // filter 1 (sub) per row + const uint8_t* row = rgb + (size_t)y * w * 3; + raw.push_back(1); + for (int x = 0; x < w * 3; ++x) raw.push_back((uint8_t)(row[x] - (x >= 3 ? row[x - 3] : 0))); + } + const std::vector z = deflate_fixed(raw); + FILE* f = nullptr; + if (fopen_s(&f, path.c_str(), "wb") != 0 || !f) return false; + auto be32 = [](std::vector& v, uint32_t x) { for (int s = 24; s >= 0; s -= 8) v.push_back((x >> s) & 0xFF); }; + auto chunk = [&](const char* type, const std::vector& data) { + std::vector c; + be32(c, (uint32_t)data.size()); + c.insert(c.end(), type, type + 4); + c.insert(c.end(), data.begin(), data.end()); + be32(c, ~crc32(c.data() + 4, c.size() - 4)); + fwrite(c.data(), 1, c.size(), f); + }; + static const uint8_t sig[8] = {0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A}; + fwrite(sig, 1, 8, f); + std::vector ihdr; + be32(ihdr, (uint32_t)w); be32(ihdr, (uint32_t)h); + ihdr.insert(ihdr.end(), {8, 2, 0, 0, 0}); + chunk("IHDR", ihdr); chunk("IDAT", z); chunk("IEND", {}); + return fclose(f) == 0; +} + +// ---------- GIF (median cut palette, LZW) ---------- +struct Palette { uint8_t rgb[256][3]; int n = 0; std::vector lut; }; // lut: 15-bit colour -> index + +inline Palette make_palette(const std::vector& imgs, int w, int h) { // imgs: RGB w*h*3 + struct Box { std::vector px; }; // packed 0xRRGGBB + std::vector all; + for (const uint8_t* im : imgs) for (size_t i = 0; i < (size_t)w * h; i += 7) all.push_back((uint32_t)im[i * 3] << 16 | (uint32_t)im[i * 3 + 1] << 8 | im[i * 3 + 2]); + std::vector boxes(1); + boxes[0].px = std::move(all); + while ((int)boxes.size() < 256) { + size_t bi = 0, best = 0; + for (size_t k = 0; k < boxes.size(); ++k) if (boxes[k].px.size() > best) { best = boxes[k].px.size(); bi = k; } + if (best < 2) break; + Box& b = boxes[bi]; + int lo[3] = {255, 255, 255}, hi[3] = {0, 0, 0}; + for (uint32_t p : b.px) for (int c = 0; c < 3; ++c) { const int v = (p >> (16 - 8 * c)) & 255; lo[c] = std::min(lo[c], v); hi[c] = std::max(hi[c], v); } + int axis = 0; for (int c = 1; c < 3; ++c) if (hi[c] - lo[c] > hi[axis] - lo[axis]) axis = c; + if (hi[axis] == lo[axis]) { bool any = false; for (size_t k = 0; k < boxes.size(); ++k) if (k != bi && boxes[k].px.size() > 1) any = true; if (!any) break; boxes[bi].px.shrink_to_fit(); Box tmp; tmp.px.assign(1, b.px[0]); std::vector rest(b.px.begin() + 1, b.px.end()); b.px = std::move(rest); boxes.push_back(std::move(tmp)); continue; } + const int sh = 16 - 8 * axis; + std::sort(b.px.begin(), b.px.end(), [&](uint32_t x, uint32_t y) { return ((x >> sh) & 255) < ((y >> sh) & 255); }); + Box nb; + nb.px.assign(b.px.begin() + b.px.size() / 2, b.px.end()); + b.px.resize(b.px.size() / 2); + boxes.push_back(std::move(nb)); + } + Palette pal; + for (const Box& b : boxes) { + if (b.px.empty() || pal.n >= 256) continue; + uint64_t s[3] = {0, 0, 0}; + for (uint32_t p : b.px) for (int c = 0; c < 3; ++c) s[c] += (p >> (16 - 8 * c)) & 255; + for (int c = 0; c < 3; ++c) pal.rgb[pal.n][c] = (uint8_t)(s[c] / b.px.size()); + ++pal.n; + } + pal.lut.assign(32768, 0); + for (int k = 0; k < 32768; ++k) { + const int r = ((k >> 10) & 31) * 255 / 31, g = ((k >> 5) & 31) * 255 / 31, bl = (k & 31) * 255 / 31; + int bestd = 1 << 30, bi = 0; + for (int p = 0; p < pal.n; ++p) { const int dr = r - pal.rgb[p][0], dg = g - pal.rgb[p][1], db = bl - pal.rgb[p][2]; const int d = dr * dr + dg * dg + db * db; if (d < bestd) { bestd = d; bi = p; } } + pal.lut[(size_t)k] = (uint8_t)bi; + } + return pal; +} + +inline void gif_lzw(std::vector& out, const std::vector& idx) { + out.push_back(8); // minimum code size + std::vector blk; + uint32_t acc = 0; int nb = 0; + auto emit = [&](uint32_t code, int bits) { acc |= code << nb; nb += bits; while (nb >= 8) { blk.push_back((uint8_t)acc); acc >>= 8; nb -= 8; } }; + std::unordered_map dict; + int next = 258, bits = 9; + emit(256, bits); + uint32_t cur = idx.empty() ? 0 : idx[0]; + for (size_t i = 1; i < idx.size(); ++i) { + const uint32_t key = cur << 8 | idx[i]; + auto it = dict.find(key); + if (it != dict.end()) { cur = it->second; continue; } + emit(cur, bits); + if (next < 4096) { dict[key] = (uint16_t)next++; if (next > (1 << bits) && bits < 12) ++bits; } + else { emit(256, bits); dict.clear(); next = 258; bits = 9; } + cur = idx[i]; + } + if (!idx.empty()) emit(cur, bits); + emit(257, bits); + if (nb > 0) blk.push_back((uint8_t)acc); + for (size_t p = 0; p < blk.size(); p += 255) { const size_t c = std::min(255, blk.size() - p); out.push_back((uint8_t)c); out.insert(out.end(), blk.begin() + p, blk.begin() + p + c); } + out.push_back(0); +} + +inline std::mutex& blosc_mutex() { static std::mutex m; return m; } + +// The BGRA pixels of a recorded frame. +inline bool decode_bgra(const ClipFrame& f, std::vector& bgra) { + bgra.resize((size_t)f.w * f.h * 4); + std::lock_guard bl(blosc_mutex()); + return blosc2_decompress(f.comp.data(), (int32_t)f.comp.size(), bgra.data(), (int32_t)bgra.size()) == (int)bgra.size(); +} + +// The rectangle (x, y, w, h) of BGRA src (sw wide) to RGB dst of dw x dh, every output pixel the mean of the source pixels it covers. +inline void crop_scale_rgb(const uint8_t* src, int sw, int x, int y, int w, int h, int dw, int dh, std::vector& dst) { + dst.resize((size_t)dw * dh * 3); + std::vector xs(dw + 1); + for (int i = 0; i <= dw; ++i) xs[i] = x + (int)((int64_t)i * w / dw); + for (int j = 0; j < dh; ++j) { + const int y0 = y + (int)((int64_t)j * h / dh), y1 = std::max(y0 + 1, y + (int)((int64_t)(j + 1) * h / dh)); + for (int i = 0; i < dw; ++i) { + const int x0 = xs[i], x1 = std::max(x0 + 1, xs[i + 1]); + uint32_t s[3] = {0, 0, 0}; + for (int yy = y0; yy < y1; ++yy) { const uint8_t* p = src + ((size_t)yy * sw + x0) * 4; for (int xx = x0; xx < x1; ++xx, p += 4) { s[0] += p[2]; s[1] += p[1]; s[2] += p[0]; } } + const uint32_t n = (uint32_t)((y1 - y0) * (x1 - x0)); + uint8_t* d = &dst[((size_t)j * dw + i) * 3]; + d[0] = (uint8_t)(s[0] / n); d[1] = (uint8_t)(s[1] / n); d[2] = (uint8_t)(s[2] / n); + } + } +} + +// The palette of one image (rgb, n pixels) for a GIF local colour table: a histogram of all its pixels (6 bits per channel), cut into boxes, always the box with +// the largest squared error, at the weighted mean of its widest channel. A small patch of a colour far from the rest (the player's robe, a spark) then gets an +// entry of its own instead of being averaged into a large box, as the plain median cut by pixel count does; an image with at most 256 colours keeps them all. +// No lut is made (GifWriter finds the nearest entry of the colours it meets). +inline Palette make_palette_frame(const uint8_t* rgb, size_t n) { + std::vector hist(1u << 18, 0); + for (size_t i = 0; i < n; ++i) ++hist[(uint32_t)(rgb[i * 3] >> 2) << 12 | (uint32_t)(rgb[i * 3 + 1] >> 2) << 6 | (uint32_t)(rgb[i * 3 + 2] >> 2)]; + struct E { uint32_t c, n; }; + std::vector es; + for (uint32_t c = 0; c < hist.size(); ++c) if (hist[c]) es.push_back({c, hist[c]}); + auto ch = [](uint32_t c, int k) { const uint32_t v = (c >> (12 - 6 * k)) & 63; return (double)(v << 2 | v >> 4); }; + struct Box { size_t b, e; double n, s[3], sse; }; + auto stats = [&](size_t b, size_t e) { + Box bx{b, e, 0, {0, 0, 0}, 0}; + double s2 = 0; + for (size_t i = b; i < e; ++i) for (int k = 0; k < 3; ++k) { const double v = ch(es[i].c, k); bx.s[k] += v * es[i].n; s2 += v * v * es[i].n; } + for (size_t i = b; i < e; ++i) bx.n += es[i].n; + bx.sse = s2 - (bx.s[0] * bx.s[0] + bx.s[1] * bx.s[1] + bx.s[2] * bx.s[2]) / std::max(1.0, bx.n); + return bx; + }; + std::vector boxes; + if (!es.empty()) boxes.push_back(stats(0, es.size())); + while (boxes.size() < 256) { + size_t bi = 0; + for (size_t k = 1; k < boxes.size(); ++k) if (boxes[k].sse > boxes[bi].sse) bi = k; + const Box bx = boxes[bi]; + if (bx.sse <= 0.5 || bx.e - bx.b < 2) break; + int axis = 0; double best = -1; + for (int k = 0; k < 3; ++k) { + const double m = bx.s[k] / bx.n; + double var = 0; + for (size_t i = bx.b; i < bx.e; ++i) { const double d = ch(es[i].c, k) - m; var += d * d * es[i].n; } + if (var > best) { best = var; axis = k; } + } + const double mean = bx.s[axis] / bx.n; + const auto mid = std::partition(es.begin() + (std::ptrdiff_t)bx.b, es.begin() + (std::ptrdiff_t)bx.e, [&](const E& x) { return ch(x.c, axis) <= mean; }); + const size_t m = (size_t)(mid - es.begin()); + if (m == bx.b || m == bx.e) break; + boxes[bi] = stats(bx.b, m); + boxes.push_back(stats(m, bx.e)); + } + Palette pal; + for (const Box& bx : boxes) { + for (int k = 0; k < 3; ++k) pal.rgb[pal.n][k] = (uint8_t)std::clamp((int)std::lround(bx.s[k] / bx.n), 0, 255); + ++pal.n; + } + if (pal.n == 0) { pal.rgb[0][0] = pal.rgb[0][1] = pal.rgb[0][2] = 0; pal.n = 1; } + return pal; +} + +// Writes a looping GIF frame by frame, so that a long clip does not have to be held in memory: with one global palette (made with make_palette, its lut used), +// or with a local colour table per frame (open without a palette, add with one from make_palette_frame), optionally with a light ordered (Bayer 4x4) dither. +class GifWriter { +public: + ~GifWriter() { if (f_) std::fclose(f_); } + bool open(const std::string& path, int w, int h, const Palette* pal) { + w_ = w; h_ = h; pal_ = pal; bytes_ = 0; + if (fopen_s(&f_, path.c_str(), "wb") != 0 || !f_) { f_ = nullptr; return false; } + std::vector g; + const char* hdr = "GIF89a"; + g.insert(g.end(), hdr, hdr + 6); + g.push_back((uint8_t)(w & 255)); g.push_back((uint8_t)(w >> 8)); g.push_back((uint8_t)(h & 255)); g.push_back((uint8_t)(h >> 8)); + g.push_back(pal ? 0xF7 : 0x70); g.push_back(0); g.push_back(0); + if (pal) for (int p = 0; p < 256; ++p) for (int c = 0; c < 3; ++c) g.push_back(p < pal->n ? pal->rgb[p][c] : 0); + const uint8_t app[] = {0x21, 0xFF, 0x0B, 'N', 'E', 'T', 'S', 'C', 'A', 'P', 'E', '2', '.', '0', 0x03, 0x01, 0, 0, 0}; // loop forever + g.insert(g.end(), app, app + sizeof app); + put(g); + return true; + } + // rgb: w*h*3; delay in hundredths of a second; local: the palette of this frame (a local colour table); dither: the span of the ordered dither (0 = none). + void add(const uint8_t* rgb, int delay_cs, const Palette* local = nullptr, int dither = 0) { + if (!f_ || (!local && !pal_)) return; + std::vector idx((size_t)w_ * h_), g; + if (!local) for (size_t i = 0; i < idx.size(); ++i) idx[i] = pal_->lut[(size_t)((rgb[i * 3] >> 3) << 10 | (rgb[i * 3 + 1] >> 3) << 5 | (rgb[i * 3 + 2] >> 3))]; + else { // the nearest entry of each colour met (6 bits per channel), cached + static const int bayer[4][4] = {{0, 8, 2, 10}, {12, 4, 14, 6}, {3, 11, 1, 9}, {15, 7, 13, 5}}; + std::vector nearest(1u << 18, -1); + for (int y = 0; y < h_; ++y) for (int x = 0; x < w_; ++x) { + const size_t i = (size_t)y * w_ + x; + const int d = dither ? (bayer[y & 3][x & 3] * 2 - 15) * dither / 32 : 0; + const int r = std::clamp(rgb[i * 3] + d, 0, 255), gg = std::clamp(rgb[i * 3 + 1] + d, 0, 255), b = std::clamp(rgb[i * 3 + 2] + d, 0, 255); + const uint32_t key = (uint32_t)(r >> 2) << 12 | (uint32_t)(gg >> 2) << 6 | (uint32_t)(b >> 2); + int16_t& e = nearest[key]; + if (e < 0) { + int bestd = 1 << 30, bi = 0; + for (int p = 0; p < local->n; ++p) { const int dr = r - local->rgb[p][0], dg = gg - local->rgb[p][1], db = b - local->rgb[p][2]; const int dd = dr * dr + dg * dg + db * db; if (dd < bestd) { bestd = dd; bi = p; } } + e = (int16_t)bi; + } + idx[i] = (uint8_t)e; + } + } + const uint8_t gce[] = {0x21, 0xF9, 0x04, 0x00, (uint8_t)(delay_cs & 255), (uint8_t)(delay_cs >> 8), 0, 0}; + g.insert(g.end(), gce, gce + sizeof gce); + g.push_back(0x2C); g.push_back(0); g.push_back(0); g.push_back(0); g.push_back(0); + g.push_back((uint8_t)(w_ & 255)); g.push_back((uint8_t)(w_ >> 8)); g.push_back((uint8_t)(h_ & 255)); g.push_back((uint8_t)(h_ >> 8)); + if (local) { g.push_back(0x87); for (int p = 0; p < 256; ++p) for (int c = 0; c < 3; ++c) g.push_back(p < local->n ? local->rgb[p][c] : 0); } // a local colour table of 256 entries + else g.push_back(0); + gif_lzw(g, idx); + put(g); + } + // Returns the size of the file, 0 if it could not be written. + size_t close() { + if (!f_) return 0; + const uint8_t end = 0x3B; + std::fwrite(&end, 1, 1, f_); ++bytes_; + const bool ok = std::fclose(f_) == 0 && !failed_; + f_ = nullptr; + return ok ? bytes_ : 0; + } +private: + void put(const std::vector& g) { if (std::fwrite(g.data(), 1, g.size(), f_) != g.size()) failed_ = true; bytes_ += g.size(); } + FILE* f_ = nullptr; + int w_ = 0, h_ = 0; + const Palette* pal_ = nullptr; + size_t bytes_ = 0; + bool failed_ = false; +}; + +// The frames of a GIF from the frame times (ms): every step-th frame of [in, out], each shown until the next one picked, in whole hundredths rounded on the running +// clock so that the GIF keeps the real speed. A frame that would last less than 2 cs is left out (browsers show such frames at 10 cs), the one before it lasts longer. +struct GifPick { int index = 0; int delay_cs = 2; }; +inline std::vector gif_pick(const std::vector& t_ms, int in, int out, int step) { + std::vector picks; + if (t_ms.empty() || in > out) return picks; + step = std::max(1, step); + const double per = out > in ? (t_ms[(size_t)out] - t_ms[(size_t)in]) / (out - in) : 1000.0 / 60.0; // the mean frame time (the last frame lasts that long times the step) + int prevCs = 0; + for (int i = in; i <= out; i += step) { + const int cs = (int)std::lround((t_ms[(size_t)i] - t_ms[(size_t)in]) / 10.0); + if (!picks.empty()) { if (cs - prevCs < 2) continue; picks.back().delay_cs = cs - prevCs; } + GifPick p; p.index = i; picks.push_back(p); + prevCs = cs; + } + const int last = picks.back().index; + const int endCs = (int)std::lround((t_ms[(size_t)last] - t_ms[(size_t)in] + per * step) / 10.0); + picks.back().delay_cs = std::max(2, endCs - prevCs); + return picks; +} + +} // namespace clipimpl + +class ClipRecorder { +public: + explicit ClipRecorder(size_t max_frames) : max_(max_frames) { blosc2_init(); worker_ = std::thread([this] { run(); }); } + ~ClipRecorder() { + { std::lock_guard l(mu_); stop_ = true; } + cv_.notify_all(); + if (worker_.joinable()) worker_.join(); + for (auto& t : writers_) if (t.joinable()) t.join(); + } + size_t frames() { std::lock_guard l(ringMu_); return ring_.size(); } + size_t dropped() const { return dropped_.load(); } + size_t bytes() { std::lock_guard l(ringMu_); size_t b = 0; for (const auto& f : ring_) b += f->comp.size(); return b; } // the compressed size of the ring + // The frames of the ring now (oldest first); they stay valid while the ring moves on. + std::vector> snapshot() { std::lock_guard l(ringMu_); return std::vector>(ring_.begin(), ring_.end()); } + + // bgra: w*h*4 bytes of the output image (blue first), read by the worker thread straight from where it is (a mapped buffer): `busy` is true until it is done with it. + void push_ref(const uint8_t* bgra, int w, int h, const ClipMeta& m, std::atomic* busy) { + std::lock_guard l(mu_); + if (queue_.size() >= 6) { ++dropped_; busy->store(false); return; } + Job j; j.w = w; j.h = h; j.meta = m; j.ref = bgra; j.busy = busy; + queue_.push_back(std::move(j)); + cv_.notify_one(); + } + + // Writes the frames of the ring to /clip_