feat: Add dev capes
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
package moe.nea.firmament.mixins.feature.devcosmetics;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||
import com.llamalad7.mixinextras.sugar.Local;
|
||||
import kotlin.Unit;
|
||||
import moe.nea.firmament.features.misc.CustomCapes;
|
||||
import net.minecraft.client.render.RenderLayer;
|
||||
import net.minecraft.client.render.VertexConsumer;
|
||||
import net.minecraft.client.render.VertexConsumerProvider;
|
||||
import net.minecraft.client.render.entity.feature.CapeFeatureRenderer;
|
||||
import net.minecraft.client.render.entity.feature.FeatureRenderer;
|
||||
import net.minecraft.client.render.entity.feature.FeatureRendererContext;
|
||||
import net.minecraft.client.render.entity.model.BipedEntityModel;
|
||||
import net.minecraft.client.render.entity.model.PlayerEntityModel;
|
||||
import net.minecraft.client.render.entity.state.PlayerEntityRenderState;
|
||||
import net.minecraft.client.util.SkinTextures;
|
||||
import net.minecraft.client.util.math.MatrixStack;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
|
||||
@Mixin(CapeFeatureRenderer.class)
|
||||
public abstract class CustomCapeFeatureRenderer extends FeatureRenderer<PlayerEntityRenderState, PlayerEntityModel> {
|
||||
public CustomCapeFeatureRenderer(FeatureRendererContext<PlayerEntityRenderState, PlayerEntityModel> context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@WrapOperation(
|
||||
method = "render(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/client/render/entity/state/PlayerEntityRenderState;FF)V",
|
||||
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/entity/model/BipedEntityModel;render(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumer;II)V")
|
||||
)
|
||||
private void onRender(BipedEntityModel instance, MatrixStack matrixStack, VertexConsumer vertexConsumer, int light, int overlay, Operation<Void> original, @Local PlayerEntityRenderState playerEntityRenderState, @Local SkinTextures skinTextures, @Local VertexConsumerProvider vertexConsumerProvider) {
|
||||
// TODO: load special cape from playerEntityRenderState
|
||||
CustomCapes.render(
|
||||
playerEntityRenderState,
|
||||
vertexConsumer,
|
||||
RenderLayer.getEntitySolid(skinTextures.capeTexture()),
|
||||
vertexConsumerProvider,
|
||||
updatedConsumer -> {
|
||||
original.call(instance, matrixStack, updatedConsumer, light, overlay);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package moe.nea.firmament.mixins.feature.devcosmetics;
|
||||
|
||||
import moe.nea.firmament.features.misc.CustomCapes;
|
||||
import net.minecraft.client.render.entity.state.PlayerEntityRenderState;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
|
||||
@Mixin(PlayerEntityRenderState.class)
|
||||
public class CustomCapeStorage implements CustomCapes.CapeStorage {
|
||||
@Unique
|
||||
CustomCapes.CustomCape customCape;
|
||||
|
||||
@Override
|
||||
public CustomCapes.@Nullable CustomCape getCape_firmament() {
|
||||
return customCape;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCape_firmament(CustomCapes.@Nullable CustomCape customCape) {
|
||||
this.customCape = customCape;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package moe.nea.firmament.mixins.feature.devcosmetics;
|
||||
|
||||
import moe.nea.firmament.features.misc.CustomCapes;
|
||||
import net.minecraft.client.network.AbstractClientPlayerEntity;
|
||||
import net.minecraft.client.render.entity.PlayerEntityRenderer;
|
||||
import net.minecraft.client.render.entity.state.PlayerEntityRenderState;
|
||||
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(PlayerEntityRenderer.class)
|
||||
public class SaveCapeToPlayerEntityRenderState {
|
||||
@Inject(method = "updateRenderState(Lnet/minecraft/client/network/AbstractClientPlayerEntity;Lnet/minecraft/client/render/entity/state/PlayerEntityRenderState;F)V",
|
||||
at = @At("TAIL"))
|
||||
private void addCustomCape(AbstractClientPlayerEntity abstractClientPlayerEntity, PlayerEntityRenderState playerEntityRenderState, float f, CallbackInfo ci) {
|
||||
CustomCapes.addCapeData(abstractClientPlayerEntity, playerEntityRenderState);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import net.minecraft.client.network.AbstractClientPlayerEntity
|
||||
import net.minecraft.client.network.ClientPlayerEntity
|
||||
import net.minecraft.entity.decoration.ArmorStandEntity
|
||||
import moe.nea.firmament.Firmament
|
||||
import moe.nea.firmament.annotations.Subscribe
|
||||
@@ -64,7 +63,7 @@ object ExportRecipe {
|
||||
?: ""
|
||||
val reply = waitForTextInput("$guessName (NPC)", "Export stub")
|
||||
val id = generateName(reply)
|
||||
ItemExporter.exportStub(id, reply) {
|
||||
ItemExporter.exportStub(id, "§9$reply") {
|
||||
val playerEntity = entity as? AbstractClientPlayerEntity
|
||||
val textureUrl = playerEntity?.skinTextures?.textureUrl
|
||||
if (textureUrl != null)
|
||||
@@ -117,7 +116,7 @@ object ExportRecipe {
|
||||
val shopId = SkyblockId(title.uppercase().replace(" ", "_") + "_NPC")
|
||||
if (!ItemExporter.isExported(shopId)) {
|
||||
// TODO: export location + skin of last clicked npc
|
||||
ItemExporter.exportStub(shopId, "$title (NPC)")
|
||||
ItemExporter.exportStub(shopId, "§9$title (NPC)")
|
||||
}
|
||||
for (index in (9..9 * 5)) {
|
||||
val item = event.screen.getSlotByIndex(index, false)?.stack ?: continue
|
||||
|
||||
165
src/main/kotlin/features/misc/CustomCapes.kt
Normal file
165
src/main/kotlin/features/misc/CustomCapes.kt
Normal file
@@ -0,0 +1,165 @@
|
||||
package moe.nea.firmament.features.misc
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem
|
||||
import java.util.OptionalDouble
|
||||
import java.util.OptionalInt
|
||||
import util.render.CustomRenderPipelines
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import net.minecraft.client.network.AbstractClientPlayerEntity
|
||||
import net.minecraft.client.render.BufferBuilder
|
||||
import net.minecraft.client.render.RenderLayer
|
||||
import net.minecraft.client.render.VertexConsumer
|
||||
import net.minecraft.client.render.VertexConsumerProvider
|
||||
import net.minecraft.client.render.entity.state.PlayerEntityRenderState
|
||||
import net.minecraft.client.util.BufferAllocator
|
||||
import net.minecraft.util.Identifier
|
||||
import moe.nea.firmament.Firmament
|
||||
import moe.nea.firmament.util.MC
|
||||
import moe.nea.firmament.util.TimeMark
|
||||
|
||||
object CustomCapes {
|
||||
interface CustomCapeRenderer {
|
||||
fun replaceRender(
|
||||
renderLayer: RenderLayer,
|
||||
vertexConsumerProvider: VertexConsumerProvider,
|
||||
model: (VertexConsumer) -> Unit
|
||||
)
|
||||
}
|
||||
|
||||
data class TexturedCapeRenderer(
|
||||
val location: Identifier
|
||||
) : CustomCapeRenderer {
|
||||
override fun replaceRender(
|
||||
renderLayer: RenderLayer,
|
||||
vertexConsumerProvider: VertexConsumerProvider,
|
||||
model: (VertexConsumer) -> Unit
|
||||
) {
|
||||
model(vertexConsumerProvider.getBuffer(RenderLayer.getEntitySolid(location)))
|
||||
}
|
||||
}
|
||||
|
||||
data class ParallaxedHighlightCapeRenderer(
|
||||
val template: Identifier,
|
||||
val background: Identifier,
|
||||
val overlay: Identifier,
|
||||
val animationSpeed: Duration,
|
||||
) : CustomCapeRenderer {
|
||||
override fun replaceRender(
|
||||
renderLayer: RenderLayer,
|
||||
vertexConsumerProvider: VertexConsumerProvider,
|
||||
model: (VertexConsumer) -> Unit
|
||||
) {
|
||||
BufferAllocator(2048).use { allocator ->
|
||||
val bufferBuilder = BufferBuilder(allocator, renderLayer.drawMode, renderLayer.vertexFormat)
|
||||
model(bufferBuilder)
|
||||
bufferBuilder.end().use { buffer ->
|
||||
val commandEncoder = RenderSystem.getDevice().createCommandEncoder()
|
||||
val vertexBuffer = renderLayer.vertexFormat.uploadImmediateVertexBuffer(buffer.buffer)
|
||||
val indexBufferConstructor = RenderSystem.getSequentialBuffer(renderLayer.drawMode)
|
||||
val indexBuffer = indexBufferConstructor.getIndexBuffer(buffer.drawParameters.indexCount)
|
||||
val templateTexture = MC.textureManager.getTexture(template)
|
||||
val backgroundTexture = MC.textureManager.getTexture(background)
|
||||
val foregroundTexture = MC.textureManager.getTexture(overlay)
|
||||
commandEncoder.createRenderPass(
|
||||
MC.instance.framebuffer.colorAttachment,
|
||||
OptionalInt.empty(),
|
||||
MC.instance.framebuffer.depthAttachment,
|
||||
OptionalDouble.empty(),
|
||||
).use { renderPass ->
|
||||
// TODO: account for lighting
|
||||
renderPass.setPipeline(CustomRenderPipelines.PARALLAX_CAPE_SHADER)
|
||||
renderPass.bindSampler("Sampler0", templateTexture.glTexture)
|
||||
renderPass.bindSampler("Sampler1", backgroundTexture.glTexture)
|
||||
renderPass.bindSampler("Sampler3", foregroundTexture.glTexture)
|
||||
val animationValue = (startTime.passedTime() / animationSpeed).mod(1F)
|
||||
renderPass.setUniform("Animation", animationValue.toFloat())
|
||||
renderPass.setIndexBuffer(indexBuffer, indexBufferConstructor.indexType)
|
||||
renderPass.setVertexBuffer(0, vertexBuffer)
|
||||
renderPass.drawIndexed(0, buffer.drawParameters.indexCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface CapeStorage {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun cast(playerEntityRenderState: PlayerEntityRenderState) =
|
||||
playerEntityRenderState as CapeStorage
|
||||
|
||||
}
|
||||
|
||||
var cape_firmament: CustomCape?
|
||||
}
|
||||
|
||||
data class CustomCape(
|
||||
val id: String,
|
||||
val label: String,
|
||||
val render: CustomCapeRenderer,
|
||||
)
|
||||
|
||||
enum class AllCapes(val label: String, val render: CustomCapeRenderer) {
|
||||
FIRMAMENT_ANIMATED(
|
||||
"Animated Firmament", ParallaxedHighlightCapeRenderer(
|
||||
Firmament.identifier("textures/cape/parallax_template.png"),
|
||||
Firmament.identifier("textures/cape/parallax_background.png"),
|
||||
Firmament.identifier("textures/cape/firmament_star.png"),
|
||||
110.seconds
|
||||
)
|
||||
),
|
||||
// FURFSKY(
|
||||
// "FurfSky",
|
||||
// TexturedCapeRenderer(Firmament.identifier("textures/cape/fsr_static.png"))
|
||||
// ),
|
||||
FIRMAMENT_STATIC(
|
||||
"Firmament",
|
||||
TexturedCapeRenderer(Firmament.identifier("textures/cape/firm_static.png"))
|
||||
)
|
||||
;
|
||||
|
||||
val cape = CustomCape(name, label, render)
|
||||
}
|
||||
|
||||
val byId = AllCapes.entries.associateBy { it.cape.id }
|
||||
val byUuid = listOf(
|
||||
Devs.nea to AllCapes.FIRMAMENT_ANIMATED,
|
||||
Devs.kath to AllCapes.FIRMAMENT_STATIC,
|
||||
Devs.jani to AllCapes.FIRMAMENT_ANIMATED,
|
||||
).flatMap { (dev, cape) -> dev.uuids.map { it to cape.cape } }.toMap()
|
||||
|
||||
@JvmStatic
|
||||
fun render(
|
||||
playerEntityRenderState: PlayerEntityRenderState,
|
||||
vertexConsumer: VertexConsumer,
|
||||
renderLayer: RenderLayer,
|
||||
vertexConsumerProvider: VertexConsumerProvider,
|
||||
model: (VertexConsumer) -> Unit
|
||||
) {
|
||||
val capeStorage = CapeStorage.cast(playerEntityRenderState)
|
||||
val firmCape = capeStorage.cape_firmament
|
||||
if (firmCape != null) {
|
||||
firmCape.render.replaceRender(renderLayer, vertexConsumerProvider, model)
|
||||
} else {
|
||||
model(vertexConsumer)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun addCapeData(
|
||||
player: AbstractClientPlayerEntity,
|
||||
playerEntityRenderState: PlayerEntityRenderState
|
||||
) {
|
||||
val cape = byUuid[player.uuid]
|
||||
val capeStorage = CapeStorage.cast(playerEntityRenderState)
|
||||
if (cape == null) {
|
||||
capeStorage.cape_firmament = null
|
||||
} else {
|
||||
capeStorage.cape_firmament = cape
|
||||
playerEntityRenderState.capeVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
val startTime = TimeMark.now()
|
||||
}
|
||||
16
src/main/kotlin/features/misc/Devs.kt
Normal file
16
src/main/kotlin/features/misc/Devs.kt
Normal file
@@ -0,0 +1,16 @@
|
||||
package moe.nea.firmament.features.misc
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
object Devs {
|
||||
data class Dev(
|
||||
val uuids: List<UUID>,
|
||||
) {
|
||||
constructor(vararg uuid: UUID) : this(uuid.toList())
|
||||
constructor(vararg uuid: String) : this(uuid.map { UUID.fromString(it) })
|
||||
}
|
||||
|
||||
val nea = Dev("d3cb85e2-3075-48a1-b213-a9bfb62360c1", "842204e6-6880-487b-ae5a-0595394f9948")
|
||||
val kath = Dev("add71246-c46e-455c-8345-c129ea6f146c", "b491990d-53fd-4c5f-a61e-19d58cc7eddf")
|
||||
val jani = Dev("8a9f1841-48e9-48ed-b14f-76a124e6c9df")
|
||||
}
|
||||
@@ -49,6 +49,15 @@ object CustomRenderPipelines {
|
||||
.withFragmentShader(Firmament.identifier("circle_discard_color"))
|
||||
.withBlend(BlendFunction.TRANSLUCENT)
|
||||
.build()
|
||||
val PARALLAX_CAPE_SHADER =
|
||||
RenderPipeline.builder(RenderPipelines.ENTITY_SNIPPET)
|
||||
.withLocation(Firmament.identifier("parallax_cape"))
|
||||
.withFragmentShader(Firmament.identifier("cape/parallax"))
|
||||
.withSampler("Sampler0")
|
||||
.withSampler("Sampler1")
|
||||
.withSampler("Sampler3")
|
||||
.withUniform("Animation", UniformType.FLOAT)
|
||||
.build()
|
||||
}
|
||||
|
||||
object CustomRenderLayers {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#version 150
|
||||
|
||||
#moj_import <minecraft:fog.glsl>
|
||||
#define M_PI 3.1415926535897932384626433832795
|
||||
#define M_TAU (2.0 * M_PI)
|
||||
uniform sampler2D Sampler0;
|
||||
uniform sampler2D Sampler1;
|
||||
uniform sampler2D Sampler3;
|
||||
|
||||
uniform vec4 ColorModulator;
|
||||
uniform float FogStart;
|
||||
uniform float FogEnd;
|
||||
uniform vec4 FogColor;
|
||||
uniform float Animation;
|
||||
|
||||
in float vertexDistance;
|
||||
in vec4 vertexColor;
|
||||
in vec4 lightMapColor;
|
||||
in vec4 overlayColor;
|
||||
in vec2 texCoord0;
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
float highlightDistance(vec2 coord, vec2 direction, float time) {
|
||||
vec2 dir = normalize(direction);
|
||||
float projection = dot(coord, dir);
|
||||
float animationTime = sin(projection + time * 13 * M_TAU);
|
||||
if (animationTime < 0.997) {
|
||||
return 0.0;
|
||||
}
|
||||
return animationTime;
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec4 color = texture(Sampler0, texCoord0);
|
||||
if (color.g > 0.99) {
|
||||
// TODO: maybe this speed in each direction should be a uniform
|
||||
color = texture(Sampler1, texCoord0 + Animation * vec2(3.0, -2.0));
|
||||
}
|
||||
|
||||
vec4 highlightColor = texture(Sampler3, texCoord0);
|
||||
if (highlightColor.a > 0.5) {
|
||||
color = highlightColor;
|
||||
float animationHighlight = highlightDistance(texCoord0, vec2(-12.0, 2.0), Animation);
|
||||
color.rgb += (animationHighlight);
|
||||
}
|
||||
#ifdef ALPHA_CUTOUT
|
||||
if (color.a < ALPHA_CUTOUT) {
|
||||
discard;
|
||||
}
|
||||
#endif
|
||||
fragColor = linear_fog(color, vertexDistance, FogStart, FogEnd, FogColor);
|
||||
}
|
||||
16
src/main/resources/assets/firmament/textures/cape/REUSE.toml
Normal file
16
src/main/resources/assets/firmament/textures/cape/REUSE.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
#SPDX-FileCopyrightText: 2025 Linnea Gräf <nea@nea.moe>
|
||||
#
|
||||
#SPDX-License-Identifier: CC0-1.0
|
||||
version = 1
|
||||
|
||||
[[annotations]]
|
||||
path = ["firmament_star.png", "parallax_background.png", "parallax_template.png"]
|
||||
SPDX-License-Identifier = "CC-BY-4.0"
|
||||
SPDX-FileCopyrightText = ["ic22487", "Linnea Gräf"]
|
||||
|
||||
[[annotations]]
|
||||
path = ["firm_static.png"]
|
||||
SPDX-License-Identifier = "CC-BY-4.0"
|
||||
SPDX-FileCopyrightText = ["ic22487", "kathund"]
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 516 B |
Reference in New Issue
Block a user