forked from mirrors/cobblemon
Initial testing for Shader support for our particles
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
|
||||
package com.cobblemon.mod.common.api.snowstorm
|
||||
|
||||
import com.cobblemon.mod.common.client.render.ShaderCompat
|
||||
import com.mojang.blaze3d.platform.GlStateManager
|
||||
import com.mojang.blaze3d.systems.RenderSystem
|
||||
import com.mojang.blaze3d.vertex.BufferBuilder
|
||||
@@ -27,7 +28,26 @@ object ParticleMaterials {
|
||||
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)
|
||||
// Premultiplied-alpha blending under an active shaderpack; straight alpha otherwise.
|
||||
//
|
||||
// WHY: Iris shaderpacks (Complementary Unbound/Reimagined, Bliss, Photon, ...) draw these
|
||||
// custom particles through their *translucent* particle program, which composites with a
|
||||
// premultiplied-alpha convention. Feeding it our straight-alpha colors makes faded
|
||||
// (low-alpha) particles darken toward black instead of fading toward the background — the
|
||||
// "gets darker as it fades" symptom. Switching to premultiplied blend (ONE, 1-SRC_ALPHA)
|
||||
// AND premultiplying the vertex color by its alpha (see SnowstormParticle.render) matches
|
||||
// that convention, so the fade looks ~like vanilla. This is one pack-agnostic code path:
|
||||
// it targets the shared premultiplied convention, not any single pack.
|
||||
//
|
||||
// SAFE FOR THE MAJORITY: with premultiplied vertex color, `ONE * (rgb*a) + (1-a)*dst`
|
||||
// equals straight alpha's `a*rgb + (1-a)*dst`, so the no-shader look is byte-identical;
|
||||
// the branch only diverges while a shaderpack is active.
|
||||
//
|
||||
// RESIDUAL LIMITATION: a pack that explicitly declares `blend.gbuffers_particles_translucent`
|
||||
// re-imposes its own blend and we cannot override that; and only the vertex alpha is
|
||||
// premultiplied (not the texture's per-texel alpha), so very soft sprite edges may read
|
||||
// slightly brighter than vanilla. The dominant over-lifetime fade is what this corrects.
|
||||
applyTranslucentBlend()
|
||||
return tessellator.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.PARTICLE)
|
||||
}
|
||||
|
||||
@@ -38,7 +58,20 @@ object ParticleMaterials {
|
||||
override fun begin(tessellator: Tesselator, textureManager: TextureManager): BufferBuilder {
|
||||
RenderSystem.enableBlend()
|
||||
RenderSystem.disableCull()
|
||||
RenderSystem.depthMask(true)
|
||||
// Translucent (additive) particles should not write depth. With a shaderpack active
|
||||
// (Iris/OptiFine) these custom render types are drawn through the pack's *translucent*
|
||||
// particle program; if they also write depth they get treated as solid geometry by the
|
||||
// pack's depth-dependent passes (fog, deferred lighting, translucent compositing), which
|
||||
// visibly dims the additive glow — the worst-affected symptom. Dropping depth writes only
|
||||
// when shaders are active keeps the vanilla (no-shader) look — used by the vast majority
|
||||
// of players — byte-for-byte identical.
|
||||
//
|
||||
// RESIDUAL LIMITATION: the additive blendFunc below (SRC_ALPHA, ONE) is honored by Iris
|
||||
// (it swaps the shader program but does not override blend for particles), but a
|
||||
// shaderpack still has final authority over how its translucent particle program lights
|
||||
// and composites the fragment. We route depth correctly and preserve additive blending,
|
||||
// but we cannot force true additive parity against a pack that composites it otherwise.
|
||||
RenderSystem.depthMask(!ShaderCompat.shadersActive())
|
||||
RenderSystem.setShader { GameRenderer.getParticleShader() }
|
||||
RenderSystem.setShaderTexture(0, TextureAtlas.LOCATION_PARTICLES)
|
||||
RenderSystem.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE)
|
||||
@@ -52,10 +85,15 @@ object ParticleMaterials {
|
||||
override fun begin(tessellator: Tesselator, textureManager: TextureManager): BufferBuilder {
|
||||
RenderSystem.enableBlend()
|
||||
RenderSystem.disableCull()
|
||||
RenderSystem.depthMask(true)
|
||||
// See ADD above: translucent particles drop depth writes only under an active shaderpack
|
||||
// so the pack's translucent pass doesn't treat them as solid. No change to the vanilla look.
|
||||
RenderSystem.depthMask(!ShaderCompat.shadersActive())
|
||||
RenderSystem.setShader { GameRenderer.getParticleShader() }
|
||||
RenderSystem.setShaderTexture(0, TextureAtlas.LOCATION_PARTICLES)
|
||||
RenderSystem.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA)
|
||||
// Premultiplied-alpha under shaders, straight alpha otherwise — see ALPHA for the full
|
||||
// rationale. Paired with the vertex-color premultiply in SnowstormParticle.render so the
|
||||
// no-shader look is unchanged.
|
||||
applyTranslucentBlend()
|
||||
return tessellator.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.PARTICLE)
|
||||
}
|
||||
|
||||
@@ -75,4 +113,26 @@ object ParticleMaterials {
|
||||
|
||||
override fun toString() = "OPAQUE"
|
||||
}
|
||||
|
||||
/**
|
||||
* True when [material]'s vertex color must be premultiplied by its alpha before being submitted
|
||||
* (see [ALPHA]). Only the alpha-composited materials need it, and only while a shaderpack is
|
||||
* active; ADD is additive and OPAQUE is not blended, so neither is premultiplied. Kept here so
|
||||
* the vertex path ([com.cobblemon.mod.common.client.render.SnowstormParticle.render]) and the
|
||||
* blend func set in [applyTranslucentBlend] always agree on which particles are premultiplied.
|
||||
*/
|
||||
fun premultipliesUnderShaders(material: ParticleMaterial): Boolean =
|
||||
(material == ParticleMaterial.ALPHA || material == ParticleMaterial.BLEND) && ShaderCompat.shadersActive()
|
||||
|
||||
/**
|
||||
* Premultiplied-alpha blend (source already carries rgb*a, so source factor is ONE) while a
|
||||
* shaderpack is active; vanilla straight-alpha blend otherwise. See [ALPHA] for why.
|
||||
*/
|
||||
private fun applyTranslucentBlend() {
|
||||
if (ShaderCompat.shadersActive()) {
|
||||
RenderSystem.blendFunc(GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA)
|
||||
} else {
|
||||
RenderSystem.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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 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
|
||||
* 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
|
||||
* 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.
|
||||
* Particle render types call this at most a handful of times per frame (once per [begin]), so the
|
||||
* cost is negligible.
|
||||
*/
|
||||
object ShaderCompat {
|
||||
|
||||
// Iris: net.irisshaders.iris.api.v0.IrisApi.getInstance().isShaderPackInUse()
|
||||
// getInstance() returns a stable singleton, so it is safe to cache.
|
||||
private val irisInstance: Any?
|
||||
private val irisIsShaderPackInUse: Method?
|
||||
|
||||
// OptiFine: net.optifine.Config.isShaders() (static)
|
||||
private val optifineIsShaders: Method?
|
||||
|
||||
init {
|
||||
var irisInst: Any? = null
|
||||
var irisMethod: Method? = null
|
||||
try {
|
||||
val irisApi = Class.forName("net.irisshaders.iris.api.v0.IrisApi")
|
||||
irisInst = irisApi.getMethod("getInstance").invoke(null)
|
||||
irisMethod = irisApi.getMethod("isShaderPackInUse")
|
||||
} catch (ignored: Throwable) {
|
||||
irisInst = null
|
||||
irisMethod = null
|
||||
}
|
||||
irisInstance = irisInst
|
||||
irisIsShaderPackInUse = irisMethod
|
||||
|
||||
var optifineMethod: Method? = null
|
||||
try {
|
||||
optifineMethod = Class.forName("net.optifine.Config").getMethod("isShaders")
|
||||
} catch (ignored: Throwable) {
|
||||
optifineMethod = null
|
||||
}
|
||||
optifineIsShaders = optifineMethod
|
||||
}
|
||||
|
||||
/**
|
||||
* `true` when Iris or OptiFine reports an active (compiled and enabled) shaderpack. Never throws;
|
||||
* any reflective failure is treated as "no shaders".
|
||||
*/
|
||||
fun shadersActive(): Boolean {
|
||||
val instance = irisInstance
|
||||
val irisMethod = irisIsShaderPackInUse
|
||||
if (instance != null && irisMethod != null) {
|
||||
try {
|
||||
if (irisMethod.invoke(instance) == true) return true
|
||||
} catch (ignored: Throwable) {}
|
||||
}
|
||||
|
||||
val optifineMethod = optifineIsShaders
|
||||
if (optifineMethod != null) {
|
||||
try {
|
||||
if (optifineMethod.invoke(null) == true) return true
|
||||
} catch (ignored: Throwable) {}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -194,6 +194,16 @@ 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)
|
||||
|
||||
// Under an active shaderpack the alpha-composited materials use premultiplied-alpha blending
|
||||
// (see ParticleMaterials.ALPHA), so the color we submit must be premultiplied by its alpha to
|
||||
// match. In vanilla this branch is skipped and the straight-alpha color is sent unchanged, so
|
||||
// the no-shader look is identical. ADD/OPAQUE are never premultiplied.
|
||||
val alpha = colour.w
|
||||
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
|
||||
|
||||
@@ -206,22 +216,22 @@ class SnowstormParticle(
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import java.net.URI
|
||||
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
force(libs.fabric.loader)
|
||||
@@ -48,6 +50,23 @@ 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")
|
||||
|
||||
modImplementation(libs.fabric.loader)
|
||||
modApi(libs.fabric.api)
|
||||
modApi(libs.bundles.fabric)
|
||||
@@ -110,6 +129,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)
|
||||
|
||||
Reference in New Issue
Block a user