Compare commits

...
18 changed files with 923 additions and 10 deletions

No files matched your search

@@ -9,8 +9,10 @@
package com.cobblemon.mod.common.compat.lambdynamiclights;
import com.cobblemon.mod.common.Cobblemon;
import com.cobblemon.mod.common.client.particle.MoveDynamicLights;
import dev.lambdaurora.lambdynlights.api.DynamicLightsContext;
import dev.lambdaurora.lambdynlights.api.DynamicLightsInitializer;
import dev.lambdaurora.lambdynlights.api.behavior.DynamicLightBehaviorManager;
import dev.lambdaurora.lambdynlights.api.entity.luminance.EntityLuminance;
import dev.lambdaurora.lambdynlights.api.item.ItemLightSourceManager;
import net.minecraft.resources.ResourceLocation;
@@ -33,6 +35,11 @@ public class LambDynamicLightsInitializer implements DynamicLightsInitializer {
context.entityLightSourceManager().onRegisterEvent().register(ctx -> {
ctx.register(EntityType.PLAYER, PlayerLuminance.INSTANCE);
});
// Attach a monochrome dynamic light to glowing particle effects (moves). The behavior reports
// isRemoved() when its effect despawns, so the manager cleans it up automatically.
final DynamicLightBehaviorManager behaviorManager = context.dynamicLightBehaviorManager();
MoveDynamicLights.setSink(storm -> behaviorManager.add(new SnowstormDynamicLight(storm)));
}
@SuppressWarnings({"removal", "UnstableApiUsage"})
@@ -0,0 +1,108 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.compat.lambdynamiclights;
import com.cobblemon.mod.common.client.particle.ParticleStorm;
import dev.lambdaurora.lambdynlights.api.behavior.DynamicLightBehavior;
import net.minecraft.core.BlockPos;
import net.minecraft.world.phys.Vec3;
/**
* A (monochrome) dynamic light attached to a glowing Cobblemon particle effect, so the area around a
* move lights up like any other dynamic light source. Brightness scales with how many particles the
* effect currently has alive ({@link ParticleStorm#glowIntensity()}), and the light follows the live
* particles' center (so a beam/AoE lights where the particles actually are, not the emitter origin on
* the Pokémon). It removes itself when the effect despawns.
*
* <p>Vanilla block light is monochrome, so this changes the light <em>level</em> only — the move's
* color is added separately by the screen-space colored pass ({@code MoveGlowRenderer}) when Iris is
* also present.</p>
*
* <p>Only instantiated by {@link LambDynamicLightsInitializer} (the LambDynamicLights entrypoint), so
* it is never loaded unless LambDynamicLights is installed.</p>
*/
public final class SnowstormDynamicLight implements DynamicLightBehavior {
/** Peak block-light level (0..15) at full intensity. Keep modest — this is real light. */
private static final int MAX_LUMINANCE = 8;
private final ParticleStorm storm;
// Cached each hasChanged() so lightAtPos()/getBoundingBox() don't recompute the centroid per block.
private double centerX;
private double centerY;
private double centerZ;
private double luminance;
private int prevX = Integer.MIN_VALUE;
private int prevY = Integer.MIN_VALUE;
private int prevZ = Integer.MIN_VALUE;
private double prevLuminance = -1.0;
public SnowstormDynamicLight(ParticleStorm storm) {
this.storm = storm;
this.centerX = storm.getX();
this.centerY = storm.getY();
this.centerZ = storm.getZ();
this.luminance = storm.glowIntensity() * MAX_LUMINANCE;
}
/** Refreshes the cached center (live-particle centroid, fallback to emitter origin) and luminance. */
private void updateState() {
Vec3 centroid = storm.liveParticleCentroid();
if (centroid != null) {
centerX = centroid.x;
centerY = centroid.y;
centerZ = centroid.z;
} else {
centerX = storm.getX();
centerY = storm.getY();
centerZ = storm.getZ();
}
luminance = storm.glowIntensity() * MAX_LUMINANCE;
}
@Override
public double lightAtPos(BlockPos pos, double falloffRatio) {
if (luminance <= 0.0) return 0.0;
double dx = pos.getX() + 0.5 - centerX;
double dy = pos.getY() + 0.5 - centerY;
double dz = pos.getZ() + 0.5 - centerZ;
double dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
// Standard block-light falloff: drops ~1 level per block from the center.
return Math.max(0.0, luminance - dist);
}
@Override
public DynamicLightBehavior.BoundingBox getBoundingBox() {
int r = Math.max(1, Math.min(MAX_LUMINANCE, (int) Math.ceil(luminance)));
int cx = (int) Math.floor(centerX);
int cy = (int) Math.floor(centerY);
int cz = (int) Math.floor(centerZ);
return new DynamicLightBehavior.BoundingBox(cx - r, cy - r, cz - r, cx + r, cy + r, cz + r);
}
@Override
public boolean hasChanged() {
updateState();
int cx = (int) Math.floor(centerX);
int cy = (int) Math.floor(centerY);
int cz = (int) Math.floor(centerZ);
boolean changed = cx != prevX || cy != prevY || cz != prevZ || Math.abs(luminance - prevLuminance) >= 1.0;
if (changed) {
prevX = cx;
prevY = cy;
prevZ = cz;
prevLuminance = luminance;
}
return changed;
}
@Override
public boolean isRemoved() {
return storm.getDespawned();
}
}
@@ -8,6 +8,8 @@
package com.cobblemon.mod.common.api.snowstorm
import com.cobblemon.mod.common.client.render.ShaderCompat
import com.cobblemon.mod.common.client.render.ShaderCompat.ShaderParticleProfile
import com.mojang.blaze3d.platform.GlStateManager
import com.mojang.blaze3d.systems.RenderSystem
import com.mojang.blaze3d.vertex.BufferBuilder
@@ -19,15 +21,28 @@ import net.minecraft.client.renderer.GameRenderer
import net.minecraft.client.renderer.texture.TextureAtlas
import net.minecraft.client.renderer.texture.TextureManager
/**
* The four particle render types for snowstorm particles.
*
* Under an active shaderpack the blend/depth state of the translucent materials is driven by a
* per-pack [ShaderParticleProfile] (see [ShaderCompat]) so each pack can be tuned independently —
* Iris routes our particles through each pack's translucent particle program, and no single blend
* works everywhere. With no shaderpack the profile is [ShaderCompat.VANILLA_PROFILE], reproducing
* stock blending exactly, so the no-shader majority path is unchanged.
*
* [begin] refreshes the cached profile once per particle batch; the per-particle vertex path reads it
* via [premultipliesUnderShaders] (see [com.cobblemon.mod.common.client.render.SnowstormParticle.render]).
*/
object ParticleMaterials {
val ALPHA = object : ParticleRenderType {
override fun begin(tessellator: Tesselator, textureManager: TextureManager): BufferBuilder {
val profile = ShaderCompat.refreshParticleProfile()
RenderSystem.enableBlend()
RenderSystem.enableCull()
RenderSystem.depthMask(true)
RenderSystem.setShader { GameRenderer.getParticleShader() }
RenderSystem.setShaderTexture(0, TextureAtlas.LOCATION_PARTICLES)
RenderSystem.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA)
applyTranslucentBlend(profile)
return tessellator.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.PARTICLE)
}
@@ -36,9 +51,13 @@ object ParticleMaterials {
val ADD = object : ParticleRenderType {
override fun begin(tessellator: Tesselator, textureManager: TextureManager): BufferBuilder {
val profile = ShaderCompat.refreshParticleProfile()
RenderSystem.enableBlend()
RenderSystem.disableCull()
RenderSystem.depthMask(true)
// Additive glow generally shouldn't write depth under shaders or the pack treats it as
// solid and dims it — but some packs need the depth write or the particles vanish, so the
// choice is per-pack (profile.addWritesDepth). Vanilla profile = true (unchanged look).
RenderSystem.depthMask(profile.addWritesDepth)
RenderSystem.setShader { GameRenderer.getParticleShader() }
RenderSystem.setShaderTexture(0, TextureAtlas.LOCATION_PARTICLES)
RenderSystem.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE)
@@ -50,12 +69,13 @@ object ParticleMaterials {
val BLEND = object : ParticleRenderType {
override fun begin(tessellator: Tesselator, textureManager: TextureManager): BufferBuilder {
val profile = ShaderCompat.refreshParticleProfile()
RenderSystem.enableBlend()
RenderSystem.disableCull()
RenderSystem.depthMask(true)
RenderSystem.depthMask(profile.blendWritesDepth)
RenderSystem.setShader { GameRenderer.getParticleShader() }
RenderSystem.setShaderTexture(0, TextureAtlas.LOCATION_PARTICLES)
RenderSystem.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA)
applyTranslucentBlend(profile)
return tessellator.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.PARTICLE)
}
@@ -64,6 +84,7 @@ object ParticleMaterials {
val OPAQUE = object : ParticleRenderType {
override fun begin(tessellator: Tesselator, textureManager: TextureManager): BufferBuilder {
ShaderCompat.refreshParticleProfile()
RenderSystem.enableBlend()
RenderSystem.enableCull()
RenderSystem.depthMask(true)
@@ -75,4 +96,29 @@ object ParticleMaterials {
override fun toString() = "OPAQUE"
}
}
/**
* True when [material]'s vertex color must be premultiplied by its alpha before being submitted.
* Driven by the cached per-pack profile, so the vertex path
* ([com.cobblemon.mod.common.client.render.SnowstormParticle.render]) and the blend func set in
* [applyTranslucentBlend] always agree. Only the alpha-composited materials (ALPHA/BLEND) are
* premultiplied; ADD is additive and OPAQUE is not blended. With no shaderpack the profile is
* VANILLA (premultiplyAlpha = false), so this returns false and the look is stock.
*/
fun premultipliesUnderShaders(material: ParticleMaterial): Boolean =
ShaderCompat.particleProfile().premultiplyAlpha &&
(material == ParticleMaterial.ALPHA || material == ParticleMaterial.BLEND)
/**
* Premultiplied-alpha blend (source already carries rgb*a, so source factor is ONE) when the
* active pack's profile asks for it; vanilla straight-alpha blend otherwise. Because
* `ONE*(rgb*a) + (1-a)*dst == a*rgb + (1-a)*dst`, the vanilla profile is byte-identical to stock.
*/
private fun applyTranslucentBlend(profile: ShaderParticleProfile) {
if (profile.premultiplyAlpha) {
RenderSystem.blendFunc(GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA)
} else {
RenderSystem.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA)
}
}
}
@@ -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.MoveGlowRenderer
import com.cobblemon.mod.common.client.render.block.*
import com.cobblemon.mod.common.client.render.boat.CobblemonBoatRenderer
import com.cobblemon.mod.common.client.render.color.AprijuiceItemColorProvider
@@ -140,6 +141,9 @@ object CobblemonClient {
PlatformEvents.CLIENT_PLAYER_LOGIN.subscribe { onLogin() }
PlatformEvents.CLIENT_PLAYER_LOGOUT.subscribe { onLogout() }
// Colored screen-space "dynamic light" for glowing moves (only acts when shaders + LambDynamicLights present).
MoveGlowRenderer.register()
this.registerBlockEntityRenderers()
registerBlockRenderTypes()
registerColors()
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.particle
/**
* API-free bridge that lets the optional LambDynamicLights integration attach a dynamic light to
* glowing particle effects without the common particle code referencing the LambDynamicLights API.
*
* When LambDynamicLights is installed, its entrypoint
* ([com.cobblemon.mod.common.compat.lambdynamiclights.LambDynamicLightsInitializer]) sets [sink] to a
* handler that registers a `DynamicLightBehavior` for the storm. When it isn't installed, [sink] stays
* null and [ParticleStorm.spawn]'s call is a no-op — so there is no hard dependency.
*/
object MoveDynamicLights {
@JvmStatic
var sink: GlowingStormLightSink? = null
}
/** Receives glow-capable [ParticleStorm]s so a (monochrome) dynamic light can be attached to them. */
fun interface GlowingStormLightSink {
fun register(storm: ParticleStorm)
}
@@ -91,6 +91,9 @@ class ParticleStorm(
runtime.environment.setSimpleVariable("entity_scale", DoubleValue((entity as? PokemonEntity)?.scale ?: 1.0))
}
Minecraft.getInstance().particleEngine.add(this)
activeStorms.add(this)
// Glow-capable effects get a (monochrome) dynamic light when LambDynamicLights is installed.
if (emitsLight) MoveDynamicLights.sink?.register(this)
}
fun addAttackingFunctions(destinationPosSupplier: (() -> Vec3)) {
@@ -150,6 +153,15 @@ class ParticleStorm(
companion object {
var contextStorm: ParticleStorm? = null
/**
* Every live particle effect, so [com.cobblemon.mod.common.client.render.MoveGlowRenderer] can
* cast a colored glow around the glowing ones. Insertion-ordered; storms use identity equality.
*/
val activeStorms: MutableSet<ParticleStorm> = LinkedHashSet()
/** Live particle count at which a glowing effect reaches full glow intensity. */
const val GLOW_FULL_PARTICLE_COUNT = 20f
fun createAtPosition(world: ClientLevel, effect: BedrockParticleOptions, position: Vec3): ParticleStorm {
val wrapper = MatrixWrapper()
val matrix = PoseStack()
@@ -218,6 +230,7 @@ class ParticleStorm(
override fun remove() {
super.remove()
activeStorms.remove(this)
if (!despawned) {
effect.emitter.expirationEvents.forEach { it.trigger(this, null) }
despawned = true
@@ -374,4 +387,50 @@ class ParticleStorm(
fun setInvisible() {
particles.forEach { particle -> particle.invisible = true }
}
/** True when this effect is glow-capable (self-lit rather than environment-lit), regardless of
* current particle count — used to decide whether to attach a dynamic light at spawn. */
val emitsLight: Boolean
get() = !effect.particle.environmentLighting
/** True when this effect is currently glowing (glow-capable AND actively emitting particles). */
val isGlowing: Boolean
get() = emitsLight && !stopped && particles.isNotEmpty()
/**
* Representative color for the move glow. Uses the color a live particle actually rendered with
* (the tint is evaluated safely there, with particle-age context); the explicit particle color is
* the fallback, then white. We must NOT call [effect].particle.tinting.getTint here — outside a
* particle render the runtime has no age context and a gradient tint throws.
*/
fun glowColor(): Vector4f =
particles.firstOrNull()?.lastColor ?: getParticleColor() ?: Vector4f(1f, 1f, 1f, 1f)
/** 0..1 glow strength from how many particles are currently alive (more particles -> brighter). */
fun glowIntensity(): Float = (particles.size.toFloat() / GLOW_FULL_PARTICLE_COUNT).coerceIn(0f, 1f)
/**
* Average position of the live (visible) particles, or null if there are none. Used to position
* the monochrome dynamic light where the particles actually are (following a beam/AoE) instead of
* pinning it to the emitter origin on the Pokémon.
*/
/**
* For targeted (beam/projectile) effects, the light segment from the emitter to the target, so the
* colored light can illuminate the whole beam path rather than a single point. Null for effects
* with no target (bursts/AoE), which fall back to per-particle sampling.
*/
fun beamEndpoints(): Pair<Vec3, Vec3>? {
val end = targetPos?.invoke() ?: return null
return Vec3(getX(), getY(), getZ()) to end
}
fun liveParticleCentroid(): Vec3? {
var n = 0
var sx = 0.0; var sy = 0.0; var sz = 0.0
for (p in particles) {
if (p.invisible) continue
sx += p.getX(); sy += p.getY(); sz += p.getZ(); n++
}
return if (n == 0) null else Vec3(sx / n, sy / n, sz / n)
}
}
@@ -0,0 +1,199 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.render
import com.cobblemon.mod.common.client.particle.MoveDynamicLights
import com.cobblemon.mod.common.client.particle.ParticleStorm
import com.cobblemon.mod.common.client.render.shader.CobblemonShaders
import com.cobblemon.mod.common.platform.events.PlatformEvents
import com.cobblemon.mod.common.platform.events.RenderEvent
import com.mojang.blaze3d.pipeline.TextureTarget
import com.mojang.blaze3d.platform.GlStateManager
import com.mojang.blaze3d.systems.RenderSystem
import com.mojang.blaze3d.vertex.BufferUploader
import com.mojang.blaze3d.vertex.DefaultVertexFormat
import com.mojang.blaze3d.vertex.Tesselator
import com.mojang.blaze3d.vertex.VertexFormat
import net.minecraft.client.Minecraft
import net.minecraft.world.phys.Vec3
import org.joml.Matrix4f
import org.joml.Vector3f
import org.lwjgl.opengl.GL14
/**
* Tier 2 of the move lighting: a **colored** screen-space "dynamic light" for glowing moves, active
* only when **both LambDynamicLights and a shaderpack (Iris/OptiFine)** are present (Lamb alone gives
* the monochrome light level via
* [com.cobblemon.mod.common.compat.lambdynamiclights.SnowstormDynamicLight]; this adds the move color).
*
* Colored world illumination has no mod API on any pack, so this is a deferred screen-space effect:
* after the world (and shaderpack) has composited, each glowing [ParticleStorm] contributes light(s)
* that read the scene depth, reconstruct each surface's position, and tint it with the move's color
* and distance falloff. Beams light along the whole emitter->target segment; bursts sample several
* real particles. The *surfaces* near the move are tinted (no visible shape), identically on every
* pack because it runs post-composite on vanilla GL.
*
* ## Non-additive accumulation
* All the lights are first drawn into an off-screen [glowTarget] with a **MAX** blend, so overlapping
* lights/effects keep the **brightest** contribution rather than summing — no additive blow-out to
* white when many effects (or a dense burst's samples) overlap. That buffer is then composited onto
* the scene **once** (additive), bounded by a single light's [PER_LIGHT_INTENSITY].
*/
object MoveGlowRenderer {
/** How many particles to sample per burst emitter as light points (spread evenly). */
private const val SAMPLES_PER_EMITTER = 10
/** Falloff radius of each colored light, in blocks. */
private const val LIGHT_RADIUS = 10.0f
/** Strength of a single light. Because accumulation is MAX (not additive), this is the actual peak
* glow a pixel can receive no matter how many lights overlap. */
private const val PER_LIGHT_INTENSITY = 0.2f
/** Safety cap on total light passes per frame (each is a fullscreen pass into the glow buffer). */
private const val GLOBAL_MAX_LIGHTS = 60
// World matrices for the frame, captured at TRANSLUCENT (known-good) for the post-composite pass.
private val modelView = Matrix4f()
private val inverseProjection = Matrix4f()
private var captured = false
// Off-screen buffer the lights MAX-accumulate into before being composited onto the scene once.
private var glowTarget: TextureTarget? = null
fun register() {
PlatformEvents.RENDER.subscribe { event ->
when (event.stage) {
RenderEvent.Stage.TRANSLUCENT -> {
modelView.set(event.modelViewMatrix)
inverseProjection.set(event.projectionMatrix).invert()
captured = true
}
RenderEvent.Stage.POST_LEVEL -> render(event)
}
}
}
private fun render(event: RenderEvent) {
if (!captured) return
// Colored layer is gated on BOTH a shaderpack AND LambDynamicLights being present.
if (!ShaderCompat.shadersActive()) return
if (MoveDynamicLights.sink == null) return
if (!CobblemonShaders.isMoveGlowLightReady()) return
val storms = ParticleStorm.activeStorms.filter { it.isGlowing }
if (storms.isEmpty()) return
val minecraft = Minecraft.getInstance()
val main = minecraft.mainRenderTarget
val target = ensureGlowTarget(main.width, main.height)
val cam = event.camera.position
val lightShader = CobblemonShaders.MOVE_GLOW_LIGHT
// --- Pass 1: accumulate all move lights into the off-screen buffer with MAX (no stacking). ---
target.setClearColor(0f, 0f, 0f, 0f)
target.clear(Minecraft.ON_OSX)
target.bindWrite(true)
RenderSystem.disableDepthTest()
RenderSystem.depthMask(false)
RenderSystem.disableCull()
RenderSystem.enableBlend()
GlStateManager._blendEquation(GL14.GL_MAX) // result = max(src, dst): brightest light wins, no sum
RenderSystem.setShader { lightShader }
lightShader.safeGetUniform("InverseProjMat").set(inverseProjection)
lightShader.setSampler("DepthSampler", main.depthTextureId) // read the real scene depth
lightShader.safeGetUniform("LightRadius")?.set(LIGHT_RADIUS)
var lightsDrawn = 0
emitters@ for (storm in storms) {
try {
val beam = storm.beamEndpoints()
if (beam != null) {
val intensity = PER_LIGHT_INTENSITY * storm.glowIntensity()
if (intensity > 0f) {
val a = viewSpace(beam.first.x, beam.first.y, beam.first.z, cam)
val b = viewSpace(beam.second.x, beam.second.y, beam.second.z, cam)
val colour = storm.glowColor()
lightShader.safeGetUniform("LightViewPosA")?.set(a.x, a.y, a.z)
lightShader.safeGetUniform("LightViewPosB")?.set(b.x, b.y, b.z)
lightShader.safeGetUniform("LightColor")?.set(colour.x, colour.y, colour.z)
lightShader.safeGetUniform("Intensity")?.set(intensity)
drawFullscreenQuad()
if (++lightsDrawn >= GLOBAL_MAX_LIGHTS) break@emitters
}
} else {
val particles = storm.particles
if (particles.isEmpty()) continue
val samples = minOf(SAMPLES_PER_EMITTER, particles.size)
val step = (particles.size / samples).coerceAtLeast(1)
for (s in 0 until samples) {
val particle = particles.getOrNull(s * step) ?: continue
if (particle.invisible) continue
val colour = particle.lastColor
val alpha = colour.w
if (alpha <= 0.01f) continue
val p = viewSpace(particle.getX(), particle.getY(), particle.getZ(), cam)
lightShader.safeGetUniform("LightViewPosA")?.set(p.x, p.y, p.z)
lightShader.safeGetUniform("LightViewPosB")?.set(p.x, p.y, p.z)
lightShader.safeGetUniform("LightColor")?.set(colour.x, colour.y, colour.z)
lightShader.safeGetUniform("Intensity")?.set(PER_LIGHT_INTENSITY * alpha)
drawFullscreenQuad()
if (++lightsDrawn >= GLOBAL_MAX_LIGHTS) break@emitters
}
}
} catch (ignored: Exception) {
// A single misbehaving effect must never break the world-render callback.
}
}
// --- Pass 2: composite the accumulated glow onto the scene once (additive). ---
GlStateManager._blendEquation(GL14.GL_FUNC_ADD)
main.bindWrite(true)
RenderSystem.blendFunc(GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ONE)
val compositeShader = CobblemonShaders.MOVE_GLOW_COMPOSITE
RenderSystem.setShader { compositeShader }
compositeShader.setSampler("GlowSampler", target.colorTextureId)
drawFullscreenQuad()
// Restore neutral GL state.
RenderSystem.depthMask(true)
RenderSystem.enableDepthTest()
RenderSystem.enableCull()
RenderSystem.disableBlend()
RenderSystem.defaultBlendFunc()
}
private fun ensureGlowTarget(width: Int, height: Int): TextureTarget {
var target = glowTarget
if (target == null) {
target = TextureTarget(width, height, false, Minecraft.ON_OSX)
glowTarget = target
} else if (target.width != width || target.height != height) {
target.resize(width, height, Minecraft.ON_OSX)
}
return target
}
/** World position -> view space (camera-relative, then the world model-view), for the shader. */
private fun viewSpace(wx: Double, wy: Double, wz: Double, cam: Vec3): Vector3f {
val v = Vector3f((wx - cam.x).toFloat(), (wy - cam.y).toFloat(), (wz - cam.z).toFloat())
modelView.transformPosition(v)
return v
}
/** A quad covering clip space (-1..1); the vertex shader passes it straight through. */
private fun drawFullscreenQuad() {
val buffer = Tesselator.getInstance().begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION)
buffer.addVertex(-1f, -1f, 0f)
buffer.addVertex(1f, -1f, 0f)
buffer.addVertex(1f, 1f, 0f)
buffer.addVertex(-1f, 1f, 0f)
val mesh = buffer.build() ?: return
BufferUploader.drawWithShader(mesh)
}
}
@@ -0,0 +1,208 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.render
import com.cobblemon.mod.common.Cobblemon.LOGGER
import java.lang.reflect.Method
/**
* Detects whether a shaderpack is active and, if so, *which* one, then exposes a per-pack
* [ShaderParticleProfile] describing how Cobblemon's snowstorm particles should be blended so they
* look right under that specific pack.
*
* ## Why per-pack?
* Iris/OptiFine route our custom translucent particles through the shaderpack's own
* `gbuffers_particles_translucent` program, and every pack composites that program differently.
* A single pack-agnostic treatment is impossible: premultiplied-alpha fixes Complementary's
* fade-to-black but makes Photon's particles vanish and leaves Bliss darkening. So instead we
* identify the pack and apply a tailored blend/depth profile. **Tune the table in [profileFor].**
*
* Everything here is reflective and fully guarded: Iris/OptiFine are optional and may be absent at
* runtime, so we never hard-reference their classes. Any reflective failure degrades to "no shaders"
* / the vanilla profile and never throws. Works identically on Fabric and NeoForge.
*/
object ShaderCompat {
/** Identifies the active shaderpack family (by name substring). [OTHER] = active but unrecognised. */
enum class ShaderPack { NONE, COMPLEMENTARY, BLISS, PHOTON, BSL, OTHER }
/**
* How to blend the snowstorm particle materials under a given pack. These are the only knobs we
* actually control in-pipeline (vertex color + GL blend/depth state); the pack's fragment program
* is out of reach.
*
* @param premultiplyAlpha ALPHA & BLEND: premultiply the vertex color by its alpha and blend with
* `(ONE, ONE_MINUS_SRC_ALPHA)`. Cures "fades to black" on packs that composite premultiplied,
* but can read too opaque on packs that already premultiply internally. (No-shader path: false.)
* @param addWritesDepth ADD material `depthMask`. `false` stops the pack treating additive glow
* as solid geometry (dimming via fog/deferred lighting); some packs need `true` or particles
* disappear. (No-shader path: true.)
* @param blendWritesDepth BLEND material `depthMask`, same trade-off as [addWritesDepth].
*/
data class ShaderParticleProfile(
val premultiplyAlpha: Boolean,
val addWritesDepth: Boolean,
val blendWritesDepth: Boolean,
/**
* Clamp emitted vertex alpha to at least this (0 = no clamp). Defeats packs that `discard`
* low-alpha particle fragments — Photon's shared translucent program does
* `if (fragment_color.a < 0.1) discard;`, so faded/soft particles vanish. ~0.12–0.2 keeps the
* particle core above the floor. (Vanilla/most packs: 0.) Trade-off: particles can't fade
* fully to transparent under that pack.
*/
val minAlpha: Float = 0f
)
/** The vanilla, no-shaderpack profile — must reproduce stock Minecraft particle blending exactly. */
val VANILLA_PROFILE = ShaderParticleProfile(premultiplyAlpha = false, addWritesDepth = true, blendWritesDepth = true)
// --- reflective handles, resolved once ------------------------------------------------------
// Iris: net.irisshaders.iris.api.v0.IrisApi.getInstance().isShaderPackInUse()
private val irisInstance: Any?
private val irisIsShaderPackInUse: Method?
// Iris: net.irisshaders.iris.Iris.getCurrentPackName() (static) -> String, "(off)" when none.
private val irisGetCurrentPackName: Method?
// OptiFine: net.optifine.Config.isShaders() (static) + net.optifine.shaders.Shaders.getShaderPackName()
private val optifineIsShaders: Method?
private val optifineGetShaderPackName: Method?
init {
var irisInst: Any? = null
var irisInUse: Method? = null
var irisPackName: Method? = null
try {
val irisApi = Class.forName("net.irisshaders.iris.api.v0.IrisApi")
irisInst = irisApi.getMethod("getInstance").invoke(null)
irisInUse = irisApi.getMethod("isShaderPackInUse")
} catch (ignored: Throwable) { irisInst = null; irisInUse = null }
try {
irisPackName = Class.forName("net.irisshaders.iris.Iris").getMethod("getCurrentPackName")
} catch (ignored: Throwable) { irisPackName = null }
irisInstance = irisInst
irisIsShaderPackInUse = irisInUse
irisGetCurrentPackName = irisPackName
var ofIsShaders: Method? = null
var ofPackName: Method? = null
try {
ofIsShaders = Class.forName("net.optifine.Config").getMethod("isShaders")
} catch (ignored: Throwable) { ofIsShaders = null }
try {
ofPackName = Class.forName("net.optifine.shaders.Shaders").getMethod("getShaderPackName")
} catch (ignored: Throwable) { ofPackName = null }
optifineIsShaders = ofIsShaders
optifineGetShaderPackName = ofPackName
}
// --- detection ------------------------------------------------------------------------------
/** `true` when Iris or OptiFine reports an active (compiled and enabled) shaderpack. Never throws. */
fun shadersActive(): Boolean {
val inst = irisInstance
val inUse = irisIsShaderPackInUse
if (inst != null && inUse != null) {
try { if (inUse.invoke(inst) == true) return true } catch (ignored: Throwable) {}
}
val of = optifineIsShaders
if (of != null) {
try { if (of.invoke(null) == true) return true } catch (ignored: Throwable) {}
}
return false
}
/** Active shaderpack display name (e.g. "ComplementaryUnbound_r5.3.zip"), or null if none/unknown. */
fun activePackName(): String? {
if (!shadersActive()) return null
irisGetCurrentPackName?.let { m ->
try {
val name = m.invoke(null) as? String
if (!name.isNullOrBlank() && !name.equals("(off)", ignoreCase = true) && !name.equals("(internal)", ignoreCase = true)) {
return name
}
} catch (ignored: Throwable) {}
}
optifineGetShaderPackName?.let { m ->
try {
val name = m.invoke(null) as? String
if (!name.isNullOrBlank() && !name.equals("OFF", ignoreCase = true) && !name.equals("(internal)", ignoreCase = true)) {
return name
}
} catch (ignored: Throwable) {}
}
return null
}
/** Classifies the active pack into a [ShaderPack] family by name substring. */
fun activePack(): ShaderPack {
val name = activePackName()?.lowercase() ?: return if (shadersActive()) ShaderPack.OTHER else ShaderPack.NONE
return when {
"complementary" in name -> ShaderPack.COMPLEMENTARY
"bliss" in name -> ShaderPack.BLISS
"photon" in name -> ShaderPack.PHOTON
"bsl" in name -> ShaderPack.BSL
else -> ShaderPack.OTHER
}
}
// --- per-pack profile -----------------------------------------------------------------------
/**
* THE TUNING TABLE. Maps each recognised pack to its particle blend profile. These are starting
* points based on observed symptoms — adjust freely while exploring; only the shaders-active path
* is affected (NONE always returns [VANILLA_PROFILE], so non-shader users are never touched).
*/
private fun profileFor(pack: ShaderPack): ShaderParticleProfile = when (pack) {
ShaderPack.NONE -> VANILLA_PROFILE
// Premultiplied fixes the fade-to-black; trade-off is it can look a touch too opaque.
ShaderPack.COMPLEMENTARY -> ShaderParticleProfile(premultiplyAlpha = true, addWritesDepth = false, blendWritesDepth = false)
// Bliss force-overrides blend to SRC_ALPHA (blend.gbuffers_textured), so premultiplied color
// gets multiplied by alpha twice -> the darkening. Straight alpha fixes it. Brightness/glow is
// driven by the lightmap (Bliss relights particles as (Indirect+Direct)*Albedo): glow particles
// are emitted full-bright via environmentLighting=false and stay bright; non-glow ones use world
// light and read as environment-lit, exactly as intended.
ShaderPack.BLISS -> ShaderParticleProfile(premultiplyAlpha = false, addWritesDepth = false, blendWritesDepth = false)
// Photon (MC <=1.21.1) disables its dedicated particle programs and routes particles through a
// shared translucent program that (a) discards fragments with alpha < 0.1, (b) force-overrides
// blend to straight alpha-over, and (c) relights RGB with diffuse-lighting * alpha (NoL needs a
// normal we can't supply). Straight alpha + lift alpha above the 0.1 discard floor; glow comes
// from the full-bright lightmap of environmentLighting=false particles. NOTE: true additive
// glow can't be reproduced here (Photon's relight*alpha + forced alpha-over) — bypass needed.
ShaderPack.PHOTON -> ShaderParticleProfile(
premultiplyAlpha = false,
addWritesDepth = true,
blendWritesDepth = true,
minAlpha = 0.15f
)
ShaderPack.BSL -> ShaderParticleProfile(premultiplyAlpha = true, addWritesDepth = false, blendWritesDepth = false)
ShaderPack.OTHER -> ShaderParticleProfile(premultiplyAlpha = true, addWritesDepth = false, blendWritesDepth = false)
}
// Cached so per-particle reads (premultipliesUnderShaders) are free; refreshed once per particle
// batch from ParticleRenderType.begin() (a few times per frame), where re-querying is negligible.
@Volatile
private var cachedProfile: ShaderParticleProfile = VANILLA_PROFILE
private var lastLoggedPack: ShaderPack? = null
/** Recomputes and caches the active profile. Call once per particle batch (from begin()). */
fun refreshParticleProfile(): ShaderParticleProfile {
val pack = activePack()
if (pack != lastLoggedPack) {
lastLoggedPack = pack
LOGGER.info("Cobblemon snowstorm particle shader profile -> $pack (${activePackName() ?: "no pack"})")
}
val profile = profileFor(pack)
cachedProfile = profile
return profile
}
/** The profile from the most recent [refreshParticleProfile]; cheap, no reflection. */
fun particleProfile(): ShaderParticleProfile = cachedProfile
}
@@ -38,6 +38,7 @@ import org.joml.AxisAngle4d
import org.joml.Quaterniond
import org.joml.Vector3d
import org.joml.Vector3f
import org.joml.Vector4f
import kotlin.random.Random
class SnowstormParticle(
@@ -64,6 +65,10 @@ class SnowstormParticle(
var texture = storm.effect.particle.texture
/** The color this particle last rendered with (tint evaluated safely in [render] with the proper
* runtime context). Sampled by [ParticleStorm.glowColor] for the move's dynamic-light color. */
var lastColor: Vector4f = Vector4f(1f, 1f, 1f, 1f)
var localX = x - storm.getX()
var localY = y - storm.getY()
var localZ = z - storm.getZ()
@@ -193,6 +198,19 @@ class SnowstormParticle(
val uvs = storm.effect.particle.uvMode.get(runtime, age / 20.0, lifetime / 20.0, uvDetails)
val colour = storm.getParticleColor() ?: storm.effect.particle.tinting.getTint(runtime)
lastColor = colour
// Per-pack shader treatment (see ShaderCompat.ShaderParticleProfile). With no shaderpack the
// profile is VANILLA, so every branch below is a no-op and the look is stock.
val profile = ShaderCompat.particleProfile()
// minAlpha lifts alpha above a pack's discard floor (e.g. Photon discards alpha < 0.1) so
// faded/soft particles aren't culled. 0 for vanilla/most packs.
val alpha = maxOf(colour.w, profile.minAlpha)
// Premultiplied-alpha materials need the color premultiplied by alpha to match the ONE blend.
val premultiply = ParticleMaterials.premultipliesUnderShaders(storm.effect.particle.material)
val red = if (premultiply) colour.x * alpha else colour.x
val green = if (premultiply) colour.y * alpha else colour.y
val blue = if (premultiply) colour.z * alpha else colour.z
val spriteURange = sprite.u1 - sprite.u0
val spriteVRange = sprite.v1 - sprite.v0
@@ -202,26 +220,31 @@ class SnowstormParticle(
val minV = uvs.startV * spriteVRange + sprite.v0
val maxV = uvs.endV * spriteVRange + sprite.v0
// Glow vs non-glow is the effect's own environmentLighting flag: non-environment-lit particles
// are full-bright (they read as "glowing", and under packs that relight particles by the
// lightmap — Bliss/Photon — full-bright keeps them bright), while environment-lit particles use
// world light and read as lit by their surroundings. This preserves the authored distinction
// under shaders; per-pack blend correctness is handled in ParticleMaterials.
val p = if (storm.effect.particle.environmentLighting) getLightColor(tickDelta) else (15 shl 20 or (15 shl 4))
vertexConsumer
.addVertex(particleVertices[0].x, particleVertices[0].y, particleVertices[0].z)
.setUv(maxU, maxV)
.setColor(colour.x, colour.y, colour.z, colour.w)
.setColor(red, green, blue, alpha)
.setLight(p)
vertexConsumer
.addVertex(particleVertices[1].x, particleVertices[1].y, particleVertices[1].z)
.setUv(maxU, minV)
.setColor(colour.x, colour.y, colour.z, colour.w)
.setColor(red, green, blue, alpha)
.setLight(p)
vertexConsumer
.addVertex(particleVertices[2].x, particleVertices[2].y, particleVertices[2].z)
.setUv(minU, minV)
.setColor(colour.x, colour.y, colour.z, colour.w)
.setColor(red, green, blue, alpha)
.setLight(p)
vertexConsumer
.addVertex(particleVertices[3].x, particleVertices[3].y, particleVertices[3].z)
.setUv(minU, maxV)
.setColor(colour.x, colour.y, colour.z, colour.w)
.setColor(red, green, blue, alpha)
.setLight(p)
}
@@ -20,6 +20,13 @@ object CobblemonShaders {
lateinit var PARTICLE_BLEND: ShaderInstance
// This is Material.ALPHA. Weird internal name for "alphatest" shader.
lateinit var PARTICLE_CUTOUT: ShaderInstance
// Fullscreen screen-space colored "dynamic light" for glowing moves (see MoveGlowRenderer).
lateinit var MOVE_GLOW_LIGHT: ShaderInstance
// Composites the move-glow accumulation buffer onto the scene (additive).
lateinit var MOVE_GLOW_COMPOSITE: ShaderInstance
/** Whether the move-glow shaders have finished loading (they load with the other core shaders). */
fun isMoveGlowLightReady(): Boolean = ::MOVE_GLOW_LIGHT.isInitialized && ::MOVE_GLOW_COMPOSITE.isInitialized
private fun registerShader(shader: (ResourceProvider) -> ShaderRegistryData, callback: Consumer<ShaderInstance>){
SHADERS_TO_REGISTER.add(Pair(shader, callback))
@@ -31,5 +38,11 @@ object CobblemonShaders {
registerShader({rm: ResourceProvider -> ShaderRegistryData(rm, cobblemonResource("particle_cutout"), DefaultVertexFormat.PARTICLE) }) {
PARTICLE_CUTOUT = it
}
registerShader({rm: ResourceProvider -> ShaderRegistryData(rm, cobblemonResource("move_glow_light"), DefaultVertexFormat.POSITION) }) {
MOVE_GLOW_LIGHT = it
}
registerShader({rm: ResourceProvider -> ShaderRegistryData(rm, cobblemonResource("move_glow_composite"), DefaultVertexFormat.POSITION) }) {
MOVE_GLOW_COMPOSITE = it
}
}
}
@@ -32,6 +32,15 @@ data class RenderEvent(
/** Represents when a layer is rendered by [LevelRenderer.renderLevel]. Ordinal corresponds with its render order. */
enum class Stage {
TRANSLUCENT
TRANSLUCENT,
/**
* After [LevelRenderer.renderLevel] has finished — on a shaderpack this is after Iris/OptiFine
* has composited the world and restored vanilla shaders, so draws here are NOT intercepted by
* the pack and render with vanilla GL while still depth-testing against the full world depth.
* Used to draw the colored move-glow ([com.cobblemon.mod.common.client.render.MoveGlowRenderer])
* so it tints the scene in the move's color identically under every pack.
*/
POST_LEVEL
}
}
@@ -0,0 +1,14 @@
#version 150
// Composites the move-glow accumulation buffer onto the scene. The buffer was filled with a MAX blend
// so overlapping move lights kept the brightest contribution (they don't sum), and this adds that
// result to the scene once. Alpha is forced to 0 so the additive composite doesn't touch scene alpha.
uniform sampler2D GlowSampler;
in vec2 texCoord;
out vec4 fragColor;
void main() {
fragColor = vec4(texture(GlowSampler, texCoord).rgb, 0.0);
}
@@ -0,0 +1,18 @@
{
"blend": {
"func": "add",
"srcrgb": "one",
"dstrgb": "one",
"srcalpha": "one",
"dstalpha": "one"
},
"vertex": "cobblemon:move_glow_light",
"fragment": "cobblemon:move_glow_composite",
"attributes": [
"Position"
],
"samplers": [
{"name": "GlowSampler"}
],
"uniforms": []
}
@@ -0,0 +1,44 @@
#version 150
// Screen-space colored "dynamic light" for glowing Cobblemon moves. Runs as a fullscreen additive
// pass AFTER the world (and shaderpack) has composited, reading the scene depth to find each surface's
// position and adding the move's color with distance falloff. This tints the actual blocks/entities
// near the move in its color (no visible shape), complementing LambDynamicLights' monochrome level.
uniform sampler2D DepthSampler; // main framebuffer depth (full world depth at this stage)
uniform mat4 InverseProjMat; // inverse of the world projection, to rebuild view-space position
uniform vec3 LightViewPosA; // light segment start, in view space (== B for a point light)
uniform vec3 LightViewPosB; // light segment end, in view space (beams: emitter->target)
uniform vec3 LightColor; // the move's color (0..1)
uniform float LightRadius; // falloff radius around the segment, in blocks
uniform float Intensity; // overall strength (0..1)
in vec2 texCoord;
out vec4 fragColor;
void main() {
float depth = texture(DepthSampler, texCoord).r;
// Don't light the sky / far plane.
if (depth >= 1.0) {
fragColor = vec4(0.0);
return;
}
// Reconstruct the surface position in view space from depth.
vec4 ndc = vec4(texCoord * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0);
vec4 viewPos = InverseProjMat * ndc;
viewPos.xyz /= viewPos.w;
// Distance to the nearest point on the light segment A->B (capsule light). For a point light
// A == B, so this reduces to distance-to-point. For a beam it lights all around the whole line.
vec3 ab = LightViewPosB - LightViewPosA;
float t = clamp(dot(viewPos.xyz - LightViewPosA, ab) / max(dot(ab, ab), 1e-5), 0.0, 1.0);
vec3 closest = LightViewPosA + ab * t;
float dist = distance(viewPos.xyz, closest);
float atten = clamp(1.0 - dist / LightRadius, 0.0, 1.0);
atten *= atten; // quadratic falloff reads more like real light
fragColor = vec4(LightColor * atten * Intensity, 1.0);
}
@@ -0,0 +1,25 @@
{
"blend": {
"func": "add",
"srcrgb": "one",
"dstrgb": "one",
"srcalpha": "one",
"dstalpha": "one"
},
"vertex": "cobblemon:move_glow_light",
"fragment": "cobblemon:move_glow_light",
"attributes": [
"Position"
],
"samplers": [
{"name": "DepthSampler"}
],
"uniforms": [
{"name": "InverseProjMat", "type": "matrix4x4", "count": 16, "values": [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]},
{"name": "LightViewPosA", "type": "float", "count": 3, "values": [0.0, 0.0, 0.0]},
{"name": "LightViewPosB", "type": "float", "count": 3, "values": [0.0, 0.0, 0.0]},
{"name": "LightColor", "type": "float", "count": 3, "values": [1.0, 1.0, 1.0]},
{"name": "LightRadius", "type": "float", "count": 1, "values": [8.0]},
{"name": "Intensity", "type": "float", "count": 1, "values": [1.0]}
]
}
@@ -0,0 +1,11 @@
#version 150
// Fullscreen pass: Position is supplied directly in NDC (-1..1), no transform needed.
in vec3 Position;
out vec2 texCoord;
void main() {
gl_Position = vec4(Position.xy, 0.0, 1.0);
texCoord = Position.xy * 0.5 + 0.5;
}
+82
View File
@@ -8,6 +8,8 @@
*
*/
import java.net.URI
configurations.all {
resolutionStrategy {
force(libs.fabric.loader)
@@ -38,6 +40,7 @@ repositories {
maven("https://oss.sonatype.org/content/repositories/snapshots")
maven(url = "https://api.modrinth.com/maven")
maven(url = "https://maven.terraformersmc.com/")
maven(url = "https://maven.gegy.dev") // LambDynamicLights + spruceui/pridelib (dev runtime)
}
dependencies {
@@ -48,6 +51,34 @@ dependencies {
isTransitive = false
}
modLocalRuntime(libs.fabric.debugutils)
// Shader-compat testing (DEV RUNTIME ONLY — never shipped, never a real dependency).
// Lets `runClient` load Iris + Sodium so shaderpacks actually run, which is required to
// exercise ParticleMaterials' depthMask branch (see ShaderCompat). isTransitive = false so
// these don't drag in a second copy of Fabric API / FLK (already provided above). If you bump
// these, keep the Iris<->Sodium pairing valid (Iris 1.8.x needs Sodium 0.6.x for MC 1.21.1).
modLocalRuntime("maven.modrinth:iris:1.8.8+1.21.1-fabric") { isTransitive = false }
modLocalRuntime("maven.modrinth:sodium:mc1.21.1-0.6.13-fabric") { isTransitive = false }
// Iris bundles these as jar-in-jar in production, but loom doesn't add a mod's nested *libraries*
// to the dev classpath, so the dev client crashes with NoClassDefFoundError (e.g. org.anarres.cpp
// / glsl-transformer) when Iris loads a shaderpack. Supply them as plain dev-runtime libs.
// Versions are taken verbatim from iris-1.8.8's META-INF/jars — keep them in lockstep if Iris is
// bumped. localRuntime = dev runtime only, not remapped, not exported to the published jar.
localRuntime("org.anarres:jcpp:1.4.14")
localRuntime("io.github.douira:glsl-transformer:2.0.1")
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.
// 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.
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")
modLocalRuntime("dev.yumi.mc.core:yumi-mc-foundation:1.0.0-alpha.7+1.21.1")
modLocalRuntime("io.github.queerbric:pridelib:1.2.1+1.21")
modImplementation(libs.fabric.loader)
modApi(libs.fabric.api)
modApi(libs.bundles.fabric)
@@ -110,6 +141,57 @@ publishing {
}
tasks {
// Downloads the Complementary Unbound shaderpack into the dev client's shaderpacks/ folder and
// enables Iris by default, so `runClient` comes up shader-ready with zero manual setup. Both
// writes are idempotent: existing files are left untouched, so your in-game shader choice and
// any pack you swap in are preserved across runs. Run dir is "runClient" (see platform-conventions).
val setupShaderTesting by registering {
group = "cobblemon"
description = "Provisions the test shaderpacks + enables Iris for the dev runClient."
// The four Iris packs we want particles to look correct under. Swap between them in-game
// (Video Settings -> Shader Packs) to A/B the premultiplied-alpha fix in ParticleMaterials.
// <filename> to <direct Modrinth CDN url>.
//
// VERSIONS ARE PINNED FOR MC 1.21.1. The newest releases of these packs reference the
// BIOME_PALE_GARDEN uniform (Pale Garden is a 1.21.4 biome), which Iris cannot resolve on
// 1.21.1 -> hard crash building the render pipeline ("Unknown variable: BIOME_PALE_GARDEN").
// Each pin below was confirmed Pale-Garden-free by grepping the archive. If this project ever
// targets >= 1.21.4, bump these to the latest versions.
val packs = mapOf(
"ComplementaryUnbound_r5.3.zip" to "https://cdn.modrinth.com/data/R6NEzAwj/versions/CVu3AGAd/ComplementaryUnbound_r5.3.zip",
"ComplementaryReimagined_r5.3.zip" to "https://cdn.modrinth.com/data/HVnmMxH1/versions/71Hn3myZ/ComplementaryReimagined_r5.3.zip",
"Bliss_v2.1.2_(Chocapic13_Shaders_edit).zip" to "https://cdn.modrinth.com/data/ZvMtQlho/versions/kC2Y8q1P/Bliss_v2.1.2_%28Chocapic13_Shaders_edit%29.zip",
"photon_v1.1.zip" to "https://cdn.modrinth.com/data/lLqFfGNs/versions/6FdrBtKp/photon_v1.1.zip"
)
val shaderpacksDir = layout.projectDirectory.dir("runClient/shaderpacks")
val irisProps = layout.projectDirectory.file("runClient/config/iris.properties")
// Default the in-game selection to Complementary Unbound (the originally-reported pack); idempotent.
val defaultPack = "ComplementaryUnbound_r5.3.zip"
outputs.dir(shaderpacksDir)
doLast {
packs.forEach { (filename, url) ->
val zip = shaderpacksDir.file(filename).asFile
if (!zip.exists()) {
zip.parentFile.mkdirs()
logger.lifecycle("Downloading shaderpack -> ${zip.relativeTo(rootProject.projectDir)}")
URI(url).toURL().openStream().use { input ->
zip.outputStream().use { output -> input.copyTo(output) }
}
}
}
val props = irisProps.asFile
if (!props.exists()) {
props.parentFile.mkdirs()
props.writeText("enableShaders=true\nshaderPack=$defaultPack\n")
logger.lifecycle("Wrote ${props.relativeTo(rootProject.projectDir)} (Iris auto-enabled with $defaultPack).")
}
}
}
named("runClient") {
dependsOn(setupShaderTesting)
}
// The AW file is needed in :fabric project resources when the game is run.
val copyAccessWidener by registering(Copy::class) {
from(loom.accessWidenerPath)
@@ -171,6 +171,21 @@ class CobblemonFabricClient: ClientModInitializer, CobblemonClientImplementation
)
}
// Fires after the level (and any shaderpack composite) — the only point a draw isn't
// intercepted by Iris/OptiFine. Used by MoveGlowRenderer for the colored move glow.
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()
)
)
}
CobblemonModelPredicateRegistry.registerPredicates()
}