forked from mirrors/cobblemon
minor refector and edits
This commit is contained in:
+3
-4
@@ -36,10 +36,9 @@ public class LambDynamicLightsInitializer implements DynamicLightsInitializer {
|
||||
ctx.register(EntityType.PLAYER, PlayerLuminance.INSTANCE);
|
||||
});
|
||||
|
||||
// Attach a real monochrome dynamic light to INDIVIDUAL glowing particles. Each light follows
|
||||
// its particle and reports isRemoved() when the particle dies, so the manager cleans it up. The
|
||||
// per-emitter cap on how many particles may be lit at once is enforced at particle spawn (see
|
||||
// BedrockParticle.maxLitParticles / SnowstormParticle), not here.
|
||||
// Attach a monochrome dynamic light to each glowing particle. The light follows its particle and
|
||||
// reports isRemoved() when the particle dies, so the manager cleans it up. The cap on how many
|
||||
// particles per emitter may be lit at once is enforced at particle spawn, not here.
|
||||
final DynamicLightBehaviorManager behaviorManager = context.dynamicLightBehaviorManager();
|
||||
MoveDynamicLights.setSink(particle -> behaviorManager.add(new SnowstormParticleLight(particle)));
|
||||
}
|
||||
|
||||
-2
@@ -37,8 +37,6 @@ public final class PokemonLuminance extends CustomLuminance implements EntityLum
|
||||
ItemStack item = pokemon.getShownItem();
|
||||
int itemLightLevel = item.isEmpty() ? 0 : itemLightSourceManager.getLuminance(item, underwater);
|
||||
int formLightLevel = extractFormLightLevel(pokemon.getForm(), underwater).orElse(0);
|
||||
|
||||
// Animation based emissives
|
||||
int animLightLevel = PokemonAnimationLight.luminanceFor(pokemon);
|
||||
|
||||
return Math.max(Math.max(itemLightLevel, formLightLevel), animLightLevel);
|
||||
|
||||
+11
-14
@@ -14,13 +14,11 @@ import net.minecraft.core.BlockPos;
|
||||
|
||||
/**
|
||||
* A real, monochrome dynamic light attached to a single Cobblemon particle, so each glowing particle
|
||||
* lights the world around it like its own light source (maximum fidelity). The light level is whatever
|
||||
* the particle currently emits ({@link SnowstormParticle#currentLightLevel()} — a flat author-set level
|
||||
* or a per-sprite-frame value), so a particle that emits 0 contributes nothing even though it exists.
|
||||
* lights the world around it like its own light source. The level is whatever the particle currently
|
||||
* emits ({@link SnowstormParticle#currentLightLevel()}); a particle emitting 0 contributes nothing.
|
||||
*
|
||||
* <p>The number of these per emitter is capped (see {@code BedrockParticle.maxLitParticles}); the cap
|
||||
* is enforced at particle spawn, not here. This follows the particle as it moves and removes itself
|
||||
* when the particle dies.</p>
|
||||
* <p>How many of these exist per emitter is capped (by the maxLitParticlesPerEmitter config, enforced
|
||||
* at particle spawn, not here). This follows the particle as it moves and removes itself when it dies.</p>
|
||||
*
|
||||
* <p>Only instantiated by {@link LambDynamicLightsInitializer} (the LambDynamicLights entrypoint), so
|
||||
* it is never loaded unless LambDynamicLights is installed.</p>
|
||||
@@ -33,7 +31,7 @@ public final class SnowstormParticleLight implements DynamicLightBehavior {
|
||||
private int prevY = Integer.MIN_VALUE;
|
||||
private int prevZ = Integer.MIN_VALUE;
|
||||
private int prevLuminance = -1;
|
||||
/** Level cached each tick in hasChanged() so the per-block lightAtPos stays cheap and consistent. */
|
||||
/** Level cached each tick in hasChanged() so the per block lightAtPos call stays cheap and stable. */
|
||||
private double cachedLuminance;
|
||||
|
||||
public SnowstormParticleLight(SnowstormParticle particle) {
|
||||
@@ -49,18 +47,17 @@ public final class SnowstormParticleLight implements DynamicLightBehavior {
|
||||
double dy = pos.getY() + 0.5 - particle.getY();
|
||||
double dz = pos.getZ() + 0.5 - particle.getZ();
|
||||
double dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
// Vanilla-style falloff: exactly 1 light level lost per block, so an authored level of N lights
|
||||
// a ~N-block radius just like a Minecraft block-light source. We deliberately IGNORE the
|
||||
// falloffRatio LambDynamicLights passes (15 / MAX_RADIUS ~= 1.94), which would otherwise
|
||||
// compress the falloff into the mod's 7.75-block cap and make every light reach only ~half its
|
||||
// level in blocks (i.e. look dimmer than the number implies). Note: the mod still hard-caps
|
||||
// reach at MAX_RADIUS (~7.75 blocks), so authored levels above ~8 are clipped at that radius.
|
||||
// Lose exactly 1 level per block so an authored level of N lights about N blocks, like vanilla
|
||||
// block light. We intentionally ignore the falloffRatio LambDynamicLights passes (15 / MAX_RADIUS,
|
||||
// about 1.94): applying it would compress the falloff into the mod's 7.75 block cap and make every
|
||||
// light reach only about half its level in blocks, looking dimmer than the number implies. The mod
|
||||
// still limits reach to MAX_RADIUS (about 7.75 blocks), so levels above 8 are clipped there.
|
||||
return Math.max(0.0, lum - dist);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DynamicLightBehavior.BoundingBox getBoundingBox() {
|
||||
// The actively-emitting volume is just the particle's block (the API expands by falloff itself).
|
||||
// Report only the emitting volume (the particle's own block); the API expands it by falloff.
|
||||
int x = (int) Math.floor(particle.getX());
|
||||
int y = (int) Math.floor(particle.getY());
|
||||
int z = (int) Math.floor(particle.getZ());
|
||||
|
||||
@@ -44,22 +44,22 @@ class BedrockParticle(
|
||||
var collision: ParticleCollision = ParticleCollision(),
|
||||
var environmentLighting: Boolean = false,
|
||||
/**
|
||||
* Block-light level (0..15) each particle of this type emits into the world via LambDynamicLights.
|
||||
* A particle existing does NOT imply light: only particles whose resolved level is > 0 emit. This
|
||||
* scalar is the flat "hard" level used when [lightEmissionFrames] is empty.
|
||||
* Block light level (0..15) each particle of this type emits into the world via LambDynamicLights.
|
||||
* A particle existing does not imply light: only particles whose resolved level is above 0 emit.
|
||||
* This is the flat level, used when [lightEmissionFrames] is empty.
|
||||
*/
|
||||
var lightEmission: Int = 0,
|
||||
/**
|
||||
* Optional per-sprite-frame light levels (0..15), indexed by the particle's current flipbook frame.
|
||||
* When non-empty it overrides [lightEmission], so a sprite-sheet particle can light differently per
|
||||
* frame (out-of-range frames clamp to the ends). Empty = use the flat [lightEmission].
|
||||
* Optional light levels (0..15) indexed by the particle's current flipbook frame. When non empty it
|
||||
* overrides [lightEmission] so a particle can light differently per frame; frames beyond the array
|
||||
* clamp to its ends. Empty means use the flat [lightEmission].
|
||||
*/
|
||||
var lightEmissionFrames: List<Int> = emptyList(),
|
||||
var creationEvents: MutableList<SimpleEventTrigger> = mutableListOf(),
|
||||
var expirationEvents: MutableList<SimpleEventTrigger> = mutableListOf(),
|
||||
var timeline: EventTriggerTimeline = EventTriggerTimeline(mutableMapOf())
|
||||
) {
|
||||
/** True when this particle type emits block light at all (flat level or any per-frame level > 0). */
|
||||
/** True when this particle type emits block light at all, via the flat level or any per frame level. */
|
||||
val emitsLight: Boolean
|
||||
get() = lightEmission > 0 || lightEmissionFrames.any { it > 0 }
|
||||
|
||||
|
||||
@@ -45,8 +45,8 @@ abstract class ParticleUVMode : CodecMapped {
|
||||
|
||||
abstract fun get(moLangRuntime: MoLangRuntime, age: Double, maxAge: Double, uvDetails: UVDetails): UVDetails
|
||||
|
||||
/** The current sprite-sheet frame index for a particle at this age (0 for non-animated modes).
|
||||
* Used to look up per-frame light levels (see [BedrockParticle.lightEmissionFrames]). */
|
||||
/** The current flipbook frame index for a particle at this age (0 for static modes). Used to look
|
||||
* up per frame light levels (see [BedrockParticle.lightEmissionFrames]). */
|
||||
open fun frameAt(moLangRuntime: MoLangRuntime, age: Double, maxAge: Double): Int = 0
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.cobblemon.mod.common.client.gui.tmmachine.TMMachineScreen
|
||||
import com.cobblemon.mod.common.client.particle.BedrockParticleOptionsRepository
|
||||
import com.cobblemon.mod.common.client.render.ClientPlayerIcon
|
||||
import com.cobblemon.mod.common.client.render.DeferredRenderer
|
||||
import com.cobblemon.mod.common.client.render.PokemonAnimationLight
|
||||
import com.cobblemon.mod.common.client.render.SnowstormShaderBypassRenderer
|
||||
import com.cobblemon.mod.common.client.render.block.*
|
||||
import com.cobblemon.mod.common.client.render.boat.CobblemonBoatRenderer
|
||||
@@ -141,7 +142,7 @@ object CobblemonClient {
|
||||
PlatformEvents.CLIENT_PLAYER_LOGIN.subscribe { onLogin() }
|
||||
PlatformEvents.CLIENT_PLAYER_LOGOUT.subscribe { onLogout() }
|
||||
|
||||
// Renders snowstorm particles with a vanilla-identical look when a shaderpack is active.
|
||||
// Redraws snowstorm particles so they match vanilla when a shaderpack is active.
|
||||
SnowstormShaderBypassRenderer.register()
|
||||
|
||||
this.registerBlockEntityRenderers()
|
||||
@@ -474,8 +475,8 @@ object CobblemonClient {
|
||||
LOGGER.info("Loading assets...")
|
||||
// Particles come first because animations need them.
|
||||
BedrockParticleOptionsRepository.loadEffects(resourceManager)
|
||||
// Per-animation emissive light windows (LambDynamicLights, no-op without it).
|
||||
com.cobblemon.mod.common.client.render.PokemonAnimationLight.load(resourceManager)
|
||||
// Emissive light windows keyed by animation; does nothing unless LambDynamicLights is installed.
|
||||
PokemonAnimationLight.load(resourceManager)
|
||||
// Animations come next because models need them.
|
||||
BedrockAnimationRepository.loadAnimations(
|
||||
resourceManager = resourceManager,
|
||||
|
||||
+10
-11
@@ -51,27 +51,26 @@ object BedrockParticleOptionsRepository {
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads per-particle dynamic-light levels from a separate Cobblemon data location and merges them
|
||||
* onto the already-loaded effects. This is kept OUT of the `.particle.json` on purpose: Snowstorm
|
||||
* (the Bedrock particle editor) rebuilds the components block on export and silently drops unknown
|
||||
* components, so a light field inside the particle file would be lost on re-save. A sidecar file is
|
||||
* never touched by Snowstorm.
|
||||
* Loads particle light levels from a separate Cobblemon data location and merges them onto the
|
||||
* already loaded effects. These are kept out of the `.particle.json` on purpose: Snowstorm (the
|
||||
* Bedrock particle editor) rebuilds the components block on export and silently drops unknown
|
||||
* components, so a light field inside the particle file would be lost the next time it is saved.
|
||||
* A sidecar file is never touched by Snowstorm.
|
||||
*
|
||||
* Files live under `assets/<namespace>/dynamiclights/particles/` (any `.json` file), each a map
|
||||
* of particle-effect id to its light config, e.g.:
|
||||
* Files live under `assets/<namespace>/dynamiclights/particles/` (any `.json` file), each a map of
|
||||
* effect id to its light config, e.g.:
|
||||
* ```json
|
||||
* {
|
||||
* "cobblemon:thunderbolt_actor": { "light_level": 14 },
|
||||
* "cobblemon:flamethrower_actor": { "light_levels": [0, 8, 14, 14, 8] }
|
||||
* }
|
||||
* ```
|
||||
* `light_level` is a flat 0..15 level every particle emits; the optional `light_levels` array gives
|
||||
* a level per sprite-sheet frame (overrides the flat level). Entries for unknown effect ids are
|
||||
* ignored.
|
||||
* `light_level` is a flat level (0..15) every particle emits; the optional `light_levels` array gives
|
||||
* a level per flipbook frame and overrides the flat level. Entries for unknown effect ids are ignored.
|
||||
*/
|
||||
private fun loadLightConfig(resourceManager: ResourceManager) {
|
||||
// LambDynamicLights is the only thing that consumes these levels. If it isn't installed, don't
|
||||
// even read the sidecar files — the light config is never parsed, applied, or considered.
|
||||
// even read the sidecar files; the light config is never parsed, applied, or considered.
|
||||
val lambDynamicLightsInstalled = Cobblemon.implementation.isModInstalled("lambdynlights") ||
|
||||
Cobblemon.implementation.isModInstalled("lambdynamiclights")
|
||||
if (!lambDynamicLightsInstalled) return
|
||||
|
||||
+7
-7
@@ -11,14 +11,14 @@ package com.cobblemon.mod.common.client.particle
|
||||
import com.cobblemon.mod.common.client.render.SnowstormParticle
|
||||
|
||||
/**
|
||||
* API-free bridge that lets the optional LambDynamicLights integration attach a real dynamic light to
|
||||
* individual glowing particles without the common particle code referencing the LambDynamicLights API.
|
||||
* Bridge that lets the optional LambDynamicLights integration attach a dynamic light to individual
|
||||
* glowing particles without the common particle code referencing the LambDynamicLights API directly.
|
||||
*
|
||||
* Each particle whose resolved light level is > 0 (and that wins one of its emitter's limited light
|
||||
* slots) registers its own light through [sink]. When LambDynamicLights is installed, its entrypoint
|
||||
* ([com.cobblemon.mod.common.compat.lambdynamiclights.LambDynamicLightsInitializer]) sets [sink] to a
|
||||
* handler that adds a per-particle `DynamicLightBehavior`. When it isn't installed [sink] stays null
|
||||
* and the registration call is a no-op — so there is no hard dependency.
|
||||
* Each particle whose resolved light level is above 0 (and that claims one of its emitter's limited
|
||||
* light slots) registers its own light through [sink]. When LambDynamicLights is installed its
|
||||
* entrypoint ([com.cobblemon.mod.common.compat.lambdynamiclights.LambDynamicLightsInitializer]) sets
|
||||
* [sink]; when it is absent [sink] stays null and the registration call does nothing, so there is no
|
||||
* hard dependency.
|
||||
*/
|
||||
object MoveDynamicLights {
|
||||
@JvmStatic
|
||||
|
||||
@@ -376,16 +376,14 @@ class ParticleStorm(
|
||||
particles.forEach { particle -> particle.invisible = true }
|
||||
}
|
||||
|
||||
// --- LambDynamicLights (per-particle dynamic light) integration -----------------------------
|
||||
|
||||
/** How many of this emitter's live particles currently hold a real dynamic light (capped by the
|
||||
* `maxLitParticlesPerEmitter` Minecraft config). A particle existing does NOT imply a light. */
|
||||
/** How many of this emitter's live particles currently hold a dynamic light, capped by the
|
||||
* maxLitParticlesPerEmitter config. A particle existing does not imply a light. */
|
||||
var litParticleCount = 0
|
||||
private set
|
||||
|
||||
/** Tries to claim one of this emitter's limited light slots for a particle. Returns true if a slot
|
||||
* was available (and reserved); false if the emitter is already at the configured cap. The cap is
|
||||
* the `maxLitParticlesPerEmitter` config value (0 disables per-particle lights entirely). */
|
||||
/** Claims one of this emitter's limited light slots for a particle. Returns true if a slot was
|
||||
* free (and reserved), false if the emitter is at the configured cap. A cap of 0 disables the
|
||||
* particle lights entirely. */
|
||||
fun reserveLightSlot(): Boolean {
|
||||
if (litParticleCount >= Cobblemon.config.maxLitParticlesPerEmitter) return false
|
||||
litParticleCount++
|
||||
|
||||
+9
-9
@@ -21,16 +21,16 @@ import java.nio.charset.StandardCharsets
|
||||
import net.minecraft.server.packs.resources.ResourceManager
|
||||
|
||||
/**
|
||||
* Per-animation emissive light windows for Pokemon whose emissive parts only appear or flare during
|
||||
* specific animations (Pikachu cheeks, the Cyndaquil line's flames flaring in battle, Bellibolt's belly,
|
||||
* etc.). A Pokemon emits the window's block-light level only while a matching animation's elapsed time
|
||||
* falls inside one of the windows. Consulted by the LambDynamicLights `PokemonLuminance`.
|
||||
* Light levels emitted by a Pokemon's emissive parts during specific animations, for the optional
|
||||
* LambDynamicLights integration. Some Pokemon only show their emissive geometry at certain moments
|
||||
* (Pikachu's cheeks, the Cyndaquil line's flames flaring in battle, Bellibolt's belly), so the level
|
||||
* applies only while a matching animation's elapsed time falls inside one of its windows.
|
||||
*
|
||||
* Matches three kinds of running animation: the current PRIMARY animation (moves like special/physical/
|
||||
* status), ACTIVE/stateful animations (cry/battle_cry), and the current POSE's idle animations
|
||||
* (battle_idle/air_idle/air_fly etc., which loop continuously). Data is loaded from JSON under
|
||||
* `assets/<namespace>/dynamiclights/entity/animations/`: species path -> animation short name ->
|
||||
* list of `{ start, end, level }` (seconds into the animation; looping/continuous anims wrap).
|
||||
* Windows are matched against the current primary animation (moves such as special, physical, status),
|
||||
* active stateful animations (cry, battle_cry), and the current pose's idle animations (battle_idle,
|
||||
* air_idle, air_fly) which loop continuously. Data lives in JSON under
|
||||
* `assets/<namespace>/dynamiclights/entity/animations/`, mapping species to animation name to a list
|
||||
* of `{ start, end, level }` in seconds.
|
||||
*/
|
||||
object PokemonAnimationLight {
|
||||
private val GSON = GsonBuilder().create()
|
||||
|
||||
@@ -14,9 +14,9 @@ import java.lang.reflect.Method
|
||||
* Detects whether a shaderpack is currently active, via Iris (Fabric/NeoForge) or OptiFine.
|
||||
*
|
||||
* Every reference here is reflective and fully guarded: Iris/OptiFine are optional and may be
|
||||
* absent at runtime, so we never hard-reference their classes. If neither is present (or their
|
||||
* absent at runtime, so we never reference their classes directly. If neither is present (or their
|
||||
* APIs ever change shape) the lookups fail silently during class init and [shadersActive] simply
|
||||
* returns `false`. This is the loader-safe contract the rest of the particle code relies on — it
|
||||
* returns `false`. The rest of the particle code relies on this contract staying safe under any loader — it
|
||||
* must work identically on Fabric and NeoForge and when no shader mod is installed at all.
|
||||
*
|
||||
* The reflective handles are resolved once; only the cheap boolean accessor is invoked per call.
|
||||
|
||||
+17
-18
@@ -57,11 +57,11 @@ class SnowstormParticle(
|
||||
const val MAXIMUM_DISTANCE_CHANGE_PER_TICK_FOR_FRICTION = 0.005
|
||||
|
||||
/**
|
||||
* Every live (non-removed) snowstorm particle, in spawn order. When a shaderpack is active we
|
||||
* pull these out of the vanilla [net.minecraft.client.particle.ParticleEngine] (see
|
||||
* [getRenderType] returning [NO_RENDER]) and redraw them post-composite via
|
||||
* [SnowstormShaderBypassRenderer], so the pack's particle program can't alter their look.
|
||||
* Insertion-ordered; particles use identity equality so a LinkedHashSet is safe.
|
||||
* Every live snowstorm particle, in spawn order. When a shaderpack is active we pull these out
|
||||
* of the vanilla [net.minecraft.client.particle.ParticleEngine] (see [getRenderType] returning
|
||||
* [NO_RENDER]) and redraw them after composite via [SnowstormShaderBypassRenderer] so the pack's
|
||||
* particle program can't alter their look. Ordered by insertion; particles use identity equality
|
||||
* so a LinkedHashSet is safe.
|
||||
*/
|
||||
val activeParticles: MutableSet<SnowstormParticle> = LinkedHashSet()
|
||||
}
|
||||
@@ -143,10 +143,9 @@ class SnowstormParticle(
|
||||
storm.effect.particle.creationEvents.forEach { it.trigger(storm, this) }
|
||||
activeParticles.add(this)
|
||||
|
||||
// If LambDynamicLights is installed and this particle type emits light, give THIS particle its
|
||||
// own real dynamic light — but only if the emitter still has a free light slot (capped by
|
||||
// BedrockParticle.maxLitParticles). A particle existing does not imply light; the level comes
|
||||
// from currentLightLevel(). No-op when the mod is absent (sink stays null).
|
||||
// Give this particle its own dynamic light only when LambDynamicLights is present, the type
|
||||
// emits light, and the emitter still has a free light slot. The level comes from
|
||||
// currentLightLevel(); when the mod is absent the sink is null and this does nothing.
|
||||
val sink = MoveDynamicLights.sink
|
||||
if (sink != null && storm.effect.particle.emitsLight && storm.reserveLightSlot()) {
|
||||
litSlot = true
|
||||
@@ -155,9 +154,9 @@ class SnowstormParticle(
|
||||
}
|
||||
|
||||
/**
|
||||
* The block-light level (0..15) this particle emits right now. Uses the per-sprite-frame array when
|
||||
* the type defines one (indexed by the current flipbook frame), else the flat level. Invisible
|
||||
* (culled) particles emit nothing.
|
||||
* The block light level (0..15) this particle emits right now. Uses the per frame array when the
|
||||
* type defines one (indexed by the current flipbook frame), otherwise the flat level. Invisible
|
||||
* particles emit nothing.
|
||||
*/
|
||||
fun currentLightLevel(): Int {
|
||||
if (invisible) return 0
|
||||
@@ -535,18 +534,18 @@ class SnowstormParticle(
|
||||
runtime.environment.variable.setDirectly("particle_lifetime", DoubleValue(lifetime / 20.0))
|
||||
}
|
||||
|
||||
// Under an active shaderpack we suppress the vanilla ParticleEngine draw (it would be routed
|
||||
// through the pack's translucent particle program and mangled differently per pack) and instead
|
||||
// redraw post-composite in SnowstormShaderBypassRenderer for a vanilla-identical, pack-agnostic
|
||||
// look. With no shaders this is the normal material sheet, so the majority path is unchanged.
|
||||
// Under an active shaderpack we suppress the vanilla ParticleEngine draw (it would be routed through
|
||||
// the pack's translucent particle program and mangled differently per pack) and instead redraw after
|
||||
// composite in SnowstormShaderBypassRenderer so the look matches vanilla under any pack. With no
|
||||
// shaders this is the normal material sheet, so the majority path is unchanged.
|
||||
override fun getRenderType() = if (Cobblemon.config.shaderParticleBypass && ShaderCompat.shadersActive()) NO_RENDER else particleTextureSheet
|
||||
|
||||
override fun remove() {
|
||||
super.remove()
|
||||
storm.particles.remove(this)
|
||||
activeParticles.remove(this)
|
||||
// Free our dynamic-light slot so another particle of this emitter can light up. The registered
|
||||
// behavior reports isRemoved() via this particle's isAlive(), so the manager drops the light.
|
||||
// Free our light slot so another particle of this emitter can light up. The registered behavior
|
||||
// reports isRemoved() via this particle's isAlive(), so the manager drops the light on its own.
|
||||
if (litSlot) {
|
||||
storm.releaseLightSlot()
|
||||
litSlot = false
|
||||
|
||||
+13
-13
@@ -18,22 +18,22 @@ import net.minecraft.client.Minecraft
|
||||
import org.joml.Matrix4f
|
||||
|
||||
/**
|
||||
* Renders Cobblemon's snowstorm particles with a vanilla-identical, pack-agnostic look while a
|
||||
* Renders Cobblemon's snowstorm particles so they look like vanilla under any pack while a
|
||||
* shaderpack (Iris/OptiFine) is active.
|
||||
*
|
||||
* ## Why this exists
|
||||
* Iris/OptiFine replace the vanilla particle shader with the shaderpack's translucent-particle
|
||||
* Iris/OptiFine replace the vanilla particle shader with the shaderpack's translucent particle
|
||||
* program for the duration of the world pass, and every pack composites that program differently —
|
||||
* which made our particles render too solid (Complementary), invisible (Photon), or still-darkening
|
||||
* which made our particles render too solid (Complementary), invisible (Photon), or still darkening
|
||||
* (Bliss) no matter how we tuned blend/alpha inside the vanilla [net.minecraft.client.particle.ParticleEngine].
|
||||
* The pack has final authority there; we cannot win.
|
||||
*
|
||||
* The one place a shaderpack does NOT intercept a draw is **after** `LevelRenderer.renderLevel`
|
||||
* returns: Iris only overrides shaders while `isRenderingWorld` is true, and it flips that off during
|
||||
* its final composite (injected at the world-render RETURN). So we:
|
||||
* its final composite (injected at the RETURN of world rendering). So we:
|
||||
* 1. suppress the vanilla draw of these particles while shaders are active
|
||||
* ([SnowstormParticle.getRenderType] returns NO_RENDER), and
|
||||
* 2. redraw them here at [RenderEvent.Stage.POST_LEVEL] (post-composite) with the exact vanilla GL
|
||||
* 2. redraw them here at [RenderEvent.Stage.POST_LEVEL] (after composite) with the exact vanilla GL
|
||||
* state from [com.cobblemon.mod.common.api.snowstorm.ParticleMaterials] and the unchanged
|
||||
* [SnowstormParticle.render] quad math.
|
||||
*
|
||||
@@ -43,16 +43,16 @@ import org.joml.Matrix4f
|
||||
* pack's lighting/fog/bloom, which is exactly the intended "looks like vanilla" result.
|
||||
*
|
||||
* When no shaderpack is active this renderer does nothing and particles draw normally through the
|
||||
* ParticleEngine, so the majority (no-shader) path is completely unchanged.
|
||||
* ParticleEngine, so the majority path (no shaders) is completely unchanged.
|
||||
*
|
||||
* @author Cobblemon
|
||||
*/
|
||||
object SnowstormShaderBypassRenderer {
|
||||
|
||||
// The world's camera view matrix for the current frame, captured at the late in-level stage
|
||||
// where Fabric/NeoForge still have the camera transform applied. POST_LEVEL fires after that
|
||||
// state is unwound, but the bypass draw still needs it because SnowstormParticle.render emits
|
||||
// camera-relative vertices just like vanilla particles do.
|
||||
// The world's camera view matrix for the current frame, captured at the late stage within the
|
||||
// level pass where Fabric/NeoForge still have the camera transform applied. POST_LEVEL fires after
|
||||
// that state is unwound, but the bypass draw still needs it because SnowstormParticle.render emits
|
||||
// vertices relative to the camera just like vanilla particles do.
|
||||
private val modelViewMatrix = Matrix4f()
|
||||
private var captured = false
|
||||
|
||||
@@ -87,8 +87,8 @@ object SnowstormShaderBypassRenderer {
|
||||
val lightTexture = minecraft.gameRenderer.lightTexture()
|
||||
val tesselator = Tesselator.getInstance()
|
||||
|
||||
// Re-establish the world's camera view so the camera-relative quads built by render() land
|
||||
// in the right place; projection is still the world's 3D projection at this point in the frame.
|
||||
// Restore the world's camera view so the quads built relative to the camera by render()
|
||||
// land in the right place; projection is still the world's 3D projection at this point in the frame.
|
||||
val modelViewStack = RenderSystem.getModelViewStack()
|
||||
modelViewStack.pushMatrix()
|
||||
modelViewStack.set(modelViewMatrix)
|
||||
@@ -98,7 +98,7 @@ object SnowstormShaderBypassRenderer {
|
||||
lightTexture.turnOnLightLayer()
|
||||
|
||||
// Group by the particle's own render type (ParticleMaterials.ALPHA/ADD/BLEND/OPAQUE). Calling
|
||||
// its begin() sets the IDENTICAL GL state vanilla would use, so the look matches no-shaders.
|
||||
// its begin() sets the IDENTICAL GL state vanilla would use, so the look matches the no shader path.
|
||||
for ((renderType, particles) in visible.groupBy { it.particleTextureSheet }) {
|
||||
val buffer = renderType.begin(tesselator, minecraft.textureManager) ?: continue
|
||||
for (particle in particles) {
|
||||
|
||||
@@ -363,11 +363,11 @@ object SnowstormParticleReader {
|
||||
ExpressionParticleTinting()
|
||||
}
|
||||
val environmentLighting = componentsJson.has("minecraft:particle_appearance_lighting")
|
||||
// NOTE: per-particle dynamic-light levels are intentionally NOT read from the .particle.json.
|
||||
// Snowstorm (the Bedrock particle editor) rebuilds the components block on export and silently
|
||||
// drops any unknown component, so a custom light field here would be lost whenever a file is
|
||||
// re-saved in Snowstorm. Instead light levels live in a separate Cobblemon data file and are
|
||||
// merged onto the effect after load (see BedrockParticleOptionsRepository.loadLightConfig).
|
||||
// Light levels are intentionally not read from the .particle.json. Snowstorm (the Bedrock
|
||||
// particle editor) rebuilds the components block on export and silently drops unknown components,
|
||||
// so a custom light field here would be lost the next time a file is saved. Light levels instead
|
||||
// live in a separate Cobblemon data file, merged onto the effect after load (see
|
||||
// BedrockParticleOptionsRepository.loadLightConfig).
|
||||
val collision = collisionJson?.let {
|
||||
var collides = true
|
||||
val radius = it.get("collision_radius")?.asString?.asExpression() ?: run {
|
||||
|
||||
@@ -45,8 +45,8 @@ data class RenderEvent(
|
||||
* Fired after [LevelRenderer.renderLevel] has fully finished — on a shaderpack, this is after
|
||||
* Iris/OptiFine has composited the world to the main framebuffer and restored vanilla shaders.
|
||||
* Draws here are NOT intercepted by the shaderpack, so they render with vanilla shaders while
|
||||
* still depth-testing against the full (opaque + translucent) world depth. Used to render
|
||||
* Cobblemon's snowstorm particles with a vanilla-identical look under shaders.
|
||||
* still depth testing against the full (opaque + translucent) world depth. Used to render
|
||||
* Cobblemon's snowstorm particles so they look like vanilla under shaders.
|
||||
*/
|
||||
POST_LEVEL
|
||||
}
|
||||
|
||||
@@ -69,11 +69,11 @@ dependencies {
|
||||
localRuntime("org.antlr:antlr4-runtime:4.13.1")
|
||||
|
||||
// Dynamic lighting (DEV RUNTIME ONLY). Cobblemon compiles against the LambDynamicLights API
|
||||
// (modCompileOnly, mojmap — see common/build.gradle.kts); this provides the running mod in dev so
|
||||
// `runClient` actually exercises the move-particle dynamic light. Pinned to 4.1.6 to match that
|
||||
// API AND fabric-loader 0.17.2 (4.8.9 needs loader >=0.18.4). Loom does NOT surface a mod's JIJ'd
|
||||
// dependency mods to the dev run, so its four bundled deps are listed explicitly (from gegy +
|
||||
// Maven Central). Bump all five together if LDL is updated.
|
||||
// (modCompileOnly, mojmap; see common/build.gradle.kts); this provides the running mod in dev so
|
||||
// runClient actually exercises the dynamic lights. Pinned to 4.1.6 to match that API and fabric
|
||||
// loader 0.17.2 (4.8.9 needs loader 0.18.4 or newer). Loom does not surface a mod's bundled
|
||||
// dependency mods to the dev run, so its four are listed explicitly (from gegy + Maven Central).
|
||||
// Bump all five together if LambDynamicLights is updated.
|
||||
modLocalRuntime("maven.modrinth:lambdynamiclights:4.1.6+1.21.1")
|
||||
modLocalRuntime("dev.lambdaurora.lambdynamiclights:lambdynamiclights-api:4.1.6+1.21.1")
|
||||
modLocalRuntime("dev.lambdaurora:spruceui:6.2.1+1.21")
|
||||
|
||||
@@ -47,6 +47,7 @@ import net.fabricmc.fabric.api.client.rendering.v1.ColorProviderRegistry
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.EntityModelLayerRegistry
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents
|
||||
import net.fabricmc.fabric.api.resource.IdentifiableResourceReloadListener
|
||||
import net.fabricmc.fabric.api.resource.ResourceManagerHelper
|
||||
@@ -158,51 +159,29 @@ class CobblemonFabricClient: ClientModInitializer, CobblemonClientImplementation
|
||||
ClientPlayConnectionEvents.DISCONNECT.register { _, client -> client.player?.let { PlatformEvents.CLIENT_PLAYER_LOGOUT.post(ClientPlayerEvent.Logout(it)) } }
|
||||
ItemTooltipCallback.EVENT.register { stack, context, type, lines -> PlatformEvents.CLIENT_ITEM_TOOLTIP.post(ItemTooltipEvent(stack, context, type, lines)) }
|
||||
|
||||
WorldRenderEvents.AFTER_TRANSLUCENT.register { context -> PlatformEvents.RENDER.post(
|
||||
RenderEvent(
|
||||
stage = RenderEvent.Stage.TRANSLUCENT,
|
||||
levelRenderer = context.worldRenderer(),
|
||||
poseStack = context.matrixStack() ?: PoseStack(),
|
||||
modelViewMatrix = context.positionMatrix(),
|
||||
projectionMatrix = context.projectionMatrix(),
|
||||
tickCounter = context.tickCounter(),
|
||||
camera = context.camera()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Capture the final in-level camera transform before Fabric unwinds render state.
|
||||
WorldRenderEvents.LAST.register { context -> PlatformEvents.RENDER.post(
|
||||
RenderEvent(
|
||||
stage = RenderEvent.Stage.LAST,
|
||||
levelRenderer = context.worldRenderer(),
|
||||
poseStack = context.matrixStack() ?: PoseStack(),
|
||||
modelViewMatrix = context.positionMatrix(),
|
||||
projectionMatrix = context.projectionMatrix(),
|
||||
tickCounter = context.tickCounter(),
|
||||
camera = context.camera()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Fires after the level (and any shaderpack composite) has finished — the only point where a
|
||||
// draw is NOT intercepted by Iris/OptiFine. Used by SnowstormShaderBypassRenderer.
|
||||
WorldRenderEvents.END.register { context -> PlatformEvents.RENDER.post(
|
||||
RenderEvent(
|
||||
stage = RenderEvent.Stage.POST_LEVEL,
|
||||
levelRenderer = context.worldRenderer(),
|
||||
poseStack = context.matrixStack() ?: PoseStack(),
|
||||
modelViewMatrix = context.positionMatrix(),
|
||||
projectionMatrix = context.projectionMatrix(),
|
||||
tickCounter = context.tickCounter(),
|
||||
camera = context.camera()
|
||||
)
|
||||
)
|
||||
}
|
||||
WorldRenderEvents.AFTER_TRANSLUCENT.register { context -> postRenderStage(RenderEvent.Stage.TRANSLUCENT, context) }
|
||||
// LAST captures the camera transform while it is still applied; POST_LEVEL fires after the level
|
||||
// (and any shaderpack composite) finishes, the only point a draw is NOT intercepted by Iris/OptiFine.
|
||||
WorldRenderEvents.LAST.register { context -> postRenderStage(RenderEvent.Stage.LAST, context) }
|
||||
WorldRenderEvents.END.register { context -> postRenderStage(RenderEvent.Stage.POST_LEVEL, context) }
|
||||
|
||||
CobblemonModelPredicateRegistry.registerPredicates()
|
||||
}
|
||||
|
||||
private fun postRenderStage(stage: RenderEvent.Stage, context: WorldRenderContext) {
|
||||
PlatformEvents.RENDER.post(
|
||||
RenderEvent(
|
||||
stage = stage,
|
||||
levelRenderer = context.worldRenderer(),
|
||||
poseStack = context.matrixStack() ?: PoseStack(),
|
||||
modelViewMatrix = context.positionMatrix(),
|
||||
projectionMatrix = context.projectionMatrix(),
|
||||
tickCounter = context.tickCounter(),
|
||||
camera = context.camera()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun registerLayer(modelLayer: ModelLayerLocation, supplier: Supplier<LayerDefinition>) {
|
||||
EntityModelLayerRegistry.registerModelLayer(modelLayer) { supplier.get() }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user