diff --git a/unstable/layoutdsl/build.gradle.kts b/unstable/layoutdsl/build.gradle.kts index dd6d03de..2b44504c 100644 --- a/unstable/layoutdsl/build.gradle.kts +++ b/unstable/layoutdsl/build.gradle.kts @@ -21,6 +21,8 @@ dependencies { compileOnly(libs.versions.universalcraft.map { "gg.essential:universalcraft-1.8.9-forge:$it" }) { attributes { attribute(common, true) } } + // Depending on LWJGL3 instead of 2 so we can choose opengl bindings only + compileOnly("org.lwjgl:lwjgl-opengl:3.3.1") } tasks.compileKotlin.setJvmDefault("all") diff --git a/unstable/layoutdsl/src/main/kotlin/gg/essential/elementa/effects/AlphaEffect.kt b/unstable/layoutdsl/src/main/kotlin/gg/essential/elementa/effects/AlphaEffect.kt new file mode 100644 index 00000000..3c1985a2 --- /dev/null +++ b/unstable/layoutdsl/src/main/kotlin/gg/essential/elementa/effects/AlphaEffect.kt @@ -0,0 +1,187 @@ +package gg.essential.elementa.effects + +import gg.essential.elementa.effects.Effect +import gg.essential.elementa.state.State +import gg.essential.universal.UGraphics +import gg.essential.universal.UMatrixStack +import gg.essential.universal.UResolution +import gg.essential.universal.shader.BlendState +import gg.essential.universal.shader.SamplerUniform +import gg.essential.universal.shader.UShader +import org.lwjgl.opengl.GL11 +import java.io.Closeable +import java.lang.ref.PhantomReference +import java.lang.ref.ReferenceQueue +import java.nio.ByteBuffer +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +/** + * Applies an alpha value to a component. This is done by snapshotting the framebuffer behind the component, + * rendering the component, then rendering the snapshot with the inverse of the desired alpha. + */ +class AlphaEffect(private val alphaState: State) : Effect() { + private val resources = Resources(this) + private var textureWidth = -1 + private var textureHeight = -1 + + override fun setup() { + initShader() + Resources.drainCleanupQueue() + resources.textureId = GL11.glGenTextures() + } + + override fun beforeDraw(matrixStack: UMatrixStack) { + if (resources.textureId == -1) error("AlphaEffect has not yet been setup or has already been cleaned up! ElementaVersion.V4 or newer is required for proper operation!") + + val scale = UResolution.scaleFactor + + // Get the coordinates of the component within the bounds of the screen in real pixels + val left = (boundComponent.getLeft() * scale).toInt().coerceIn(0..UResolution.viewportWidth) + val right = (boundComponent.getRight() * scale).toInt().coerceIn(0..UResolution.viewportWidth) + val top = (boundComponent.getTop() * scale).toInt().coerceIn(0..UResolution.viewportHeight) + val bottom = (boundComponent.getBottom() * scale).toInt().coerceIn(0..UResolution.viewportHeight) + + val x = left + val y = UResolution.viewportHeight - bottom // OpenGL screen coordinates start in the bottom left + val width = right - left + val height = bottom - top + + if (width == 0 || height == 0 || !shader.usable) { + return + } + + UGraphics.configureTexture(resources.textureId) { + if (width != textureWidth || height != textureHeight) { + GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, width, height, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, null as ByteBuffer?) + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST) + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST) + textureWidth = width + textureHeight = height + } + + GL11.glCopyTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, x, y, width, height) + } + } + + override fun afterDraw(matrixStack: UMatrixStack) { + // Get the coordinates of the component within the bounds of the screen in fractional MC pixels + val left = boundComponent.getLeft().toDouble().coerceIn(0.0..UResolution.viewportWidth / UResolution.scaleFactor) + val right = boundComponent.getRight().toDouble().coerceIn(0.0..UResolution.viewportWidth / UResolution.scaleFactor) + val top = boundComponent.getTop().toDouble().coerceIn(0.0..UResolution.viewportHeight / UResolution.scaleFactor) + val bottom = boundComponent.getBottom().toDouble().coerceIn(0.0..UResolution.viewportHeight / UResolution.scaleFactor) + + val x = left + val y = top + val width = right - left + val height = bottom - top + + if (width == 0.0 || height == 0.0 || !shader.usable) { + return + } + + val red = 1f + val green = 1f + val blue = 1f + val alpha = 1f - alphaState.get() + + var prevAlphaTestFunc = 0 + var prevAlphaTestRef = 0f + if (!UGraphics.isCoreProfile()) { + prevAlphaTestFunc = GL11.glGetInteger(GL11.GL_ALPHA_TEST_FUNC) + prevAlphaTestRef = GL11.glGetFloat(GL11.GL_ALPHA_TEST_REF) + UGraphics.alphaFunc(GL11.GL_ALWAYS, 0f) + } + + shader.bind() + textureUniform.setValue(resources.textureId) + + val worldRenderer = UGraphics.getFromTessellator() + worldRenderer.beginWithActiveShader(UGraphics.DrawMode.QUADS, UGraphics.CommonVertexFormats.POSITION_TEXTURE_COLOR) + worldRenderer.pos(matrixStack, x, y + height, 0.0).tex(0.0, 0.0).color(red, green, blue, alpha).endVertex() + worldRenderer.pos(matrixStack, x + width, y + height, 0.0).tex(1.0, 0.0).color(red, green, blue, alpha).endVertex() + worldRenderer.pos(matrixStack, x + width, y, 0.0).tex(1.0, 1.0).color(red, green, blue, alpha).endVertex() + worldRenderer.pos(matrixStack, x, y, 0.0).tex(0.0, 1.0).color(red, green, blue, alpha).endVertex() + worldRenderer.drawDirect() + + shader.unbind() + + if (!UGraphics.isCoreProfile()) { + UGraphics.alphaFunc(prevAlphaTestFunc, prevAlphaTestRef) + } + } + + fun cleanup() { + resources.close() + } + + private class Resources(effect: AlphaEffect) : PhantomReference(effect, referenceQueue), Closeable { + var textureId = -1 + + init { + toBeCleanedUp.add(this) + } + + override fun close() { + toBeCleanedUp.remove(this) + + if (textureId != -1) { + GL11.glDeleteTextures(textureId) + textureId = -1 + } + } + + companion object { + val referenceQueue = ReferenceQueue() + val toBeCleanedUp: MutableSet = Collections.newSetFromMap(ConcurrentHashMap()) + + fun drainCleanupQueue() { + while (true) { + ((referenceQueue.poll() ?: break) as Resources).close() + } + } + } + } + + companion object { + private lateinit var shader: UShader + private lateinit var textureUniform: SamplerUniform + + private fun initShader() { + if (::shader.isInitialized) return + + shader = UShader.fromLegacyShader(""" + #version 110 + + varying vec2 f_Position; + varying vec2 f_TexCoord; + + void main() { + f_Position = gl_Vertex.xy; + f_TexCoord = gl_MultiTexCoord0.st; + + gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; + gl_FrontColor = gl_Color; + } + """.trimIndent(), """ + #version 110 + + uniform sampler2D u_Texture; + + varying vec2 f_Position; + varying vec2 f_TexCoord; + + void main() { + gl_FragColor = gl_Color * vec4(texture2D(u_Texture, f_TexCoord).rgb, 1.0); + } + """.trimIndent(), BlendState.NORMAL, UGraphics.CommonVertexFormats.POSITION_TEXTURE_COLOR) + + if (!shader.usable) { + println("Failed to load AlphaEffect shader") + return + } + + textureUniform = shader.getSamplerUniform("u_Texture") + } + } +} \ No newline at end of file diff --git a/unstable/layoutdsl/src/main/kotlin/gg/essential/elementa/effects/GradientEffect.kt b/unstable/layoutdsl/src/main/kotlin/gg/essential/elementa/effects/GradientEffect.kt new file mode 100644 index 00000000..e1e25030 --- /dev/null +++ b/unstable/layoutdsl/src/main/kotlin/gg/essential/elementa/effects/GradientEffect.kt @@ -0,0 +1,110 @@ +package gg.essential.elementa.effects + +import gg.essential.elementa.effects.Effect +import gg.essential.elementa.state.v2.State +import gg.essential.universal.UGraphics +import gg.essential.universal.UMatrixStack +import gg.essential.universal.shader.BlendState +import gg.essential.universal.shader.UShader +import org.intellij.lang.annotations.Language +import org.lwjgl.opengl.GL11 +import java.awt.Color + +/** + * Draws a gradient (smooth color transition) behind the bound component. + * + * Unlike [gg.essential.elementa.components.GradientComponent], this effect also applies dithering to the gradient to + * mitigate color banding artifacts. + * + * Note: The behavior of non-axis-aligned gradients (e.g. more than two colors, or diagonal) is currently undefined. + */ +class GradientEffect( + private val topLeft: State, + private val topRight: State, + private val bottomLeft: State, + private val bottomRight: State, +) : Effect() { + override fun beforeChildrenDraw(matrixStack: UMatrixStack) { + val topLeft = this.topLeft.get() + val topRight = this.topRight.get() + val bottomLeft = this.bottomLeft.get() + val bottomRight = this.bottomRight.get() + + val dither = topLeft != topRight || topLeft != bottomLeft || bottomLeft != bottomRight + if (dither) { + shader.bind() + } + + val buffer = UGraphics.getFromTessellator() + if (dither) { + buffer.beginWithActiveShader(UGraphics.DrawMode.QUADS, UGraphics.CommonVertexFormats.POSITION_COLOR) + } else { + buffer.beginWithDefaultShader(UGraphics.DrawMode.QUADS, UGraphics.CommonVertexFormats.POSITION_COLOR) + } + + val x1 = boundComponent.getLeft().toDouble() + val x2 = boundComponent.getRight().toDouble() + val y1 = boundComponent.getTop().toDouble() + val y2 = boundComponent.getBottom().toDouble() + + buffer.pos(matrixStack, x2, y1, 0.0).color(topRight).endVertex() + buffer.pos(matrixStack, x1, y1, 0.0).color(topLeft).endVertex() + buffer.pos(matrixStack, x1, y2, 0.0).color(bottomLeft).endVertex() + buffer.pos(matrixStack, x2, y2, 0.0).color(bottomRight).endVertex() + + var prevAlphaTestFunc = 0 + var prevAlphaTestRef = 0f + if (!UGraphics.isCoreProfile()) { + prevAlphaTestFunc = GL11.glGetInteger(GL11.GL_ALPHA_TEST_FUNC) + prevAlphaTestRef = GL11.glGetFloat(GL11.GL_ALPHA_TEST_REF) + UGraphics.alphaFunc(GL11.GL_ALWAYS, 0f) + } + + // See UIBlock.drawBlock for why we use this depth function + UGraphics.enableDepth() + UGraphics.depthFunc(GL11.GL_ALWAYS) + buffer.drawDirect() + UGraphics.disableDepth() + UGraphics.depthFunc(GL11.GL_LEQUAL) + + if (!UGraphics.isCoreProfile()) { + UGraphics.alphaFunc(prevAlphaTestFunc, prevAlphaTestRef) + } + + if (dither) { + shader.unbind() + } + } + + companion object { + @Language("GLSL") + private val vertSource = """ + varying vec4 vColor; + + void main() { + gl_Position = gl_ProjectionMatrix * gl_ModelViewMatrix * gl_Vertex; + vColor = gl_Color; + } + """.trimIndent() + + @Language("GLSL") + private val fragSource = """ + varying vec4 vColor; + + void main() { + // Generate four pseudo-random values in range [-0.5; 0.5] for the current fragment coords, based on + // Vlachos 2016, "Advanced VR Rendering" + vec4 noise = vec4(dot(vec2(171.0, 231.0), gl_FragCoord.xy)); + noise = fract(noise / vec4(103.0, 71.0, 97.0, 127.0)) - 0.5; + + // Apply dithering, i.e. randomly offset all the values within a color band, so there are no harsh + // edges between different bands after quantization. + gl_FragColor = vColor + noise / 255.0; + } + """.trimIndent() + + private val shader: UShader by lazy { + UShader.fromLegacyShader(vertSource, fragSource, BlendState.NORMAL, UGraphics.CommonVertexFormats.POSITION_COLOR) + } + } +} diff --git a/unstable/layoutdsl/src/main/kotlin/gg/essential/elementa/layoutdsl/gradient.kt b/unstable/layoutdsl/src/main/kotlin/gg/essential/elementa/layoutdsl/gradient.kt new file mode 100644 index 00000000..887c7a5e --- /dev/null +++ b/unstable/layoutdsl/src/main/kotlin/gg/essential/elementa/layoutdsl/gradient.kt @@ -0,0 +1,32 @@ +package gg.essential.elementa.layoutdsl + +import gg.essential.elementa.effects.Effect +import gg.essential.elementa.effects.GradientEffect +import gg.essential.elementa.state.v2.State +import gg.essential.elementa.state.v2.stateOf +import java.awt.Color + +fun Modifier.gradient(top: Color, bottom: Color, _desc: GradientVertDesc = GradientDesc) = gradient(stateOf(top), stateOf(bottom), _desc) +fun Modifier.gradient(left: Color, right: Color, _desc: GradientHorzDesc = GradientDesc) = gradient(stateOf(left), stateOf(right), _desc) + +fun Modifier.gradient(top: State, bottom: State, _desc: GradientVertDesc = GradientDesc) = gradient(top, top, bottom, bottom) +fun Modifier.gradient(left: State, right: State, _desc: GradientHorzDesc = GradientDesc) = gradient(left, right, left, right) + +sealed interface GradientVertDesc +sealed interface GradientHorzDesc +private object GradientDesc : GradientVertDesc, GradientHorzDesc + +fun Modifier.gradient( + topLeft: State, + topRight: State, + bottomLeft: State, + bottomRight: State, +) = effect { GradientEffect(topLeft, topRight, bottomLeft, bottomRight) } + +private fun Modifier.effect(effect: () -> Effect) = this then { + val instance = effect() + enableEffect(instance) + return@then { + removeEffect(instance) + } +} diff --git a/unstable/layoutdsl/src/main/kotlin/gg/essential/elementa/transitions/FadeInTransition.kt b/unstable/layoutdsl/src/main/kotlin/gg/essential/elementa/transitions/FadeInTransition.kt new file mode 100644 index 00000000..9768289b --- /dev/null +++ b/unstable/layoutdsl/src/main/kotlin/gg/essential/elementa/transitions/FadeInTransition.kt @@ -0,0 +1,41 @@ +package gg.essential.elementa.transitions + +import gg.essential.elementa.constraints.animation.AnimatingConstraints +import gg.essential.elementa.constraints.animation.Animations +import gg.essential.elementa.state.BasicState +import gg.essential.elementa.transitions.BoundTransition +import gg.essential.elementa.effects.AlphaEffect +import kotlin.properties.Delegates + +/** + * Fades a component and all of its children in. This is done using + * [AlphaEffect]. When the transition is finished, the effect is removed. + */ +class FadeInTransition @JvmOverloads constructor( + private val time: Float = 1f, + private val animationType: Animations = Animations.OUT_EXP, +) : BoundTransition() { + + private val alphaState = BasicState(0f) + private var alpha by Delegates.observable(0f) { _, _, newValue -> + alphaState.set(newValue) + } + + private val effect = AlphaEffect(alphaState) + + override fun beforeTransition() { + boundComponent.enableEffect(effect) + } + + override fun doTransition(constraints: AnimatingConstraints) { + constraints.setExtraDelay(time) + boundComponent.apply { + ::alpha.animate(animationType, time, 1f) + } + } + + override fun afterTransition() { + boundComponent.removeEffect(effect) + effect.cleanup() + } +} \ No newline at end of file diff --git a/unstable/layoutdsl/src/main/kotlin/gg/essential/elementa/transitions/FadeOutTransition.kt b/unstable/layoutdsl/src/main/kotlin/gg/essential/elementa/transitions/FadeOutTransition.kt new file mode 100644 index 00000000..38852d22 --- /dev/null +++ b/unstable/layoutdsl/src/main/kotlin/gg/essential/elementa/transitions/FadeOutTransition.kt @@ -0,0 +1,42 @@ +package gg.essential.elementa.transitions + +import gg.essential.elementa.constraints.animation.AnimatingConstraints +import gg.essential.elementa.constraints.animation.Animations +import gg.essential.elementa.state.BasicState +import gg.essential.elementa.transitions.BoundTransition +import gg.essential.elementa.effects.AlphaEffect +import kotlin.properties.Delegates + +/** + * Fades a component and all of its children out. This is done using + * [AlphaEffect]. When the transition is finished, the effect is removed. + * Typically, one would hide the component after this transition is finished. + */ +class FadeOutTransition @JvmOverloads constructor( + private val time: Float = 1f, + private val animationType: Animations = Animations.OUT_EXP, +) : BoundTransition() { + + private val alphaState = BasicState(1f) + private var alpha by Delegates.observable(1f) { _, _, newValue -> + alphaState.set(newValue) + } + + private val effect = AlphaEffect(alphaState) + + override fun beforeTransition() { + boundComponent.enableEffect(effect) + } + + override fun doTransition(constraints: AnimatingConstraints) { + constraints.setExtraDelay(time) + boundComponent.apply { + ::alpha.animate(animationType, time, 0f) + } + } + + override fun afterTransition() { + boundComponent.removeEffect(effect) + effect.cleanup() + } +} \ No newline at end of file