From 138052adfc2074fc6e42d0f8856d6cdca37dbd77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20M=C3=BCller?= <34514239+appgurueu@users.noreply.github.com> Date: Tue, 19 Nov 2024 13:30:17 +0100 Subject: [PATCH] Add particle blend mode "clip" (#15444) This lets modders avoid alpha blending rendering bugs as well as potential (future) performance issues. The appropriate blend modes are also used for node dig particles. --------- Co-authored-by: sfan5 --- builtin/game/features.lua | 1 + doc/breakages.md | 1 + doc/lua_api.md | 10 +++ games/devtest/mods/testtools/particles.lua | 28 ++++++-- .../textures/testtools_particle_clip.png | Bin 0 -> 179 bytes src/client/particles.cpp | 62 +++++++++++------- src/client/particles.h | 5 +- src/network/networkprotocol.cpp | 5 +- src/particles.cpp | 8 ++- src/particles.h | 3 +- src/script/lua_api/l_particleparams.h | 3 +- 11 files changed, 93 insertions(+), 33 deletions(-) create mode 100644 games/devtest/mods/testtools/textures/testtools_particle_clip.png diff --git a/builtin/game/features.lua b/builtin/game/features.lua index 8d7753839..ea80a09fb 100644 --- a/builtin/game/features.lua +++ b/builtin/game/features.lua @@ -44,6 +44,7 @@ core.features = { bulk_lbms = true, abm_without_neighbors = true, biome_weights = true, + particle_blend_clip = true, } function core.has_feature(arg) diff --git a/doc/breakages.md b/doc/breakages.md index 0aded3d92..2d2994385 100644 --- a/doc/breakages.md +++ b/doc/breakages.md @@ -21,3 +21,4 @@ This list is largely advisory and items may be reevaluated once the time comes. * merge `sound` and `sounds` table in itemdef * remove `DIR_DELIM` from Lua * stop reading initial properties from bare entity def +* change particle default blend mode to `clip` diff --git a/doc/lua_api.md b/doc/lua_api.md index 48ef798c4..e82b075fb 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -5657,6 +5657,8 @@ Utilities abm_without_neighbors = true, -- biomes have a weight parameter (5.11.0) biome_weights = true, + -- Particles can specify a "clip" blend mode (5.11.0) + particle_blend_clip = true, } ``` @@ -11483,6 +11485,14 @@ texture = { -- (default) blends transparent pixels with those they are drawn atop -- according to the alpha channel of the source texture. useful for -- e.g. material objects like rocks, dirt, smoke, or node chunks + -- note: there will be rendering bugs when particles interact with + -- translucent nodes. particles are also not transparency-sorted + -- relative to each other. + blend = "clip", + -- pixels are either fully opaque or fully transparent, + -- depending on whether alpha is greater than or less than 50% + -- (just like `use_texture_alpha = "clip"` for nodes). + -- you should prefer this if you don't need semi-transparency, as it's faster. blend = "add", -- adds the value of pixels to those underneath them, modulo the sources -- alpha channel. useful for e.g. bright light effects like sparks or fire diff --git a/games/devtest/mods/testtools/particles.lua b/games/devtest/mods/testtools/particles.lua index 17f4f5c80..da6b6cab9 100644 --- a/games/devtest/mods/testtools/particles.lua +++ b/games/devtest/mods/testtools/particles.lua @@ -1,14 +1,27 @@ +local function spawn_clip_test_particle(pos) + core.add_particle({ + pos = pos, + size = 5, + expirationtime = 10, + texture = { + name = "testtools_particle_clip.png", + blend = "clip", + }, + }) +end + core.register_tool("testtools:particle_spawner", { - description = "Particle Spawner".."\n".. + description = table.concat({ + "Particle Spawner", "Punch: Spawn random test particle", + "Place: Spawn clip test particle", + }, "\n"), inventory_image = "testtools_particle_spawner.png", groups = { testtool = 1, disable_repair = 1 }, on_use = function(itemstack, user, pointed_thing) local pos = core.get_pointed_thing_position(pointed_thing, true) if pos == nil then - if user then - pos = user:get_pos() - end + pos = assert(user):get_pos() end pos = vector.add(pos, {x=0, y=0.5, z=0}) local tex, anim @@ -32,5 +45,12 @@ core.register_tool("testtools:particle_spawner", { glow = math.random(0, 5), }) end, + on_place = function(itemstack, user, pointed_thing) + local pos = assert(core.get_pointed_thing_position(pointed_thing, true)) + spawn_clip_test_particle(pos) + end, + on_secondary_use = function(_, user) + spawn_clip_test_particle(assert(user):get_pos()) + end, }) diff --git a/games/devtest/mods/testtools/textures/testtools_particle_clip.png b/games/devtest/mods/testtools/textures/testtools_particle_clip.png new file mode 100644 index 0000000000000000000000000000000000000000..5fb9ad09a6c8596e60004b045007fdecb5039c08 GIT binary patch literal 179 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|ay?xfLoEE0 zQx*sqG=166BmR%y*uCay^f6}r*8kQW`&mynHhegJ`v3i|*GBt9=JAHJ3yIgyKE+|q zutabf<9tR1-vsa0^#Agv4QC7-`bxW)1k(g=UbM(!P|_<%Q=1_au*~V4&xSn>LLF0{ aWH5Bde-YerZTTjk#SEUVelF{r5}E*N6h21) literal 0 HcmV?d00001 diff --git a/src/client/particles.cpp b/src/client/particles.cpp index 6ba7fa701..c6891f8ef 100644 --- a/src/client/particles.cpp +++ b/src/client/particles.cpp @@ -22,6 +22,8 @@ #include "settings.h" #include "profiler.h" +using BlendMode = ParticleParamTypes::BlendMode; + ClientParticleTexture::ClientParticleTexture(const ServerParticleTexture& p, ITextureSource *tsrc) { tex = p; @@ -603,8 +605,11 @@ video::S3DVertex *ParticleBuffer::getVertices(u16 index) void ParticleBuffer::OnRegisterSceneNode() { - if (IsVisible) - SceneManager->registerNodeForRendering(this, scene::ESNRP_TRANSPARENT_EFFECT); + if (IsVisible) { + SceneManager->registerNodeForRendering(this, + m_mesh_buffer->getMaterial().MaterialType == video::EMT_TRANSPARENT_ALPHA_CHANNEL_REF + ? scene::ESNRP_SOLID : scene::ESNRP_TRANSPARENT_EFFECT); + } scene::ISceneNode::OnRegisterSceneNode(); } @@ -906,6 +911,9 @@ void ParticleManager::addNodeParticle(IGameDef *gamedef, if (!getNodeParticleParams(n, f, p, &ref, texpos, texsize, &color)) return; + p.texture.blendmode = f.alpha == ALPHAMODE_BLEND + ? BlendMode::alpha : BlendMode::clip; + p.expirationtime = myrand_range(0, 100) / 100.0f; // Physics @@ -940,40 +948,47 @@ void ParticleManager::reserveParticleSpace(size_t max_estimate) m_particles.reserve(m_particles.size() + max_estimate); } -video::SMaterial ParticleManager::getMaterialForParticle(const ClientParticleTexRef &texture) +static void setBlendMode(video::SMaterial &material, BlendMode blendmode) { - // translate blend modes to GL blend functions video::E_BLEND_FACTOR bfsrc, bfdst; video::E_BLEND_OPERATION blendop; - const auto blendmode = texture.tex ? texture.tex->blendmode : - ParticleParamTypes::BlendMode::alpha; - switch (blendmode) { - case ParticleParamTypes::BlendMode::add: + case BlendMode::add: bfsrc = video::EBF_SRC_ALPHA; bfdst = video::EBF_DST_ALPHA; blendop = video::EBO_ADD; break; - case ParticleParamTypes::BlendMode::sub: + case BlendMode::sub: bfsrc = video::EBF_SRC_ALPHA; bfdst = video::EBF_DST_ALPHA; blendop = video::EBO_REVSUBTRACT; break; - case ParticleParamTypes::BlendMode::screen: + case BlendMode::screen: bfsrc = video::EBF_ONE; bfdst = video::EBF_ONE_MINUS_SRC_COLOR; blendop = video::EBO_ADD; break; - default: // includes ParticleParamTypes::BlendMode::alpha + default: // includes BlendMode::alpha bfsrc = video::EBF_SRC_ALPHA; bfdst = video::EBF_ONE_MINUS_SRC_ALPHA; blendop = video::EBO_ADD; break; } + material.MaterialTypeParam = video::pack_textureBlendFunc( + bfsrc, bfdst, + video::EMFN_MODULATE_1X, + video::EAS_TEXTURE | video::EAS_VERTEX_COLOR); + material.BlendOperation = blendop; +} + +video::SMaterial ParticleManager::getMaterialForParticle(const Particle *particle) +{ + const ClientParticleTexRef &texture = particle->getTextureRef(); + video::SMaterial material; // Texture @@ -984,17 +999,18 @@ video::SMaterial ParticleManager::getMaterialForParticle(const ClientParticleTex tex.MagFilter = video::ETMAGF_NEAREST; }); - // We don't have working transparency sorting. Disable Z-Write for - // correct results for clipped-alpha at least. - material.ZWriteEnable = video::EZW_OFF; - - // enable alpha blending and set blend mode - material.MaterialType = video::EMT_ONETEXTURE_BLEND; - material.MaterialTypeParam = video::pack_textureBlendFunc( - bfsrc, bfdst, - video::EMFN_MODULATE_1X, - video::EAS_TEXTURE | video::EAS_VERTEX_COLOR); - material.BlendOperation = blendop; + const auto blendmode = particle->getBlendMode(); + if (blendmode == BlendMode::clip) { + material.ZWriteEnable = video::EZW_ON; + material.MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL_REF; + material.MaterialTypeParam = 0.5f; + } else { + // We don't have working transparency sorting. Disable Z-Write for + // correct results for clipped-alpha at least. + material.ZWriteEnable = video::EZW_OFF; + material.MaterialType = video::EMT_ONETEXTURE_BLEND; + setBlendMode(material, blendmode); + } material.setTexture(0, texture.ref); return material; @@ -1004,7 +1020,7 @@ bool ParticleManager::addParticle(std::unique_ptr toadd) { MutexAutoLock lock(m_particle_list_lock); - auto material = getMaterialForParticle(toadd->getTextureRef()); + auto material = getMaterialForParticle(toadd.get()); ParticleBuffer *found = nullptr; // simple shortcut when multiple particles of the same type get added diff --git a/src/client/particles.h b/src/client/particles.h index 6a8d09a47..619877744 100644 --- a/src/client/particles.h +++ b/src/client/particles.h @@ -77,6 +77,9 @@ public: const ClientParticleTexRef &getTextureRef() const { return m_texture; } + ParticleParamTypes::BlendMode getBlendMode() const + { return m_texture.tex ? m_texture.tex->blendmode : m_p.texture.blendmode; } + ParticleBuffer *getBuffer() const { return m_buffer; } bool attachToBuffer(ParticleBuffer *buffer); @@ -231,7 +234,7 @@ protected: ParticleParameters &p, video::ITexture **texture, v2f &texpos, v2f &texsize, video::SColor *color, u8 tilenum = 0); - static video::SMaterial getMaterialForParticle(const ClientParticleTexRef &texture); + static video::SMaterial getMaterialForParticle(const Particle *texture); bool addParticle(std::unique_ptr toadd); diff --git a/src/network/networkprotocol.cpp b/src/network/networkprotocol.cpp index 40f8acef1..f77e85d3c 100644 --- a/src/network/networkprotocol.cpp +++ b/src/network/networkprotocol.cpp @@ -59,9 +59,12 @@ Rename TOSERVER_RESPAWN to TOSERVER_RESPAWN_LEGACY Support float animation frame numbers in TOCLIENT_LOCAL_PLAYER_ANIMATIONS [scheduled bump for 5.10.0] + PROTOCOL VERSION 47 + Add particle blend mode "clip" + [scheduled bump for 5.11.0] */ -const u16 LATEST_PROTOCOL_VERSION = 46; +const u16 LATEST_PROTOCOL_VERSION = 47; // See also formspec [Version History] in doc/lua_api.md const u16 FORMSPEC_API_VERSION = 8; diff --git a/src/particles.cpp b/src/particles.cpp index 2c470d2d3..660ea8d2f 100644 --- a/src/particles.cpp +++ b/src/particles.cpp @@ -190,8 +190,10 @@ void ServerParticleTexture::serialize(std::ostream &os, u16 protocol_ver, FlagT flags = 0; if (animated) flags |= FlagT(ParticleTextureFlags::animated); - if (blendmode != BlendMode::alpha) - flags |= FlagT(blendmode) << 1; + // Default to `blend = "alpha"` for older clients that don't support `blend = "clip"` + auto sent_blendmode = (protocol_ver < 47 && blendmode == BlendMode::clip) + ? BlendMode::alpha : blendmode; + flags |= FlagT(sent_blendmode) << 1; serializeParameterValue(os, flags); alpha.serialize(os); @@ -215,6 +217,8 @@ void ServerParticleTexture::deSerialize(std::istream &is, u16 protocol_ver, animated = !!(flags & FlagT(ParticleTextureFlags::animated)); blendmode = BlendMode((flags & FlagT(ParticleTextureFlags::blend)) >> 1); + if (blendmode >= BlendMode::BlendMode_END) + throw SerializationError("invalid blend mode"); alpha.deSerialize(is); scale.deSerialize(is); diff --git a/src/particles.h b/src/particles.h index 52fe26661..00d04dbeb 100644 --- a/src/particles.h +++ b/src/particles.h @@ -233,7 +233,8 @@ namespace ParticleParamTypes } enum class AttractorKind : u8 { none, point, line, plane }; - enum class BlendMode : u8 { alpha, add, sub, screen }; + // Note: Allows at most 8 enum members (due to how this is serialized) + enum class BlendMode : u8 { alpha, add, sub, screen, clip, BlendMode_END }; // these are consistently-named convenience aliases to make code more readable without `using ParticleParamTypes` declarations using v3fRange = RangedParameter; diff --git a/src/script/lua_api/l_particleparams.h b/src/script/lua_api/l_particleparams.h index 7303081db..0e508148f 100644 --- a/src/script/lua_api/l_particleparams.h +++ b/src/script/lua_api/l_particleparams.h @@ -118,13 +118,14 @@ namespace LuaParticleParams {(int)BlendMode::add, "add"}, {(int)BlendMode::sub, "sub"}, {(int)BlendMode::screen, "screen"}, + {(int)BlendMode::clip, "clip"}, {0, nullptr}, }; luaL_checktype(L, -1, LUA_TSTRING); int v = (int)BlendMode::alpha; if (!string_to_enum(opts, v, lua_tostring(L, -1))) { - throw LuaError("blend mode must be one of ('alpha', 'add', 'sub', 'screen')"); + throw LuaError("blend mode must be one of ('alpha', 'clip', 'add', 'sub', 'screen')"); } ret = (BlendMode)v; }