From 414f31e9e6839359dbf8ff024762aab444286ed3 Mon Sep 17 00:00:00 2001 From: MrLeonardosVoid Date: Thu, 12 Mar 2026 05:19:37 +0300 Subject: [PATCH] Changed the appearance of Vis Nodes and the aspect transfer visuals from the aura node --- .../thavma/impl/client/T7RenderTypes.kt | 5 +- .../impl/client/event/T7ClientModEvents.kt | 5 + .../client/particle/EternalFlameParticle.kt | 8 + .../impl/client/particle/VisTrailParticle.kt | 194 ++++++++++++++++++ .../client/renderer/FlyingAspectsRenderer.kt | 144 +++++++++---- .../renderer/blockentity/AuraNodeBER.kt | 16 +- .../client/renderer/blockentity/MatrixBER.kt | 11 +- .../impl/client/renderer/entity/VisER.kt | 49 +++-- .../T7ParticleDescriptionProvider.kt | 2 + .../registries/deferred/T7ParticleTypes.kt | 1 + .../assets/thavma/particles/vis_trail.json | 5 + .../assets/thavma/shaders/core/aura_node.fsh | 65 +++++- 12 files changed, 429 insertions(+), 76 deletions(-) create mode 100644 src/main/java/me/alegian/thavma/impl/client/particle/VisTrailParticle.kt create mode 100644 src/main/resources/assets/thavma/particles/vis_trail.json diff --git a/src/main/java/me/alegian/thavma/impl/client/T7RenderTypes.kt b/src/main/java/me/alegian/thavma/impl/client/T7RenderTypes.kt index 4fb77d04..7083a4eb 100644 --- a/src/main/java/me/alegian/thavma/impl/client/T7RenderTypes.kt +++ b/src/main/java/me/alegian/thavma/impl/client/T7RenderTypes.kt @@ -25,7 +25,8 @@ object T7RenderTypes { .setShaderState(T7RenderStateShards.AURA_NODE_SHADER) .setTransparencyState(RenderStateShard.TRANSLUCENT_TRANSPARENCY) .setTextureState(RenderStateShard.NO_TEXTURE) - .setDepthTestState(T7RenderStateShards.NOT_EQUAL_DEPTH_TEST) // alpha colors do not stack in aura node layers, and aura nodes can be seen through blocks + .setDepthTestState(T7RenderStateShards.NOT_EQUAL_DEPTH_TEST) + .setWriteMaskState(RenderStateShard.COLOR_WRITE) .createCompositeState(false) ) @@ -39,7 +40,7 @@ object T7RenderTypes { true, CompositeState.builder() .setShaderState(RenderStateShard.POSITION_COLOR_SHADER) - .setTransparencyState(RenderStateShard.TRANSLUCENT_TRANSPARENCY) + .setTransparencyState(RenderStateShard.LIGHTNING_TRANSPARENCY) .setCullState(RenderStateShard.NO_CULL) .setWriteMaskState(RenderStateShard.COLOR_WRITE) .createCompositeState(false) diff --git a/src/main/java/me/alegian/thavma/impl/client/event/T7ClientModEvents.kt b/src/main/java/me/alegian/thavma/impl/client/event/T7ClientModEvents.kt index 0f179c4b..e12e728e 100644 --- a/src/main/java/me/alegian/thavma/impl/client/event/T7ClientModEvents.kt +++ b/src/main/java/me/alegian/thavma/impl/client/event/T7ClientModEvents.kt @@ -17,6 +17,7 @@ import me.alegian.thavma.impl.client.particle.CrucibleBubbleParticle import me.alegian.thavma.impl.client.particle.EternalFlameParticle import me.alegian.thavma.impl.client.particle.InfusionItemParticle import me.alegian.thavma.impl.client.particle.InfusionRuneParticle +import me.alegian.thavma.impl.client.particle.VisTrailParticle import me.alegian.thavma.impl.client.renderer.blockentity.* import me.alegian.thavma.impl.client.renderer.blockentity.withoutlevel.BlockItemBEWLR import me.alegian.thavma.impl.client.renderer.blockentity.withoutlevel.NodeJarBEWLR @@ -78,6 +79,10 @@ private fun registerParticleProviders(event: RegisterParticleProvidersEvent) { T7ParticleTypes.ETERNAL_FLAME.get(), EternalFlameParticle::Provider ) + event.registerSpriteSet( + T7ParticleTypes.VIS_TRAIL.get(), + VisTrailParticle::Provider + ) event.registerSpriteSet( T7ParticleTypes.INFUSION_ITEM.get(), InfusionItemParticle::Provider diff --git a/src/main/java/me/alegian/thavma/impl/client/particle/EternalFlameParticle.kt b/src/main/java/me/alegian/thavma/impl/client/particle/EternalFlameParticle.kt index 1e781cd8..93d81552 100644 --- a/src/main/java/me/alegian/thavma/impl/client/particle/EternalFlameParticle.kt +++ b/src/main/java/me/alegian/thavma/impl/client/particle/EternalFlameParticle.kt @@ -11,6 +11,10 @@ class EternalFlameParticle(level: ClientLevel, private val centerX: Double, priv private var initialSize = 0f private val initialAlpha = 1f + companion object { + var spriteSet: SpriteSet? = null + } + init { // add randomness to position xo += random.nextDouble() * 0.2 - 0.1 @@ -50,6 +54,10 @@ class EternalFlameParticle(level: ClientLevel, private val centerX: Double, priv override fun getRenderType() = T7ParticleRenderTypes.ETERNAL_FLAME class Provider(private val sprite: SpriteSet) : ParticleProvider { + init { + spriteSet = sprite + } + override fun createParticle(type: SimpleParticleType, level: ClientLevel, x: Double, y: Double, z: Double, xSpeed: Double, ySpeed: Double, zSpeed: Double) = EternalFlameParticle(level, x, y, z, xSpeed, ySpeed, zSpeed, sprite) } diff --git a/src/main/java/me/alegian/thavma/impl/client/particle/VisTrailParticle.kt b/src/main/java/me/alegian/thavma/impl/client/particle/VisTrailParticle.kt new file mode 100644 index 00000000..ee5fdddd --- /dev/null +++ b/src/main/java/me/alegian/thavma/impl/client/particle/VisTrailParticle.kt @@ -0,0 +1,194 @@ +package me.alegian.thavma.impl.client.particle + +import com.mojang.blaze3d.vertex.VertexConsumer +import net.minecraft.client.Camera +import net.minecraft.client.Minecraft +import net.minecraft.client.multiplayer.ClientLevel +import net.minecraft.client.particle.ParticleProvider +import net.minecraft.client.particle.TextureSheetParticle +import net.minecraft.client.particle.SpriteSet +import net.minecraft.client.renderer.LightTexture +import net.minecraft.core.particles.SimpleParticleType +import net.minecraft.util.Mth +import net.minecraft.world.phys.Vec3 +import org.joml.Quaternionf +import org.joml.Vector3f +import kotlin.math.abs +import kotlin.math.cos +import kotlin.math.max +import kotlin.math.min +import kotlin.math.roundToInt +import kotlin.math.sin +import kotlin.math.sqrt + +private const val STRETCH_RATIO = 5.0f +private const val MIN_HALF_LENGTH = 0.06f + +class VisTrailParticle private constructor( + level: ClientLevel, + x: Double, + y: Double, + z: Double, + private val target: Vec3, + color: Int, + private val phase: Float, + scale: Float, + private val spriteSet: SpriteSet +) : TextureSheetParticle(level, x, y, z, .0, .0, .0) { + private val initialSize: Float + + companion object { + var spriteSet: SpriteSet? = null + + fun spawn(start: Vec3, target: Vec3, color: Int, phase: Int, scale: Float, progress: Float = 0f) { + val mc = Minecraft.getInstance() + val level = mc.level as? ClientLevel ?: return + val spriteSet = spriteSet ?: return + + val spawnPos = start.lerp(target, progress.toDouble()) + val jitter = 0.015 + val x = spawnPos.x + (level.random.nextDouble() - 0.5) * jitter + val y = spawnPos.y + (level.random.nextDouble() - 0.5) * jitter + val z = spawnPos.z + (level.random.nextDouble() - 0.5) * jitter + runCatching { + mc.particleEngine.add(VisTrailParticle(level, x, y, z, target, color, phase.toFloat(), scale, spriteSet)) + } + } + } + + init { + val toTarget = target.subtract(x, y, z) + val distance = toTarget.length().coerceAtLeast(0.001) + val forward = toTarget.normalize() + val tempUp = if (abs(forward.y) > 0.9) Vec3(1.0, 0.0, 0.0) else Vec3(0.0, 1.0, 0.0) + val side = forward.cross(tempUp).normalize() + val up = side.cross(forward).normalize() + + val colorVariance = 0.05f + val red = (color shr 16 and 0xFF) / 255f + val green = (color shr 8 and 0xFF) / 255f + val blue = (color and 0xFF) / 255f + setColor( + red - red * colorVariance + random.nextFloat() * red * colorVariance, + green - green * colorVariance + random.nextFloat() * green * colorVariance, + blue - blue * colorVariance + random.nextFloat() * blue * colorVariance + ) + + alpha = 0.6f + hasPhysics = false + gravity = 0f + lifetime = (distance * 22.0).roundToInt().coerceIn(14, 50) + quadSize = scale * (0.92f + random.nextFloat() * 0.16f) + initialSize = quadSize + pickSprite(spriteSet) + + val swirl1 = sin(phase * 0.43f).toDouble() * 0.006 + val swirl2 = cos(phase * 0.31f).toDouble() * 0.004 + xd = forward.x * 0.04 + side.x * swirl1 + up.x * swirl2 + random.nextGaussian() * 0.0004 + yd = forward.y * 0.04 + side.y * swirl1 + up.y * swirl2 + random.nextGaussian() * 0.0004 + zd = forward.z * 0.04 + side.z * swirl1 + up.z * swirl2 + random.nextGaussian() * 0.0004 + } + + override fun render(buffer: VertexConsumer, camera: Camera, partialTick: Float) { + val px = Mth.lerp(partialTick.toDouble(), xo, x) - camera.position.x + val py = Mth.lerp(partialTick.toDouble(), yo, y) - camera.position.y + val pz = Mth.lerp(partialTick.toDouble(), zo, z) - camera.position.z + + val vel = Vec3(xd, yd, zd) + val speed = vel.length() + val stretchDir = if (speed > 1.0E-6) vel.normalize() else (target.subtract(x, y, z)).let { if (it.lengthSqr() > 1.0E-6) it.normalize() else Vec3(1.0, 0.0, 0.0) } + val camToParticle = Vec3(px, py, pz) + if (camToParticle.lengthSqr() < 1.0E-6) { super.render(buffer, camera, partialTick); return } + val camDir = camToParticle.normalize() + + var sideVec = stretchDir.cross(camDir) + if (sideVec.lengthSqr() < 1.0E-6) { super.render(buffer, camera, partialTick); return } + sideVec = sideVec.normalize() + + val halfWidth = quadSize * 0.28f + val halfLength = max(quadSize * STRETCH_RATIO * 0.5f, MIN_HALF_LENGTH) + + val sx = sideVec.x.toFloat() * halfWidth + val sy = sideVec.y.toFloat() * halfWidth + val sz = sideVec.z.toFloat() * halfWidth + val vx = stretchDir.x.toFloat() * halfLength + val vy = stretchDir.y.toFloat() * halfLength + val vz = stretchDir.z.toFloat() * halfLength + + val cx = px.toFloat() + val cy = py.toFloat() + val cz = pz.toFloat() + + val u0 = getU0() + val u1 = getU1() + val v0 = getV0() + val v1 = getV1() + val light = getLightColor(partialTick) + + buffer.addVertex(cx - sx + vx, cy - sy + vy, cz - sz + vz) + .setUv(u1, v0).setColor(rCol, gCol, bCol, alpha).setLight(light) + buffer.addVertex(cx + sx + vx, cy + sy + vy, cz + sz + vz) + .setUv(u1, v1).setColor(rCol, gCol, bCol, alpha).setLight(light) + buffer.addVertex(cx + sx - vx, cy + sy - vy, cz + sz - vz) + .setUv(u0, v1).setColor(rCol, gCol, bCol, alpha).setLight(light) + buffer.addVertex(cx - sx - vx, cy - sy - vy, cz - sz - vz) + .setUv(u0, v0).setColor(rCol, gCol, bCol, alpha).setLight(light) + } + + override fun tick() { + xo = x + yo = y + zo = z + + if (age++ >= lifetime) { + remove() + return + } + + val dx = target.x - x + val dy = target.y - y + val dz = target.z - z + val distance = sqrt(dx * dx + dy * dy + dz * dz) + if (distance < 0.1) { + remove() + return + } + + val invDistance = 1.0 / distance + val attraction = 0.02 + 0.035 / max(1.0, distance) + xd = Mth.clamp(xd * 0.96 + dx * invDistance * attraction, -0.09, 0.09) + yd = Mth.clamp(yd * 0.96 + dy * invDistance * attraction, -0.09, 0.09) + zd = Mth.clamp(zd * 0.96 + dz * invDistance * attraction, -0.09, 0.09) + move(xd, yd, zd) + + val pulse = 0.94f + 0.08f * sin((age + phase) / 4.5f) + val shrinkNearEnd = Mth.clamp((distance / 1.7).toFloat(), 0.45f, 1.0f) + quadSize = initialSize * pulse * shrinkNearEnd + alpha = 0.3f + 0.5f * min(1f, distance.toFloat()) + } + + override fun getLightColor(partialTick: Float) = LightTexture.FULL_BRIGHT + + override fun getRenderType() = T7ParticleRenderTypes.ETERNAL_FLAME + + class Provider(private val sprites: SpriteSet) : ParticleProvider { + init { + spriteSet = sprites + } + + override fun createParticle(type: SimpleParticleType, level: ClientLevel, x: Double, y: Double, z: Double, xSpeed: Double, ySpeed: Double, zSpeed: Double) = + VisTrailParticle(level, x, y, z, Vec3(x + xSpeed, y + ySpeed, z + zSpeed), 0xFFFFFF, 0f, 0.1f, sprites) + } +} + +object VisTrailParticles { + fun spawnBurst(start: Vec3, target: Vec3, colors: List, seed: Int, count: Int, scale: Float) { + if (colors.isEmpty()) return + val safeCount = count.coerceAtLeast(1) + repeat(safeCount) { index -> + val color = colors[(seed + index).mod(colors.size)] + val progress = if (safeCount == 1) 0f else index.toFloat() / safeCount * 0.28f + VisTrailParticle.spawn(start, target, color, seed * 7 + index * 13, scale, progress) + } + } +} diff --git a/src/main/java/me/alegian/thavma/impl/client/renderer/FlyingAspectsRenderer.kt b/src/main/java/me/alegian/thavma/impl/client/renderer/FlyingAspectsRenderer.kt index e2257239..634d984d 100644 --- a/src/main/java/me/alegian/thavma/impl/client/renderer/FlyingAspectsRenderer.kt +++ b/src/main/java/me/alegian/thavma/impl/client/renderer/FlyingAspectsRenderer.kt @@ -2,6 +2,7 @@ package me.alegian.thavma.impl.client.renderer import com.mojang.blaze3d.vertex.PoseStack import com.mojang.blaze3d.vertex.VertexConsumer +import net.minecraft.client.Minecraft import me.alegian.thavma.impl.client.T7RenderTypes import me.alegian.thavma.impl.client.util.addVertex import me.alegian.thavma.impl.common.infusion.MAIN_AXIS_RESOLUTION @@ -19,64 +20,95 @@ import kotlin.math.* // number of "corners" of every 3d cylinder slice const val CR0SS_AXIS_RESOLUTION = 16 - // the range at which a trajectory point is considered an endpoint const val ENDPOINT_RANGE = 0.3 -// height is how high the "inverse gravity" parabola will rise -private fun trajectory(start: Vec3, end: Vec3, height: Double): List { +private fun trajectory(start: Vec3, end: Vec3, height: Double, ticks: Float): List { val diff = end - start - val dl = diff.normalize() / MAIN_AXIS_RESOLUTION.toDouble() - val trajectoryLength = trajectoryLength(start, end) - return (0..trajectoryLength).map { - val t = it.toDouble() / trajectoryLength // t ranges from 0 to 1 - val quadraticYOffset = Vec3(0.0, 1.0, 0.0) * t * (1 - t) * 4.0 * height - start + dl * it.toDouble() + quadraticYOffset + val length = diff.length() + if (length < 1.0E-4) return listOf(start, end) + + val trajLen = trajectoryLength(start, end).coerceAtLeast(1) + + val forward = diff.normalize() + val tempUp = if (abs(forward.y) > 0.9) Vec3(1.0, 0.0, 0.0) else Vec3(0.0, 1.0, 0.0) + val right = (forward cross tempUp).normalize() + val up = (right cross forward).normalize() + + val seed = abs(start.x * 0.731 + start.y * 0.293 + start.z * 0.517) + val displacementScale = min(length * 0.01, 0.035) + + return (0..trajLen).map { idx -> + val t = idx.toDouble() / trajLen + val base = start + diff * t + val heightOffset = Vec3(0.0, 1.0, 0.0) * t * (1 - t) * 4.0 * height + + val fade = sin(t * PI).pow(1.15) + val time = ticks.toDouble() * 0.006 + + val dx = (sin(t * PI * 1.15 + seed + time) * 0.68 + + sin(t * PI * 0.55 + seed * 1.7 - time * 0.4) * 0.32) * fade * displacementScale + val dy = cos(t * PI * 0.9 + seed * 0.9 + time * 0.45) * fade * displacementScale * 0.2 + + base + heightOffset + right * dx + up * dy } } -fun renderFlyingAspects(start: Vec3, end: Vec3, trajectoryHeight: Double, headIndex: Int, length: Int, poseStack: PoseStack, multiBufferSource: MultiBufferSource, ticks: Float, color: Int, radius: Double) { +fun renderFlyingAspects(start: Vec3, end: Vec3, trajectoryHeight: Double, headIndex: Int, length: Int, poseStack: PoseStack, multiBufferSource: MultiBufferSource, ticks: Float, colors: List, radius: Double) { val vc = multiBufferSource.getBuffer(T7RenderTypes.FLYING_ASPECTS) - val traj = trajectory(start, end, trajectoryHeight) + val traj = trajectory(start, end, trajectoryHeight, ticks) + val sub = traj.subList(max(0, headIndex - length), headIndex + 1) + if (sub.size < 2) return + + renderGradientRibbon(sub, vc, poseStack, ticks, colors, radius) - renderVariableRadiusCylinder(traj.subList(max(0, headIndex - length), headIndex + 1), vc, poseStack, ticks, color, radius) + val coreColors = colors.map { + val rgb = brightenRGB(it and 0x00FFFFFF, 0.6f) + val a = min(((it ushr 24) and 0xFF) * 5 / 4, 255) + (a shl 24) or rgb + } + renderGradientRibbon(sub, vc, poseStack, ticks, coreColors, radius * 0.35) } -private fun renderVariableRadiusCylinder(trajectory: List, vc: VertexConsumer, poseStack: PoseStack, ticks: Float, color: Int, baseRadius: Double) { - // we keep track of the previous normals to fix open ends in the cylinder, in non-linear trajectories - var prevNormal1 = Vec3(0.0, 1.0, 0.0) - var prevNormal2 = Vec3(1.0, 0.0, 0.0) +private fun renderGradientRibbon(trajectory: List, vc: VertexConsumer, poseStack: PoseStack, ticks: Float, colors: List, baseRadius: Double) { + var prevSide = Vec3(1.0, 0.0, 0.0) + val segCount = trajectory.size - 1 + val cameraPos = Minecraft.getInstance().gameRenderer.mainCamera.position - for (i in 0 until trajectory.size - 1) { + for (i in 0 until segCount) { val currentPoint = trajectory[i] val nextPoint = trajectory[i + 1] val direction = nextPoint - currentPoint - val randomOtherDirection = direction - Vec3(0.0, 1.0, 0.0) - val normal1 = (direction cross randomOtherDirection).normalize() - val normal2 = (direction cross normal1).normalize() + val forward = direction.normalize() + + val toCurrentCamera = cameraPos - currentPoint + var currentSide = if (toCurrentCamera.lengthSqr() < 1.0E-6) prevSide else forward cross toCurrentCamera.normalize() + if (currentSide.lengthSqr() < 1.0E-6) currentSide = prevSide + else currentSide = currentSide.normalize() + if (currentSide.dot(prevSide) < 0) currentSide = currentSide.scale(-1.0) + + val toNextCamera = cameraPos - nextPoint + var nextSide = if (toNextCamera.lengthSqr() < 1.0E-6) currentSide else forward cross toNextCamera.normalize() + if (nextSide.lengthSqr() < 1.0E-6) nextSide = currentSide + else nextSide = nextSide.normalize() + if (nextSide.dot(currentSide) < 0) nextSide = nextSide.scale(-1.0) val radius = oscillatingRadius(i, trajectory, ticks, baseRadius) val nextRadius = oscillatingRadius(i + 1, trajectory, ticks, baseRadius) - for (j in 0..CR0SS_AXIS_RESOLUTION - 1) { - val angle = 2 * PI * j / CR0SS_AXIS_RESOLUTION - val nextAngle = 2 * PI * (j + 1) / CR0SS_AXIS_RESOLUTION + val t = i.toFloat() / segCount + val tNext = (i + 1).toFloat() / segCount - val offset1 = prevNormal1 * cos(angle) + prevNormal2 * sin(angle) - val offset2 = prevNormal1 * cos(nextAngle) + prevNormal2 * sin(nextAngle) - val offset3 = normal1 * cos(nextAngle) + normal2 * sin(nextAngle) - val offset4 = normal1 * cos(angle) + normal2 * sin(angle) + val color1 = modulateAlpha(sampleColor(colors, t, ticks * 0.0015f), smoothstep(t.toDouble())) + val color2 = modulateAlpha(sampleColor(colors, tNext, ticks * 0.0015f), smoothstep(tNext.toDouble())) - // the first vertices use the previous normals, to avoid open ends - vc.addVertex(poseStack, currentPoint + offset1 * radius).setColor(color) - vc.addVertex(poseStack, currentPoint + offset2 * radius).setColor(color) - vc.addVertex(poseStack, nextPoint + offset3 * nextRadius).setColor(color) - vc.addVertex(poseStack, nextPoint + offset4 * nextRadius).setColor(color) - } + vc.addVertex(poseStack, currentPoint - currentSide * radius).setColor(color1) + vc.addVertex(poseStack, currentPoint + currentSide * radius).setColor(color1) + vc.addVertex(poseStack, nextPoint + nextSide * nextRadius).setColor(color2) + vc.addVertex(poseStack, nextPoint - nextSide * nextRadius).setColor(color2) - prevNormal1 = normal1 - prevNormal2 = normal2 + prevSide = (currentSide * 0.35 + nextSide * 0.65).normalize() } } @@ -85,12 +117,44 @@ private fun renderVariableRadiusCylinder(trajectory: List, vc: VertexConsu * Endpoint radii are multiplied with an extra term to avoid open ends. */ private fun oscillatingRadius(i: Int, trajectory: List, ticks: Float, scale: Double): Double { - val timePhase = -1.5 * 2 * PI * ticks / 20 // minus makes it look like start is being sucked into end - val default = scale + 0.02 * sin(i * 2 * PI / MAIN_AXIS_RESOLUTION + timePhase) - val x = trajectory[i] val distanceToEndpoint = min(x.distanceTo(trajectory.first()), x.distanceTo(trajectory.last())) val linearFade = (distanceToEndpoint / ENDPOINT_RANGE).coerceIn(0.0, 1.0) - if (distanceToEndpoint < 0.3) return default * smoothstep(linearFade) - return default + val endpointFade = if (distanceToEndpoint < ENDPOINT_RANGE) smoothstep(linearFade) else 1.0 + val timePhase = ticks * 0.008f + i * 0.012f + val breathing = 0.985 + 0.015 * sin(timePhase) + return scale * endpointFade * breathing +} + +private fun sampleColor(colors: List, t: Float, timeOffset: Float): Int { + if (colors.size == 1) return colors[0] + val n = colors.size + val phase = t * n + timeOffset + val idx = floor(phase).toInt().mod(n) + val nextIdx = (idx + 1).mod(n) + val blend = phase - floor(phase) + return lerpColor(colors[idx], colors[nextIdx], blend) +} + +private fun modulateAlpha(color: Int, alphaMul: Double): Int { + val a = (color ushr 24) and 0xFF + val modA = (a * (0.35 + 0.65 * alphaMul)).toInt().coerceIn(0, 255) + return (modA shl 24) or (color and 0x00FFFFFF) +} + +private fun lerpColor(c1: Int, c2: Int, t: Float): Int { + val a = (((c1 ushr 24) and 0xFF) + (((c2 ushr 24) and 0xFF) - ((c1 ushr 24) and 0xFF)) * t).toInt().coerceIn(0, 255) + val r = (((c1 shr 16) and 0xFF) + (((c2 shr 16) and 0xFF) - ((c1 shr 16) and 0xFF)) * t).toInt().coerceIn(0, 255) + val g = (((c1 shr 8) and 0xFF) + (((c2 shr 8) and 0xFF) - ((c1 shr 8) and 0xFF)) * t).toInt().coerceIn(0, 255) + val b = ((c1 and 0xFF) + ((c2 and 0xFF) - (c1 and 0xFF)) * t).toInt().coerceIn(0, 255) + return (a shl 24) or (r shl 16) or (g shl 8) or b +} + +private fun brightenRGB(rgb: Int, factor: Float): Int { + val r = (rgb shr 16) and 0xFF + val g = (rgb shr 8) and 0xFF + val b = rgb and 0xFF + return ((r + (255 - r) * factor).toInt().coerceIn(0, 255) shl 16) or + ((g + (255 - g) * factor).toInt().coerceIn(0, 255) shl 8) or + (b + (255 - b) * factor).toInt().coerceIn(0, 255) } diff --git a/src/main/java/me/alegian/thavma/impl/client/renderer/blockentity/AuraNodeBER.kt b/src/main/java/me/alegian/thavma/impl/client/renderer/blockentity/AuraNodeBER.kt index e07e1104..3d778c84 100644 --- a/src/main/java/me/alegian/thavma/impl/client/renderer/blockentity/AuraNodeBER.kt +++ b/src/main/java/me/alegian/thavma/impl/client/renderer/blockentity/AuraNodeBER.kt @@ -78,15 +78,17 @@ class AuraNodeBER : BlockEntityRenderer { fun renderNode(aspectMap: AspectMap, poseStack: PoseStack, bufferSource: MultiBufferSource) { poseStack.use { - var alpha = 0.1f + var alpha = 0.15f if (clientPlayerHasRevealing()) alpha = MAX_ALPHA - // empty nodes look like small black circles - aspectMap.toSortedList().run { - if (this.isNotEmpty()) - for (stack in this) - renderAuraNodeLayer(poseStack, bufferSource, stack.aspect.color, alpha, stack.amount / 2f / AuraNodeBE.MAX_ASPECTS) - else renderAuraNodeLayer(poseStack, bufferSource, 0, alpha / MAX_ALPHA, 1f / AuraNodeBE.MAX_ASPECTS) + val sorted = aspectMap.toSortedList() + if (sorted.isNotEmpty()) { + for (stack in sorted.reversed()) + renderAuraNodeLayer(poseStack, bufferSource, stack.aspect.color, alpha, stack.amount / 2f / AuraNodeBE.MAX_ASPECTS + 0.08f) + + renderAuraNodeLayer(poseStack, bufferSource, 0xFFFFFF, alpha * 0.7f, 0.06f) + } else { + renderAuraNodeLayer(poseStack, bufferSource, 0, alpha / MAX_ALPHA, 1f / AuraNodeBE.MAX_ASPECTS) } } } diff --git a/src/main/java/me/alegian/thavma/impl/client/renderer/blockentity/MatrixBER.kt b/src/main/java/me/alegian/thavma/impl/client/renderer/blockentity/MatrixBER.kt index 960570ac..622adbce 100644 --- a/src/main/java/me/alegian/thavma/impl/client/renderer/blockentity/MatrixBER.kt +++ b/src/main/java/me/alegian/thavma/impl/client/renderer/blockentity/MatrixBER.kt @@ -2,9 +2,9 @@ package me.alegian.thavma.impl.client.renderer.blockentity import com.mojang.blaze3d.vertex.PoseStack import com.mojang.blaze3d.vertex.VertexConsumer +import me.alegian.thavma.impl.client.particle.VisTrailParticles import me.alegian.thavma.impl.client.renderer.geo.layer.EmissiveGeoLayer import me.alegian.thavma.impl.client.renderer.geo.layer.ItemRenderLayer -import me.alegian.thavma.impl.client.renderer.renderFlyingAspects import me.alegian.thavma.impl.client.util.translate import me.alegian.thavma.impl.common.block.entity.MatrixBE import me.alegian.thavma.impl.common.infusion.trajectoryLength @@ -42,6 +42,9 @@ private class FlyingAspectsRenderLayer(renderer: MatrixBER) : GeoRenderLayer 20) 3 else 2 + VisTrailParticles.spawnBurst(f.key, be.drainPos, listOf(aspect.color and 0x00FFFFFF), ticks.toInt() + firstIndex, count, 0.078f) } } } } + +private val matrixParticleTicks = mutableMapOf() diff --git a/src/main/java/me/alegian/thavma/impl/client/renderer/entity/VisER.kt b/src/main/java/me/alegian/thavma/impl/client/renderer/entity/VisER.kt index d2bbe934..76247a38 100644 --- a/src/main/java/me/alegian/thavma/impl/client/renderer/entity/VisER.kt +++ b/src/main/java/me/alegian/thavma/impl/client/renderer/entity/VisER.kt @@ -1,14 +1,11 @@ package me.alegian.thavma.impl.client.renderer.entity import com.mojang.blaze3d.vertex.PoseStack -import me.alegian.thavma.impl.client.T7Colors -import me.alegian.thavma.impl.client.renderer.renderFlyingAspects -import me.alegian.thavma.impl.client.util.translate +import me.alegian.thavma.impl.client.particle.VisTrailParticles +import me.alegian.thavma.impl.common.data.capability.AspectContainer +import me.alegian.thavma.impl.init.registries.deferred.Aspects import me.alegian.thavma.impl.common.entity.VisEntity -import me.alegian.thavma.impl.common.infusion.trajectoryLength -import me.alegian.thavma.impl.common.util.unaryMinus import me.alegian.thavma.impl.common.util.plus -import me.alegian.thavma.impl.common.util.use import net.minecraft.client.Minecraft import net.minecraft.client.renderer.MultiBufferSource import net.minecraft.client.renderer.culling.Frustum @@ -23,14 +20,11 @@ import net.minecraft.world.phys.Vec3 class VisER(pContext: EntityRendererProvider.Context) : EntityRenderer(pContext) { override fun render(visEntity: VisEntity, pEntityYaw: Float, pPartialTick: Float, poseStack: PoseStack, pBufferSource: MultiBufferSource, pPackedLight: Int) { val player = visEntity.player ?: return - - poseStack.use { - translate(-visEntity.position()) // we are inside an entity renderer - val playerHandPos = preparePlayerHandPosition(pPartialTick, player) - val length = trajectoryLength(visEntity.position(), playerHandPos) - val colorWithAlpha = 0x44000000 or (T7Colors.PURPLE and 0xffffff) - renderFlyingAspects(visEntity.position(), playerHandPos, 0.2, length - 1, length, this, pBufferSource, visEntity.tickCount + pPartialTick, colorWithAlpha, 0.06) - } + val playerHandPos = preparePlayerHandPosition(pPartialTick, player) + val colors = AspectContainer.at(visEntity.level(), visEntity.blockPosition())?.aspects?.toSortedList()?.map { it.aspect.color and 0x00FFFFFF } + ?.takeIf { it.isNotEmpty() } + ?: Aspects.PRIMAL_ASPECTS.map { it.get().color and 0x00FFFFFF } + spawnTrailParticles(visEntity, playerHandPos, colors) } override fun getTextureLocation(pEntity: VisEntity) = @@ -44,6 +38,23 @@ class VisER(pContext: EntityRendererProvider.Context) : EntityRenderer() + +private fun spawnTrailParticles(visEntity: VisEntity, target: Vec3, colors: List) { + val tick = visEntity.tickCount + if (visParticleTicks[visEntity.id] == tick) return + visParticleTicks[visEntity.id] = tick + + val start = visEntity.position() + val distance = start.distanceTo(target) + val count = when { + distance < 1.5 -> 2 + distance < 3.0 -> 3 + else -> 4 + } + VisTrailParticles.spawnBurst(start, target, colors, tick + visEntity.id, count, 0.095f) +} + /** * Prepare a Vec3 position of the Player's hand. This is only an approximation, * and it does not follow the PlayerModel's animations, because using these @@ -56,11 +67,13 @@ private fun preparePlayerHandPosition(pPartialTick: Float, player: Player): Vec3 // for first person, if it is the client player, we follow the camera if (player === Minecraft.getInstance().player && Minecraft.getInstance().options.cameraType.isFirstPerson) { + val cameraPos = Minecraft.getInstance().gameRenderer.mainCamera.position + val view = player.getViewVector(pPartialTick).normalize().scale(.45) val angle = Math.PI / 2 - player.getViewYRot(pPartialTick) / 360f * 2 * Math.PI - val translation = player.getViewVector(pPartialTick).normalize().scale(.1) - position += Vec3(0.0, player.eyeHeight + 0.01, 0.0) - position += translation - val horizontalOffset = Vec3(0.0, 0.0, (if (arm == HumanoidArm.RIGHT) -.06f else .06f).toDouble()) + position = cameraPos + position += view + position += Vec3(0.0, -0.16, 0.0) + val horizontalOffset = Vec3(0.0, 0.0, (if (arm == HumanoidArm.RIGHT) -.14f else .14f).toDouble()) position += horizontalOffset.yRot(angle.toFloat()) } else { // for third person, we follow body rotation val angle = Math.PI / 2 - player.getPreciseBodyRotation(pPartialTick) / 360f * 2 * Math.PI diff --git a/src/main/java/me/alegian/thavma/impl/init/data/providers/T7ParticleDescriptionProvider.kt b/src/main/java/me/alegian/thavma/impl/init/data/providers/T7ParticleDescriptionProvider.kt index 15d5128d..da8fef6e 100644 --- a/src/main/java/me/alegian/thavma/impl/init/data/providers/T7ParticleDescriptionProvider.kt +++ b/src/main/java/me/alegian/thavma/impl/init/data/providers/T7ParticleDescriptionProvider.kt @@ -3,6 +3,7 @@ package me.alegian.thavma.impl.init.data.providers import me.alegian.thavma.impl.init.registries.deferred.T7ParticleTypes.CRUCIBLE_BUBBLE import me.alegian.thavma.impl.init.registries.deferred.T7ParticleTypes.ETERNAL_FLAME import me.alegian.thavma.impl.init.registries.deferred.T7ParticleTypes.INFUSION_RUNE +import me.alegian.thavma.impl.init.registries.deferred.T7ParticleTypes.VIS_TRAIL import me.alegian.thavma.impl.rl import net.minecraft.data.PackOutput import net.minecraft.resources.ResourceLocation @@ -13,6 +14,7 @@ class T7ParticleDescriptionProvider(output: PackOutput, fileHelper: ExistingFile override fun addDescriptions() { spriteSet(CRUCIBLE_BUBBLE.get(), ResourceLocation.withDefaultNamespace("bubble")) spriteSet(ETERNAL_FLAME.get(), rl("eternal_flame/red"), rl("eternal_flame/orange"), rl("eternal_flame/yellow")) + spriteSet(VIS_TRAIL.get(), rl("eternal_flame/yellow")) spriteSet(INFUSION_RUNE.get(), (0..23).map { rl("infusion/$it") }) } } diff --git a/src/main/java/me/alegian/thavma/impl/init/registries/deferred/T7ParticleTypes.kt b/src/main/java/me/alegian/thavma/impl/init/registries/deferred/T7ParticleTypes.kt index 6f790254..aff16fbd 100644 --- a/src/main/java/me/alegian/thavma/impl/init/registries/deferred/T7ParticleTypes.kt +++ b/src/main/java/me/alegian/thavma/impl/init/registries/deferred/T7ParticleTypes.kt @@ -12,6 +12,7 @@ object T7ParticleTypes { val CRUCIBLE_BUBBLE = REGISTRAR.register("crucible_bubble") { -> SimpleParticleType(true) } val ETERNAL_FLAME = REGISTRAR.register("eternal_flame") { -> SimpleParticleType(true) } + val VIS_TRAIL = REGISTRAR.register("vis_trail") { -> SimpleParticleType(true) } val INFUSION_RUNE = REGISTRAR.register("infusion_rune") { -> SimpleParticleType(true) } val INFUSION_ITEM = REGISTRAR.register("infusion_item") { -> object : ParticleType(false) { diff --git a/src/main/resources/assets/thavma/particles/vis_trail.json b/src/main/resources/assets/thavma/particles/vis_trail.json new file mode 100644 index 00000000..6650e9a8 --- /dev/null +++ b/src/main/resources/assets/thavma/particles/vis_trail.json @@ -0,0 +1,5 @@ +{ + "textures": [ + "thavma:eternal_flame/yellow" + ] +} diff --git a/src/main/resources/assets/thavma/shaders/core/aura_node.fsh b/src/main/resources/assets/thavma/shaders/core/aura_node.fsh index 3c5096dc..4a88c28f 100644 --- a/src/main/resources/assets/thavma/shaders/core/aura_node.fsh +++ b/src/main/resources/assets/thavma/shaders/core/aura_node.fsh @@ -35,15 +35,68 @@ float noise(vec2 st) { dot(random2(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0)), u.x), u.y); } +float fbm(vec2 st) { + float value = 0.0; + float amplitude = 0.5; + for (int i = 0; i < 4; i++) { + value += amplitude * noise(st); + st *= 2.1; + amplitude *= 0.5; + } + return value; +} + void main() { vec3 diff = fragCenter - fragPosition; vec3 maxDiff = fragCenter - cornerPosition; + float maxLen = length(maxDiff); + float dist = length(diff); + float safeScale = max(scale, 0.001); + float scaledDist = dist / (maxLen * safeScale); + + if (scaledDist > 1.15) discard; + + float time = GameTime * 1200.0; + float phase = random(scale + 0.1) * 6.28318; + vec4 cameraDiff = ModelViewMat * vec4(diff, 1.0); float angle = atan(cameraDiff.y, cameraDiff.x); - float phase = random(scale) * 2 * 3.14159; - // wavy patterns only for nodes with a decent radius - if (length(diff) > scale * length(maxDiff) * (1 - 0.2 * (sin(angle * 32 + phase) + 1)) && scale > 0.2) discard; - // smaller ones are circular - if (length(diff) > scale * length(maxDiff)) discard; - fragColor = vertexColor; + + // --- organic edge distortion --- + float n1 = noise(vec2(angle * 1.5 + time * 0.25, phase + time * 0.1)) * 0.15; + float n2 = noise(vec2(angle * 3.0 - time * 0.4, phase + 7.0)) * 0.08; + float n3 = noise(vec2(angle * 6.0 + time * 0.6, phase + 15.0)) * 0.04; + float edgeDistortion = n1 + n2 + n3; + + float pulse = sin(time * 0.7 + phase) * 0.04 + sin(time * 1.3 + phase * 2.0) * 0.02; + + float edgeThreshold = 1.0 - edgeDistortion - pulse; + + if (scaledDist > edgeThreshold && safeScale > 0.12) discard; + if (scaledDist > 1.0) discard; + + // --- swirling internal texture --- + float swirlAngle = angle + scaledDist * 2.0 - time * 0.15; + vec2 swirlCoord = vec2(swirlAngle * 2.0 / 6.28318, scaledDist * 3.0 + time * 0.08); + float swirl = fbm(swirlCoord + vec2(phase, phase * 0.7)) * 0.35 + 0.65; + + // --- radial gradient --- + float gradient = 1.0 - pow(scaledDist, 1.2); + + // --- inner glow --- + float innerGlow = exp(-scaledDist * 5.0); + + // --- soft edge fadeout --- + float edgeFade = 1.0 - smoothstep(0.55, min(edgeThreshold, 1.0), scaledDist); + + // --- color composition --- + vec3 baseColor = vertexColor.rgb; + vec3 brightened = mix(baseColor, vec3(1.0), innerGlow * 0.65); + brightened *= swirl; + brightened += vec3(innerGlow * 0.25); + + float alpha = vertexColor.a * gradient * edgeFade; + alpha = min(alpha + innerGlow * vertexColor.a * 0.4, vertexColor.a * 1.2); + + fragColor = vec4(brightened, alpha); }