Add particle blend mode "clip" ()

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
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

(image error) 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;
}); });
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 // We don't have working transparency sorting. Disable Z-Write for
// correct results for clipped-alpha at least. // correct results for clipped-alpha at least.
material.ZWriteEnable = video::EZW_OFF; material.ZWriteEnable = video::EZW_OFF;
// enable alpha blending and set blend mode
material.MaterialType = video::EMT_ONETEXTURE_BLEND; material.MaterialType = video::EMT_ONETEXTURE_BLEND;
material.MaterialTypeParam = video::pack_textureBlendFunc( setBlendMode(material, blendmode);
bfsrc, bfdst, }
video::EMFN_MODULATE_1X,
video::EAS_TEXTURE | video::EAS_VERTEX_COLOR);
material.BlendOperation = blendop;
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;
} }