Skip to content

Commit

Permalink
Make atmosphere shader compatible with sciview inspector (#707)
Browse files Browse the repository at this point in the history
* Atmosphere: introduced hasControls flag, changed to toggle behavior
* AtmosphereExample: change to toggleRotateBehavior
* Atmosphere: add attach/detach methods, make compatible with sciview inspector
* Atmosphere: add documentation for updateEmissionStrength
* Atmosphere: introduce methods setSunDirectionFromAngles and -FromVector and azimuth/elevation properties
  • Loading branch information
smlpt committed Apr 18, 2024
1 parent 2b3d583 commit 0d751ce
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 52 deletions.
169 changes: 120 additions & 49 deletions src/main/kotlin/graphics/scenery/primitives/Atmosphere.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,122 +4,166 @@ 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)
* cos(latitudeRad)
* 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.
* @param arrowKey The direction to be passed as [String].
* */
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<String, Pair<Float, Vector3f>>()
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) } }
Expand All @@ -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)
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
* <Description>
Expand Down Expand Up @@ -92,7 +93,7 @@ class AtmosphereExample : SceneryBase("Atmosphere Example",

setupCameraModeSwitching()

inputHandler?.let { atmos.attachRotateBehaviours(it) }
inputHandler?.let { atmos.attachBehaviors(it) }
}

companion object {
Expand Down

0 comments on commit 0d751ce

Please sign in to comment.