diff --git a/src/main/kotlin/graphics/scenery/primitives/Atmosphere.kt b/src/main/kotlin/graphics/scenery/primitives/Atmosphere.kt index 18390f3d1..e9bc1ac4f 100644 --- a/src/main/kotlin/graphics/scenery/primitives/Atmosphere.kt +++ b/src/main/kotlin/graphics/scenery/primitives/Atmosphere.kt @@ -4,69 +4,93 @@ import graphics.scenery.* import graphics.scenery.attribute.material.Material import graphics.scenery.controls.InputHandler import kotlinx.coroutines.* -import org.joml.Quaternionf import org.joml.Vector3f import org.joml.Vector4f import org.scijava.ui.behaviour.ClickBehaviour +import java.lang.Math.toDegrees import java.lang.Math.toRadians import java.time.LocalDateTime -import kotlin.collections.HashMap import kotlin.math.* +import kotlin.time.Duration.Companion.seconds /** * Implementation of a Nishita sky shader, applied to an [Icosphere] that wraps around the scene as a skybox. * The shader code is ported from Rye Terrells [repository](https://github.com/wwwtyro/glsl-atmosphere). - * To move the sun with arrow keybinds, attach the behaviours using the [attachRotateBehaviours] function. - * @param initSunDir [Vector3f] of the sun position. Defaults to sun elevation of the current local time. - * @param emissionStrength Emission strength of the atmosphere shader. Defaults to 0.3f. + * To move the sun with arrow keys, attach the behaviours using the [attachBehaviors] function. + * @param initSunDirection [Vector3f] of the sun position. Defaults to sun elevation of the current local time. + * @param emissionStrength Emission strength of the atmosphere shader. Defaults to 1f. * @param latitude Latitude of the user; needed to calculate the local sun position. Defaults to 50.0, which is central Germany. */ -open class Atmosphere(initSunDir: Vector3f? = null, emissionStrength: Float = 0.3f, var latitude: Double = 50.0) : +open class Atmosphere( + initSunDirection: Vector3f? = null, + emissionStrength: Float = 1.0f, + var latitude: Float = 50.0f +) : Icosphere(10f, 2, insideNormals = true) { @ShaderProperty - var sunDir: Vector3f + var sunDirection = Vector3f(1f, 1f, 1f) - private var sunDirectionManual: Boolean = false + /** Is set to true if the user manually moved the sun direction. This disables automatic updating.*/ + var isSunAnimated: Boolean = true + /** Flag that tracks whether the sun position controls are currently attached to the input handler. */ + var hasControls: Boolean = false + private set + + var azimuth = 180f + var elevation = 45f + + // Coroutine job for updating the sun direction + private var job = CoroutineScope(Dispatchers.Default).launch(start = CoroutineStart.LAZY) { + logger.debug("Launched sun updating job") + while (this.coroutineContext.isActive) { + if (isSunAnimated) { + setSunDirectionFromTime() + } + delay(2.seconds) + } + } + + // automatically update the material when this property is changed + var emissionStrength = emissionStrength + set(value) { + field = value + material { emissive = Vector4f(0f, 0f, 0f, emissionStrength * 0.3f) } + } init { this.name = "Atmosphere" setMaterial(ShaderMaterial.fromClass(this::class.java)) material { cullingMode = Material.CullingMode.Front + depthTest = true depthOp = Material.DepthTest.LessEqual - emissive = Vector4f(0f, 0f, 0f, emissionStrength) + emissive = Vector4f(0f, 0f, 0f, emissionStrength * 0.3f) } - // Only use time-based elevation when the formal parameter is empty - if (initSunDir == null) { - sunDir = getSunDirFromTime() - } - else { - sunDir = initSunDir - sunDirectionManual = true + // Only animate the sun when no direction is passed to the constructor + isSunAnimated = if (initSunDirection == null) { + setSunDirectionFromTime() + true + } else { + sunDirection = initSunDirection + false } // Spawn a coroutine to update the sun direction - val job = CoroutineScope(Dispatchers.Default).launch { - while (!sunDirectionManual) { - sunDir = getSunDirFromTime() - // Wait 30 seconds - delay(30 * 1000) - } - } + job.start() } - /** Turn the current local time into a sun elevation angle, encoded as cartesian [Vector3f]. + /** Set the sun direction by the current local time. * @param localTime local time parameter, defaults to [LocalDateTime.now]. */ - private fun getSunDirFromTime(localTime: LocalDateTime = LocalDateTime.now()): Vector3f { - val latitudeRad = toRadians(latitude) + fun setSunDirectionFromTime(localTime: LocalDateTime = LocalDateTime.now()) { + val latitudeRad = toRadians(latitude.toDouble()) val dayOfYear = localTime.dayOfYear.toDouble() val declination = toRadians(-23.45 * cos(360.0 / 365.0 * (dayOfYear + 10))) val hourAngle = toRadians((localTime.hour + localTime.minute / 60.0 - 12) * 15) - val elevation = asin( + val elevationRad = asin( sin(toRadians(declination)) * sin(latitudeRad) + cos(declination) @@ -74,18 +98,40 @@ open class Atmosphere(initSunDir: Vector3f? = null, emissionStrength: Float = 0. * cos(hourAngle) ) - val azimuth = atan2( + val azimuthRad = atan2( sin(hourAngle), cos(hourAngle) * sin(latitudeRad) - tan(declination) * cos(latitudeRad) ) - PI / 2 - val result = Vector3f( - cos(azimuth).toFloat(), - sin(elevation).toFloat(), - sin(azimuth).toFloat() + // update global sun angle properties; these are needed for the sciview inspector fields + azimuth = toDegrees(azimuthRad).toFloat() + elevation = toDegrees(elevationRad).toFloat() + + sunDirection = Vector3f( + cos(azimuthRad).toFloat(), + sin(elevationRad).toFloat(), + sin(azimuthRad).toFloat() + ) + logger.info("Updated sun direction to {}.", sunDirection) + } + + /** Set the sun direction by passing a 3D directional vector. */ + fun setSunDirectionFromVector(direction: Vector3f) { + isSunAnimated = false + sunDirection = direction.normalize() + } + + /** Set the sun direction by passing angles for [elevation] and [azimuth] in degrees. */ + fun setSunDirectionFromAngles(elevation: Float, azimuth: Float) { + isSunAnimated = false + this.elevation = elevation + this.azimuth = azimuth + + sunDirection = Vector3f( + cos(toRadians(this.azimuth.toDouble())).toFloat(), + sin(toRadians(this.elevation.toDouble())).toFloat(), + sin(toRadians(this.azimuth.toDouble())).toFloat() ) - logger.debug("Updated sun direction to {}.", result) - return result } /** Move the shader sun in increments by passing a direction and optionally an increment value. @@ -93,33 +139,31 @@ open class Atmosphere(initSunDir: Vector3f? = null, emissionStrength: Float = 0. * */ private fun moveSun(arrowKey: String, increment: Float) { // Indicate that the user switched to manual sun direction controls - if (!sunDirectionManual) { - sunDirectionManual = true + if (isSunAnimated) { + isSunAnimated = false logger.info("Switched to manual sun direction.") } - // Define a HashMap to map arrow key dimension strings to rotation angles and axes - val arrowKeyMappings = HashMap>() - arrowKeyMappings["UP"] = Pair(increment, Vector3f(1f, 0f, 0f)) - arrowKeyMappings["DOWN"] = Pair(-increment, Vector3f(1f, 0f, 0f)) - arrowKeyMappings["LEFT"] = Pair(increment, Vector3f(0f, 1f, 0f)) - arrowKeyMappings["RIGHT"] = Pair(-increment, Vector3f(0f, 1f, 0f)) - - val mapping = arrowKeyMappings[arrowKey] - if (mapping != null) { - val (angle, axis) = mapping - val rotation = Quaternionf().rotationAxis(toRadians(angle.toDouble()).toFloat(), axis.x, axis.y, axis.z) - sunDir.rotate(rotation) + + when (arrowKey) { + "UP" -> elevation += increment + "DOWN" -> elevation -= increment + "LEFT" -> azimuth -= increment + "RIGHT" -> azimuth += increment } + setSunDirectionFromAngles(elevation, azimuth) } /** Attach Up, Down, Left, Right key mappings to the inputhandler to rotate the sun in increments. * Keybinds are Ctrl + cursor keys for fast movement and Ctrl + Shift + cursor keys for slow movement. + * Moving the sun will disable the automatic sun animation. * @param increment Increment value for the rotation in degrees, defaults to 20°. Slow movement is always 10% of [increment]. */ - fun attachRotateBehaviours(inputHandler: InputHandler, increment: Float = 20f) { + fun attachBehaviors(inputHandler: InputHandler, increment: Float = 20f) { + hasControls = true val incMap = mapOf( "fast" to increment, "slow" to increment / 10 ) + for (speed in listOf("fast", "slow")) { for (direction in listOf("UP", "DOWN", "LEFT", "RIGHT")) { val clickBehaviour = ClickBehaviour { _, _ -> incMap[speed]?.let { moveSun(direction, it) } } @@ -131,5 +175,32 @@ open class Atmosphere(initSunDir: Vector3f? = null, emissionStrength: Float = 0. } } } + + /** Detach the key bindings from the input handler. + * Per default this also re-enables the sun animation, but it can be turned off with [enableAnimation]. */ + fun detachBehaviors(inputHandler: InputHandler, enableAnimation: Boolean = true) { + hasControls = false + if (enableAnimation) { + isSunAnimated = true + } + val behaviors = inputHandler.behaviourMap.keys() + behaviors.forEach { + if (it.contains("move_sun")) { + inputHandler.removeBehaviour(it) + inputHandler.removeKeyBinding(it) + } + } + } + + /** Attach or detach Up, Down, Left, Right key mappings to the inputhandler to rotate the sun in increments. + * Keybinds are Ctrl + cursor keys for fast movement and Ctrl + Shift + cursor keys for slow movement. + * @param increment Increment value for the rotation in degrees, defaults to 20°. Slow movement is always 10% of [increment]. */ + fun toggleBehaviors(inputHandler: InputHandler, increment: Float = 20f) { + if (hasControls) { + detachBehaviors(inputHandler) + } else { + attachBehaviors(inputHandler, increment) + } + } } diff --git a/src/main/resources/graphics/scenery/backends/shaders/Atmosphere.frag b/src/main/resources/graphics/scenery/backends/shaders/Atmosphere.frag index c2e6d80d8..84913d565 100644 --- a/src/main/resources/graphics/scenery/backends/shaders/Atmosphere.frag +++ b/src/main/resources/graphics/scenery/backends/shaders/Atmosphere.frag @@ -86,7 +86,7 @@ layout(push_constant) uniform currentEye_t { layout(set = 4, binding = 0) uniform sampler2D ObjectTextures[NUM_OBJECT_TEXTURES]; layout(set = 5, binding = 0) uniform ShaderProperties { - vec3 sunDir; + vec3 sunDirection; }; // courtesy of Christian Schueler - http://www.thetenthplanet.de/archives/1180 @@ -296,7 +296,7 @@ void main() { vec3 color = atmosphere( normalize(Vertex.FragPosition), // normalized ray direction vec3(0,6372e3,0), // ray origin - sunDir, // position of the sun + sunDirection, // position of the sun 22.0, // intensity of the sun 6371e3, // radius of the planet in meters 6471e3, // radius of the atmosphere in meters diff --git a/src/test/kotlin/graphics/scenery/tests/examples/basic/AtmosphereExample.kt b/src/test/kotlin/graphics/scenery/tests/examples/basic/AtmosphereExample.kt index 2060053cc..8bfb0ee17 100644 --- a/src/test/kotlin/graphics/scenery/tests/examples/basic/AtmosphereExample.kt +++ b/src/test/kotlin/graphics/scenery/tests/examples/basic/AtmosphereExample.kt @@ -16,6 +16,7 @@ import net.imglib2.img.Img import net.imglib2.img.display.imagej.ImageJFunctions import net.imglib2.type.numeric.integer.UnsignedShortType import org.joml.Vector3f +import kotlin.concurrent.thread /** * @@ -92,7 +93,7 @@ class AtmosphereExample : SceneryBase("Atmosphere Example", setupCameraModeSwitching() - inputHandler?.let { atmos.attachRotateBehaviours(it) } + inputHandler?.let { atmos.attachBehaviors(it) } } companion object {