Skip to content

Commit

Permalink
unstable: Import AlphaEffect, GradientEffect, and related from Essential
Browse files Browse the repository at this point in the history
Source-Commit: 1ecfce45f56459000f495b98bd4a3308be228e06
Co-authored-by: Jonas Herzig <[email protected]>
Co-authored-by: DJtheRedstoner
<[email protected]>
  • Loading branch information
CallumBugajski authored and Johni0702 committed Sep 5, 2024
1 parent f66a554 commit ddd0c43
Show file tree
Hide file tree
Showing 6 changed files with 414 additions and 0 deletions.
2 changes: 2 additions & 0 deletions unstable/layoutdsl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Float>) : 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<AlphaEffect>(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<AlphaEffect>()
val toBeCleanedUp: MutableSet<Resources> = 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")
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Color>,
private val topRight: State<Color>,
private val bottomLeft: State<Color>,
private val bottomRight: State<Color>,
) : 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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Color>, bottom: State<Color>, _desc: GradientVertDesc = GradientDesc) = gradient(top, top, bottom, bottom)
fun Modifier.gradient(left: State<Color>, right: State<Color>, _desc: GradientHorzDesc = GradientDesc) = gradient(left, right, left, right)

sealed interface GradientVertDesc
sealed interface GradientHorzDesc
private object GradientDesc : GradientVertDesc, GradientHorzDesc

fun Modifier.gradient(
topLeft: State<Color>,
topRight: State<Color>,
bottomLeft: State<Color>,
bottomRight: State<Color>,
) = effect { GradientEffect(topLeft, topRight, bottomLeft, bottomRight) }

private fun Modifier.effect(effect: () -> Effect) = this then {
val instance = effect()
enableEffect(instance)
return@then {
removeEffect(instance)
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
Loading

0 comments on commit ddd0c43

Please sign in to comment.