Skip to content

Commit

Permalink
Add particle blend mode "clip" (#15444)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
appgurueu and sfan5 authored Nov 19, 2024
1 parent f493e73 commit 138052a
Show file tree
Hide file tree
Showing 11 changed files with 93 additions and 33 deletions.
1 change: 1 addition & 0 deletions builtin/game/features.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions doc/breakages.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
10 changes: 10 additions & 0 deletions doc/lua_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
```

Expand Down Expand Up @@ -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
Expand Down
28 changes: 24 additions & 4 deletions games/devtest/mods/testtools/particles.lua
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
})

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
62 changes: 39 additions & 23 deletions src/client/particles.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
#include "settings.h"
#include "profiler.h"

using BlendMode = ParticleParamTypes::BlendMode;

ClientParticleTexture::ClientParticleTexture(const ServerParticleTexture& p, ITextureSource *tsrc)
{
tex = p;
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -1004,7 +1020,7 @@ bool ParticleManager::addParticle(std::unique_ptr<Particle> 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
Expand Down
5 changes: 4 additions & 1 deletion src/client/particles.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ class Particle

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

Expand Down Expand Up @@ -231,7 +234,7 @@ class ParticleManager
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<Particle> toadd);

Expand Down
5 changes: 4 additions & 1 deletion src/network/networkprotocol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
8 changes: 6 additions & 2 deletions src/particles.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion src/particles.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<v3fParameter>;
Expand Down
3 changes: 2 additions & 1 deletion src/script/lua_api/l_particleparams.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down

0 comments on commit 138052a

Please sign in to comment.