add glow halo around terastal

This commit is contained in:
Maxime Desforest
2026-10-06 20:39:47 +02:00
parent f114a107ca
commit b1585f60cc
14 changed files with 331 additions and 20 deletions
@@ -9,6 +9,7 @@
package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.api.scheduling.ClientTaskTracker;
import com.cobblemon.mod.common.client.render.shader.TeraHalo;
import com.cobblemon.mod.common.integration.IrisCompatibility;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.llamalad7.mixinextras.sugar.Local;
@@ -59,6 +60,7 @@ public class GameRendererMixin {
)
private void cobblemon$drawAfterShaderPack(DeltaTracker deltaTracker, CallbackInfo ci) {
IrisCompatibility.INSTANCE.drawDeferred();
TeraHalo.INSTANCE.draw();
}
@ModifyArg(
@@ -0,0 +1,34 @@
/*
* 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.mixin.client;
import com.cobblemon.mod.common.client.render.shader.TeraHalo;
import net.minecraft.client.renderer.LevelRenderer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(LevelRenderer.class)
public class LevelRendererMixin {
@Inject(method = "initOutline", at = @At("TAIL"))
private void cobblemon$loadTeraHalo(CallbackInfo ci) {
TeraHalo.INSTANCE.load();
}
@Inject(method = "resize", at = @At("TAIL"))
private void cobblemon$resizeTeraHalo(int width, int height, CallbackInfo ci) {
TeraHalo.INSTANCE.resize(width, height);
}
@Inject(method = "close", at = @At("TAIL"))
private void cobblemon$closeTeraHalo(CallbackInfo ci) {
TeraHalo.INSTANCE.close();
}
}
@@ -54,6 +54,8 @@ import net.minecraft.client.model.geom.ModelPart
import net.minecraft.client.renderer.MultiBufferSource
import net.minecraft.client.renderer.RenderStateShard
import com.cobblemon.mod.common.integration.IrisCompatibility
import com.cobblemon.mod.common.client.render.shader.TeraHalo
import com.cobblemon.mod.common.client.render.shader.TeraRender
import net.minecraft.client.renderer.RenderType
import net.minecraft.client.renderer.texture.DynamicTexture
import net.minecraft.resources.ResourceLocation
@@ -177,7 +179,7 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
var currentState: PosableState? = null
@Transient
var teraOverlay: Pair<RenderType, Int>? = null
var tera: TeraRender? = null
@Transient
val functions = QueryStruct(this.animationFunctions())
@@ -268,7 +270,7 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
currentLayers = emptyList()
bufferProvider = null
currentState = null
teraOverlay = null
tera = null
}
/**
@@ -495,8 +497,9 @@ open class PosableModel(@Transient override val rootPart: Bone) : ModelFrame {
)
stack.popPose()
}
teraOverlay?.let { (renderType, tint) ->
IrisCompatibility.renderAfterShaderPack(provider, renderType) { rootPart.render(context, stack, it, packedLight, packedOverlay, tint) }
tera?.let { tera ->
IrisCompatibility.renderAfterShaderPack(provider, tera.overlay) { rootPart.render(context, stack, it, packedLight, packedOverlay, tera.tint) }
TeraHalo.record(tera.halo) { rootPart.render(context, stack, it, packedLight, packedOverlay, tera.tint) }
}
}
}
@@ -145,7 +145,7 @@ class PokemonRenderer(
}
modelNow.setLayerContext(buffer, clientDelegate, VaryingModelRepository.getLayers(entity.pokemon.species.resourceIdentifier, clientDelegate))
modelNow.teraOverlay = TeraOverlay.forAspects(entity.aspects, getTextureLocation(entity))
modelNow.tera = TeraOverlay.forAspects(entity.aspects, getTextureLocation(entity))
if (entity.passengers.isNotEmpty()) {
@@ -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.client.render.shader
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.integration.DeferredMeshes
import com.cobblemon.mod.common.integration.IrisCompatibility
import com.cobblemon.mod.common.util.cobblemonResource
import com.mojang.blaze3d.platform.GlStateManager
import com.mojang.blaze3d.systems.RenderSystem
import com.mojang.blaze3d.vertex.DefaultVertexFormat
import com.mojang.blaze3d.vertex.VertexConsumer
import com.mojang.blaze3d.vertex.VertexFormat
import net.minecraft.Util
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.GameRenderer
import net.minecraft.client.renderer.PostChain
import net.minecraft.client.renderer.RenderStateShard
import net.minecraft.client.renderer.RenderType
import net.minecraft.resources.ResourceLocation
object TeraHalo {
private val CHAIN_LOCATION = cobblemonResource("shaders/post/tera_halo.json")
private val deferred = DeferredMeshes()
private var chain: PostChain? = null
private val MASK_TARGET = RenderStateShard.OutputStateShard(
"cobblemon_tera_halo_mask",
{ chain?.getTempTarget("mask")?.bindWrite(false) },
{ Minecraft.getInstance().mainRenderTarget.bindWrite(false) }
)
private val renderType = Util.memoize { texture: ResourceLocation ->
RenderType.create(
"cobblemon_tera_halo",
DefaultVertexFormat.POSITION_TEX_COLOR,
VertexFormat.Mode.QUADS,
256,
false,
false,
RenderType.CompositeState.builder()
.setShaderState(RenderStateShard.ShaderStateShard(GameRenderer::getRendertypeOutlineShader))
.setTextureState(RenderStateShard.TextureStateShard(texture, false, false))
.setDepthTestState(RenderStateShard.LEQUAL_DEPTH_TEST)
.setCullState(RenderStateShard.CULL)
.setWriteMaskState(RenderStateShard.COLOR_WRITE)
.setOutputState(MASK_TARGET)
.createCompositeState(false)
)
}
fun renderType(texture: ResourceLocation): RenderType = renderType.apply(texture)
fun record(renderType: RenderType, draw: (VertexConsumer) -> Unit) {
if (IrisCompatibility.isRenderingShadowPass()) return
deferred.record(renderType, draw)
}
fun draw() {
if (deferred.isEmpty()) return
val chain = chain ?: return deferred.discard()
val main = Minecraft.getInstance().mainRenderTarget
val mask = chain.getTempTarget("mask")
mask.setClearColor(0F, 0F, 0F, 0F)
mask.clear(Minecraft.ON_OSX)
mask.copyDepthFrom(main)
deferred.drawAll()
chain.process(0F)
main.bindWrite(false)
RenderSystem.enableBlend()
RenderSystem.blendFunc(GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ONE_MINUS_SRC_COLOR)
chain.getTempTarget("swap").blitToScreen(main.width, main.height, false)
RenderSystem.disableBlend()
RenderSystem.defaultBlendFunc()
}
fun load() {
close()
val minecraft = Minecraft.getInstance()
chain = try {
PostChain(minecraft.textureManager, minecraft.resourceManager, minecraft.mainRenderTarget, CHAIN_LOCATION).also {
it.resize(minecraft.mainRenderTarget.width, minecraft.mainRenderTarget.height)
}
} catch (e: Exception) {
Cobblemon.LOGGER.error("Failed to load tera halo post chain $CHAIN_LOCATION", e)
null
}
}
fun resize(width: Int, height: Int) {
chain?.resize(width, height)
}
fun close() {
chain?.close()
chain = null
}
}
@@ -18,6 +18,8 @@ import net.minecraft.client.renderer.RenderStateShard
import net.minecraft.client.renderer.RenderType
import net.minecraft.resources.ResourceLocation
data class TeraRender(val overlay: RenderType, val halo: RenderType, val tint: Int)
object TeraOverlay {
private const val ASPECT = "terastal"
@@ -60,9 +62,9 @@ object TeraOverlay {
)
}
fun forAspects(aspects: Set<String>, texture: ResourceLocation): Pair<RenderType, Int>? {
fun forAspects(aspects: Set<String>, texture: ResourceLocation): TeraRender? {
val aspect = aspects.firstOrNull { it == ASPECT || it.startsWith("$ASPECT-") } ?: return null
val hue = ElementalTypes.get(aspect.removePrefix(ASPECT).removePrefix("-"))?.hue
return renderType.apply(texture) to (0xFF000000.toInt() or (hue ?: 0xFFFFFF))
return TeraRender(renderType.apply(texture), TeraHalo.renderType(texture), 0xFF000000.toInt() or (hue ?: 0xFFFFFF))
}
}
@@ -25,10 +25,7 @@ object IrisCompatibility {
private val shaderPackInUse = api?.getMethod("isShaderPackInUse")
private val renderingShadowPass = api?.getMethod("isRenderingShadowPass")
private val bytes = ByteBufferBuilder(1 shl 18)
private val deferred = mutableListOf<Pair<RenderType, MeshData>>()
private val modelView = Matrix4f()
private val projection = Matrix4f()
private val deferred = DeferredMeshes()
fun isShaderPackInUse() = shaderPackInUse?.invoke(instance) == true
@@ -38,18 +35,36 @@ object IrisCompatibility {
if (!isShaderPackInUse()) return draw(provider.getBuffer(renderType))
if (isRenderingShadowPass()) return
if (deferred.isEmpty()) {
modelView.set(RenderSystem.getModelViewMatrix())
projection.set(RenderSystem.getProjectionMatrix())
}
val builder = BufferBuilder(bytes, renderType.mode(), renderType.format())
draw(builder)
builder.build()?.let { deferred += renderType to it }
deferred.record(renderType, draw)
}
fun drawDeferred() {
if (deferred.isEmpty()) return
Minecraft.getInstance().mainRenderTarget.bindWrite(false)
deferred.drawAll()
}
}
/** Meshes recorded during level rendering, drawn later with the camera matrices they were recorded under. */
class DeferredMeshes {
private val bytes = ByteBufferBuilder(1 shl 18)
private val meshes = mutableListOf<Pair<RenderType, MeshData>>()
private val modelView = Matrix4f()
private val projection = Matrix4f()
fun isEmpty() = meshes.isEmpty()
fun record(renderType: RenderType, draw: (VertexConsumer) -> Unit) {
if (meshes.isEmpty()) {
modelView.set(RenderSystem.getModelViewMatrix())
projection.set(RenderSystem.getProjectionMatrix())
}
val builder = BufferBuilder(bytes, renderType.mode(), renderType.format())
draw(builder)
builder.build()?.let { meshes += renderType to it }
}
fun drawAll() {
RenderSystem.backupProjectionMatrix()
RenderSystem.setProjectionMatrix(projection, VertexSorting.DISTANCE_TO_ORIGIN)
val stack = RenderSystem.getModelViewStack()
@@ -57,11 +72,16 @@ object IrisCompatibility {
stack.set(modelView)
RenderSystem.applyModelViewMatrix()
deferred.forEach { (renderType, mesh) -> renderType.draw(mesh) }
deferred.clear()
meshes.forEach { (renderType, mesh) -> renderType.draw(mesh) }
meshes.clear()
stack.popMatrix()
RenderSystem.applyModelViewMatrix()
RenderSystem.restoreProjectionMatrix()
}
fun discard() {
meshes.forEach { it.second.close() }
meshes.clear()
}
}
@@ -0,0 +1,60 @@
{
"targets": [
"mask",
"swap",
"glow"
],
"passes": [
{
"name": "cobblemon_tera_halo_blur",
"intarget": "mask",
"outtarget": "swap",
"use_linear_filter": true,
"uniforms": [
{ "name": "BlurDir", "values": [ 1.0, 0.0 ] },
{ "name": "Radius", "values": [ 16.0 ] }
]
},
{
"name": "cobblemon_tera_halo_blur",
"intarget": "swap",
"outtarget": "glow",
"use_linear_filter": true,
"uniforms": [
{ "name": "BlurDir", "values": [ 0.0, 1.0 ] },
{ "name": "Radius", "values": [ 16.0 ] }
]
},
{
"name": "cobblemon_tera_halo_blur",
"intarget": "glow",
"outtarget": "swap",
"use_linear_filter": true,
"uniforms": [
{ "name": "BlurDir", "values": [ 1.0, 0.0 ] },
{ "name": "Radius", "values": [ 32.0 ] }
]
},
{
"name": "cobblemon_tera_halo_blur",
"intarget": "swap",
"outtarget": "glow",
"use_linear_filter": true,
"uniforms": [
{ "name": "BlurDir", "values": [ 0.0, 1.0 ] },
{ "name": "Radius", "values": [ 32.0 ] }
]
},
{
"name": "cobblemon_tera_halo_composite",
"intarget": "glow",
"outtarget": "swap",
"auxtargets": [
{ "name": "MaskSampler", "id": "mask" }
],
"uniforms": [
{ "name": "Strength", "values": [ 1.0 ] }
]
}
]
}
@@ -0,0 +1,23 @@
#version 150
uniform sampler2D DiffuseSampler;
uniform vec2 InSize;
// How far the blur reaches, in pixels on a 1080p screen
uniform float Radius;
in vec2 texCoord;
in vec2 sampleStep;
out vec4 fragColor;
const float WEIGHTS[5] = float[](0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216);
void main() {
vec2 tap = sampleStep * Radius * (InSize.y / 1080.0) / 4.0;
vec4 color = texture(DiffuseSampler, texCoord) * WEIGHTS[0];
for (int i = 1; i < 5; i++) {
color += texture(DiffuseSampler, texCoord + tap * float(i)) * WEIGHTS[i];
color += texture(DiffuseSampler, texCoord - tap * float(i)) * WEIGHTS[i];
}
fragColor = color;
}
@@ -0,0 +1,20 @@
{
"blend": {
"func": "add",
"srcrgb": "one",
"dstrgb": "zero"
},
"vertex": "blur",
"fragment": "cobblemon_tera_halo_blur",
"attributes": [ "Position" ],
"samplers": [
{ "name": "DiffuseSampler" }
],
"uniforms": [
{ "name": "ProjMat", "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": "InSize", "type": "float", "count": 2, "values": [ 1.0, 1.0 ] },
{ "name": "OutSize", "type": "float", "count": 2, "values": [ 1.0, 1.0 ] },
{ "name": "BlurDir", "type": "float", "count": 2, "values": [ 1.0, 0.0 ] },
{ "name": "Radius", "type": "float", "count": 1, "values": [ 20.0 ] }
]
}
@@ -0,0 +1,18 @@
#version 150
uniform sampler2D DiffuseSampler;
uniform sampler2D MaskSampler;
uniform float Strength;
in vec2 texCoord;
out vec4 fragColor;
// How much the glow is dimmed over the mon itself, so the crystal overlay stays readable
const float INNER_DIM = 0.9;
void main() {
vec3 glow = texture(DiffuseSampler, texCoord).rgb;
float inside = texture(MaskSampler, texCoord).a;
fragColor = vec4(min(glow * Strength, vec3(1.0)) * (1.0 - INNER_DIM * inside), 1.0);
}
@@ -0,0 +1,19 @@
{
"blend": {
"func": "add",
"srcrgb": "one",
"dstrgb": "zero"
},
"vertex": "sobel",
"fragment": "cobblemon_tera_halo_composite",
"attributes": [ "Position" ],
"samplers": [
{ "name": "DiffuseSampler" },
{ "name": "MaskSampler" }
],
"uniforms": [
{ "name": "ProjMat", "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": "OutSize", "type": "float", "count": 2, "values": [ 1.0, 1.0 ] },
{ "name": "Strength", "type": "float", "count": 1, "values": [ 2.2 ] }
]
}
@@ -71,6 +71,7 @@ transitive-accessible field net/minecraft/client/renderer/RenderStateShard NO_CU
transitive-accessible field net/minecraft/client/renderer/RenderStateShard COLOR_WRITE Lnet/minecraft/client/renderer/RenderStateShard$WriteMaskStateShard;
transitive-accessible field net/minecraft/client/renderer/RenderStateShard NO_DEPTH_TEST Lnet/minecraft/client/renderer/RenderStateShard$DepthTestStateShard;
transitive-accessible class net/minecraft/client/renderer/RenderStateShard$DepthTestStateShard
transitive-accessible class net/minecraft/client/renderer/RenderStateShard$OutputStateShard
transitive-accessible field net/minecraft/client/renderer/RenderStateShard LEQUAL_DEPTH_TEST Lnet/minecraft/client/renderer/RenderStateShard$DepthTestStateShard;
transitive-accessible field net/minecraft/client/renderer/RenderStateShard GLINT_TRANSPARENCY Lnet/minecraft/client/renderer/RenderStateShard$TransparencyStateShard;
transitive-accessible field net/minecraft/client/renderer/RenderStateShard ADDITIVE_TRANSPARENCY Lnet/minecraft/client/renderer/RenderStateShard$TransparencyStateShard;
@@ -29,6 +29,7 @@
"client.ItemInHandLayerMixin",
"client.ItemInHandRendererMixin",
"client.ItemRendererMixin",
"client.LevelRendererMixin",
"client.LivingEntityRendererMixin",
"client.LocalPlayerMixin",
"client.ModelLoaderMixin",