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 <sfan5@live.de>
This commit is contained in:
Lars Müller 2024-11-19 13:30:17 +01:00 committed by GitHub
parent f493e73aeb
commit 138052adfc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 93 additions and 33 deletions

@ -44,6 +44,7 @@ core.features = {
bulk_lbms = true, bulk_lbms = true,
abm_without_neighbors = true, abm_without_neighbors = true,
biome_weights = true, biome_weights = true,
particle_blend_clip = true,
} }
function core.has_feature(arg) function core.has_feature(arg)

@ -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 * merge `sound` and `sounds` table in itemdef
* remove `DIR_DELIM` from Lua * remove `DIR_DELIM` from Lua
* stop reading initial properties from bare entity def * stop reading initial properties from bare entity def
* change particle default blend mode to `clip`

@ -5657,6 +5657,8 @@ Utilities
abm_without_neighbors = true, abm_without_neighbors = true,
-- biomes have a weight parameter (5.11.0) -- biomes have a weight parameter (5.11.0)
biome_weights = true, 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 -- (default) blends transparent pixels with those they are drawn atop
-- according to the alpha channel of the source texture. useful for -- according to the alpha channel of the source texture. useful for
-- e.g. material objects like rocks, dirt, smoke, or node chunks -- 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", blend = "add",
-- adds the value of pixels to those underneath them, modulo the sources -- 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 -- alpha channel. useful for e.g. bright light effects like sparks or fire

@ -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", { core.register_tool("testtools:particle_spawner", {
description = "Particle Spawner".."\n".. description = table.concat({
"Particle Spawner",
"Punch: Spawn random test particle", "Punch: Spawn random test particle",
"Place: Spawn clip test particle",
}, "\n"),
inventory_image = "testtools_particle_spawner.png", inventory_image = "testtools_particle_spawner.png",
groups = { testtool = 1, disable_repair = 1 }, groups = { testtool = 1, disable_repair = 1 },
on_use = function(itemstack, user, pointed_thing) on_use = function(itemstack, user, pointed_thing)
local pos = core.get_pointed_thing_position(pointed_thing, true) local pos = core.get_pointed_thing_position(pointed_thing, true)
if pos == nil then if pos == nil then
if user then pos = assert(user):get_pos()
pos = user:get_pos()
end
end end
pos = vector.add(pos, {x=0, y=0.5, z=0}) pos = vector.add(pos, {x=0, y=0.5, z=0})
local tex, anim local tex, anim
@ -32,5 +45,12 @@ core.register_tool("testtools:particle_spawner", {
glow = math.random(0, 5), glow = math.random(0, 5),
}) })
end, 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,
}) })

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 B

@ -22,6 +22,8 @@
#include "settings.h" #include "settings.h"
#include "profiler.h" #include "profiler.h"
using BlendMode = ParticleParamTypes::BlendMode;
ClientParticleTexture::ClientParticleTexture(const ServerParticleTexture& p, ITextureSource *tsrc) ClientParticleTexture::ClientParticleTexture(const ServerParticleTexture& p, ITextureSource *tsrc)
{ {
tex = p; tex = p;
@ -603,8 +605,11 @@ video::S3DVertex *ParticleBuffer::getVertices(u16 index)
void ParticleBuffer::OnRegisterSceneNode() void ParticleBuffer::OnRegisterSceneNode()
{ {
if (IsVisible) if (IsVisible) {
SceneManager->registerNodeForRendering(this, scene::ESNRP_TRANSPARENT_EFFECT); SceneManager->registerNodeForRendering(this,
m_mesh_buffer->getMaterial().MaterialType == video::EMT_TRANSPARENT_ALPHA_CHANNEL_REF
? scene::ESNRP_SOLID : scene::ESNRP_TRANSPARENT_EFFECT);
}
scene::ISceneNode::OnRegisterSceneNode(); scene::ISceneNode::OnRegisterSceneNode();
} }
@ -906,6 +911,9 @@ void ParticleManager::addNodeParticle(IGameDef *gamedef,
if (!getNodeParticleParams(n, f, p, &ref, texpos, texsize, &color)) if (!getNodeParticleParams(n, f, p, &ref, texpos, texsize, &color))
return; return;
p.texture.blendmode = f.alpha == ALPHAMODE_BLEND
? BlendMode::alpha : BlendMode::clip;
p.expirationtime = myrand_range(0, 100) / 100.0f; p.expirationtime = myrand_range(0, 100) / 100.0f;
// Physics // Physics
@ -940,40 +948,47 @@ void ParticleManager::reserveParticleSpace(size_t max_estimate)
m_particles.reserve(m_particles.size() + 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_FACTOR bfsrc, bfdst;
video::E_BLEND_OPERATION blendop; video::E_BLEND_OPERATION blendop;
const auto blendmode = texture.tex ? texture.tex->blendmode :
ParticleParamTypes::BlendMode::alpha;
switch (blendmode) { switch (blendmode) {
case ParticleParamTypes::BlendMode::add: case BlendMode::add:
bfsrc = video::EBF_SRC_ALPHA; bfsrc = video::EBF_SRC_ALPHA;
bfdst = video::EBF_DST_ALPHA; bfdst = video::EBF_DST_ALPHA;
blendop = video::EBO_ADD; blendop = video::EBO_ADD;
break; break;
case ParticleParamTypes::BlendMode::sub: case BlendMode::sub:
bfsrc = video::EBF_SRC_ALPHA; bfsrc = video::EBF_SRC_ALPHA;
bfdst = video::EBF_DST_ALPHA; bfdst = video::EBF_DST_ALPHA;
blendop = video::EBO_REVSUBTRACT; blendop = video::EBO_REVSUBTRACT;
break; break;
case ParticleParamTypes::BlendMode::screen: case BlendMode::screen:
bfsrc = video::EBF_ONE; bfsrc = video::EBF_ONE;
bfdst = video::EBF_ONE_MINUS_SRC_COLOR; bfdst = video::EBF_ONE_MINUS_SRC_COLOR;
blendop = video::EBO_ADD; blendop = video::EBO_ADD;
break; break;
default: // includes ParticleParamTypes::BlendMode::alpha default: // includes BlendMode::alpha
bfsrc = video::EBF_SRC_ALPHA; bfsrc = video::EBF_SRC_ALPHA;
bfdst = video::EBF_ONE_MINUS_SRC_ALPHA; bfdst = video::EBF_ONE_MINUS_SRC_ALPHA;
blendop = video::EBO_ADD; blendop = video::EBO_ADD;
break; 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; video::SMaterial material;
// Texture // Texture
@ -984,17 +999,18 @@ video::SMaterial ParticleManager::getMaterialForParticle(const ClientParticleTex
tex.MagFilter = video::ETMAGF_NEAREST; tex.MagFilter = video::ETMAGF_NEAREST;
}); });
// We don't have working transparency sorting. Disable Z-Write for const auto blendmode = particle->getBlendMode();
// correct results for clipped-alpha at least. if (blendmode == BlendMode::clip) {
material.ZWriteEnable = video::EZW_OFF; material.ZWriteEnable = video::EZW_ON;
material.MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL_REF;
// enable alpha blending and set blend mode material.MaterialTypeParam = 0.5f;
material.MaterialType = video::EMT_ONETEXTURE_BLEND; } else {
material.MaterialTypeParam = video::pack_textureBlendFunc( // We don't have working transparency sorting. Disable Z-Write for
bfsrc, bfdst, // correct results for clipped-alpha at least.
video::EMFN_MODULATE_1X, material.ZWriteEnable = video::EZW_OFF;
video::EAS_TEXTURE | video::EAS_VERTEX_COLOR); material.MaterialType = video::EMT_ONETEXTURE_BLEND;
material.BlendOperation = blendop; setBlendMode(material, blendmode);
}
material.setTexture(0, texture.ref); material.setTexture(0, texture.ref);
return material; return material;
@ -1004,7 +1020,7 @@ bool ParticleManager::addParticle(std::unique_ptr<Particle> toadd)
{ {
MutexAutoLock lock(m_particle_list_lock); MutexAutoLock lock(m_particle_list_lock);
auto material = getMaterialForParticle(toadd->getTextureRef()); auto material = getMaterialForParticle(toadd.get());
ParticleBuffer *found = nullptr; ParticleBuffer *found = nullptr;
// simple shortcut when multiple particles of the same type get added // simple shortcut when multiple particles of the same type get added

@ -77,6 +77,9 @@ public:
const ClientParticleTexRef &getTextureRef() const { return m_texture; } 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; } ParticleBuffer *getBuffer() const { return m_buffer; }
bool attachToBuffer(ParticleBuffer *buffer); bool attachToBuffer(ParticleBuffer *buffer);
@ -231,7 +234,7 @@ protected:
ParticleParameters &p, video::ITexture **texture, v2f &texpos, ParticleParameters &p, video::ITexture **texture, v2f &texpos,
v2f &texsize, video::SColor *color, u8 tilenum = 0); 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<Particle> toadd); bool addParticle(std::unique_ptr<Particle> toadd);

@ -59,9 +59,12 @@
Rename TOSERVER_RESPAWN to TOSERVER_RESPAWN_LEGACY Rename TOSERVER_RESPAWN to TOSERVER_RESPAWN_LEGACY
Support float animation frame numbers in TOCLIENT_LOCAL_PLAYER_ANIMATIONS Support float animation frame numbers in TOCLIENT_LOCAL_PLAYER_ANIMATIONS
[scheduled bump for 5.10.0] [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 // See also formspec [Version History] in doc/lua_api.md
const u16 FORMSPEC_API_VERSION = 8; const u16 FORMSPEC_API_VERSION = 8;

@ -190,8 +190,10 @@ void ServerParticleTexture::serialize(std::ostream &os, u16 protocol_ver,
FlagT flags = 0; FlagT flags = 0;
if (animated) if (animated)
flags |= FlagT(ParticleTextureFlags::animated); flags |= FlagT(ParticleTextureFlags::animated);
if (blendmode != BlendMode::alpha) // Default to `blend = "alpha"` for older clients that don't support `blend = "clip"`
flags |= FlagT(blendmode) << 1; auto sent_blendmode = (protocol_ver < 47 && blendmode == BlendMode::clip)
? BlendMode::alpha : blendmode;
flags |= FlagT(sent_blendmode) << 1;
serializeParameterValue(os, flags); serializeParameterValue(os, flags);
alpha.serialize(os); alpha.serialize(os);
@ -215,6 +217,8 @@ void ServerParticleTexture::deSerialize(std::istream &is, u16 protocol_ver,
animated = !!(flags & FlagT(ParticleTextureFlags::animated)); animated = !!(flags & FlagT(ParticleTextureFlags::animated));
blendmode = BlendMode((flags & FlagT(ParticleTextureFlags::blend)) >> 1); blendmode = BlendMode((flags & FlagT(ParticleTextureFlags::blend)) >> 1);
if (blendmode >= BlendMode::BlendMode_END)
throw SerializationError("invalid blend mode");
alpha.deSerialize(is); alpha.deSerialize(is);
scale.deSerialize(is); scale.deSerialize(is);

@ -233,7 +233,8 @@ namespace ParticleParamTypes
} }
enum class AttractorKind : u8 { none, point, line, plane }; 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 // these are consistently-named convenience aliases to make code more readable without `using ParticleParamTypes` declarations
using v3fRange = RangedParameter<v3fParameter>; using v3fRange = RangedParameter<v3fParameter>;

@ -118,13 +118,14 @@ namespace LuaParticleParams
{(int)BlendMode::add, "add"}, {(int)BlendMode::add, "add"},
{(int)BlendMode::sub, "sub"}, {(int)BlendMode::sub, "sub"},
{(int)BlendMode::screen, "screen"}, {(int)BlendMode::screen, "screen"},
{(int)BlendMode::clip, "clip"},
{0, nullptr}, {0, nullptr},
}; };
luaL_checktype(L, -1, LUA_TSTRING); luaL_checktype(L, -1, LUA_TSTRING);
int v = (int)BlendMode::alpha; int v = (int)BlendMode::alpha;
if (!string_to_enum(opts, v, lua_tostring(L, -1))) { 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; ret = (BlendMode)v;
} }