Rather than transforming the character maybe transform the 'world'(planet)?

MirceaKitsune
Not sure I understand your environment problem. If you're walking on the surface of a spherical planet the environment could in reality get "tilted" in all kinds of ways. What's the meaning of having a "horizon" in planet's environment anyway? There should only be sun. The actual horizon is planet's contour.

But if you want the environment horizon to align with planet's horizon you'll need to use a custom sky/sun shader. Or do what @Megalomaniak said.

    MirceaKitsune I managed to simplify it a bit so this is the only transform required for the base math:

    global_basis.y = global_position.normalized()
    global_basis.x = global_basis.y.cross(global_basis.z)
    global_basis = global_basis.orthonormalized()

    For a bulletproof solution, you need the second cross product as well. The third one in my example was redundant.
    The order of cross products the engine does in Basis::orthonormalized() is not guaranteed. So if your code does not ensure orthogonality the whole thing may potentially misbehave.

    xyz Thanks for clarifying: I added just the second cross product back in that case.

    For the environment: Imagine the sun is shining from any direction on a planet with an atmosphere. If you stay in an area where the sun is above (noon) the sun light is white and the sky is cyan colored, if you move away (dusk) sunlight is more yellow and the sky becomes dull toward the horizon, move further away (sunset) the sun becomes red at an angle as the sky grows dark. The default sky assumes -Y as down and can't easily change to reflect the atmosphere seen from different angles as the sun shines from the same direction. Most likely I need a different type of sky, possibly a custom shader like you said... getting a tilt corrected horizon to work would be a very complex issue in any case.

    Even so I don't know what algorithm I'd need: I'll need some way to convert the player's position on the planet into a 0 to 1 range... 1 is the player standing right between the center of the planet and the direction in which the sun shines, 0 is the player standing on the exact opposite side of the planet from the directional light's perspective. I should be able to do the sun in just one axis which will help simplify this a lot, but the sky tilt still needs to be corrected in 2D or 3D space which is more problematic.

      MirceaKitsune 1 is the player standing right between the center of the planet and the direction in which the sun shines, 0 is the player standing on the exact opposite side of the planet from the directional light's perspective.

      Is your planet spinning or stationary?

        MirceaKitsune getting a tilt corrected horizon to work would be a very complex issue in any case.

        It's actually relatively simple. Normalized player's position is a vector that points straight up to the zenith. Rotate the sky coordinates by the angle between the global up vector (which is the default zenith) and player's zenith vector, and you'll have the horizon in the right place. Sun direction is absolute so simply take the light source's direction vector.

        You can use a sky shader converted from engine's sky material and just modify it to handle the horizon as I described above.

          Tomcat The planet / disk / ring is stationary, with its center hardcoded at the world origin of 0 0 0. The best design by far was to make it static and have the player independently move around it. I don't plan for multiplayer support of all things, but just in case a solution that works per player sounds ideal.

          xyz Thanks, I'll keep this in mind for when I look into it. The trick is that I need to rotate the sky so the horizon is always down from the perspective of the player no matter their location, but at the same time the sun disk must be drawn at the same sport based on the sun light's rotation: You can set a sky rotation on the PhysicalSkyMaterial in WorldEnvironment properties, problem is that also rotates the sun with it... I'm not sure where I could subtract this transform to get the sun position back without having to actually rotate the light entity which would change the actual direction of the light.

          • xyz replied to this.

            MirceaKitsune Thanks, I'll keep this in mind for when I look into it. The trick is that I need to rotate the sky so the horizon is always down from the perspective of the player no matter their location, but at the same time the sun disk must be drawn at the same sport based on the sun light's rotation: You can set a sky rotation on the PhysicalSkyMaterial in WorldEnvironment properties, problem is that also rotates the sun with it... I'm not sure where I could subtract this transform to get the sun position back without having to actually rotate the light entity which would change the actual direction of the light.

            Well that's precisely why you need to do it in a custom shader. It can't be done only with the default sky material.

            MirceaKitsune Here's your recipe:

            • Convert environment's sky material (procedural sky) to shader material
            • Edit the shader and in shader code do the following
            • In the uniform declaration block add uniform mat3 inv_horizon_matrix;
            • In the sky() function insert this line at the beginning: vec3 eyedir_horiz = inv_horizon_matrix * EYEDIR;
            • In the next line below replace EYEDIR with eyedir_horiz
            • In the last line in the sky() function replace EYEDIR with eyedir_horiz as well
            • each frame, feed the matrix to the shader from script:
              var zenith = player.global_position.normalized()	
              environment_node.environment.sky.sky_material.set_shader_parameter("inv_horizon_matrix", Quaternion(zenith, Vector3.UP))

            You owe me a beer (or two) at this point 😉

              xyz Darn, I really need to post a good project at this point, at least some demo basis for what I'm working on. Thanks again, you're awesome! Just finished threading the planet chunk generator accordingly and shall definitely give this a try next.

              9 days later

              Alrighty: Got some really amazing stuff working. The world script uses the camera view position, the shader was rewritten almost entirely to simplify it while supporting more features and extra detail such as the horizon lighting up as the sun approaches it. My version is meant to work with two lights, a sun and a moon. Here's a view of it with the script and shader:

              extends WorldEnvironment
              
              @onready var cam = get_viewport().get_camera_3d()
              
              func _process(_delta):
              	var pos = cam.global_position.normalized()
              	environment.sky.sky_material.set_shader_parameter("inv_horizon_matrix", Quaternion(pos, Vector3.UP))
              // Rotating sky shader similar to Godot 4.2.2 ProceduralSkyMaterial
              // Accepts two lights (sun and moon) with light color and intensity controlling the sky
              
              shader_type sky;
              render_mode use_debanding;
              
              uniform mat3 inv_horizon_matrix;
              uniform vec4 sky_top_color : source_color = vec4(0.4, 0.6, 0.8, 1.0);
              uniform vec4 sky_horizon_color : source_color = vec4(0.6, 0.7, 0.8, 1.0);
              uniform vec4 sky_bottom_color : source_color = vec4(0.2, 0.1, 0.1, 1.0);
              uniform float sky_top_curve : hint_range(0, 1) = 0.25;
              uniform float sky_bottom_curve : hint_range(0, 1) = 0.025;
              uniform float sky_energy = 0.0;
              uniform float cover_energy = 1.0;
              uniform float sun_energy = 1.0;
              uniform float moon_energy = 1.0;
              uniform float sun_scale = 1.0;
              uniform float moon_scale = 1.0;
              uniform float sun_disk : hint_range(0, 1) = 0.05;
              uniform float moon_disk : hint_range(0, 1) = 0.05;
              uniform sampler2D sky_cover : filter_linear, source_color, hint_default_black;
              
              void sky() {
              	vec3 eyedir_horiz = inv_horizon_matrix * EYEDIR;
              	float v_angle = acos(clamp(eyedir_horiz.y, -1.0, 1.0));
              	float a0 = (1.0 - v_angle / (PI * 0.5));
              	float a1 = (v_angle - (PI * 0.5)) / (PI * 0.5);
              
              	vec3 sky_gradient = mix(sky_top_color.rgb, sky_horizon_color.rgb, clamp(pow(1.0 - a0, 1.0 / sky_top_curve), 0.0, 1.0));
              	vec3 sky = sky_gradient * sky_energy;
              
              	if (LIGHT0_ENABLED) {
              		float angle = acos(dot(LIGHT0_DIRECTION, EYEDIR));
              		if (angle < sun_scale) {
              			float c1 = (angle - LIGHT0_SIZE) / (sun_scale - LIGHT0_SIZE);
              			float c2 = clamp(sun_disk + pow(1.0 - a0, 1.0 / sky_top_curve), 0.0, 1.0);
              			sky = mix(sky, LIGHT0_COLOR * LIGHT0_ENERGY, clamp(pow(1.0 - c1, 1.0 / c2), 0.0, 1.0));
              		}
              		sky += (sky_gradient * LIGHT0_COLOR) * sun_energy * LIGHT0_ENERGY * (1.0 - angle / PI / 2.0);
              	}
              
              	if (LIGHT1_ENABLED) {
              		float angle = acos(dot(LIGHT1_DIRECTION, EYEDIR));
              		if (angle < moon_scale) {
              			float c1 = (angle - LIGHT1_SIZE) / (moon_scale - LIGHT1_SIZE);
              			float c2 = clamp(moon_disk + pow(1.0 - a0, 1.0 / sky_top_curve), 0.0, 1.0);
              			sky = mix(sky, LIGHT1_COLOR * LIGHT1_ENERGY, clamp(pow(1.0 - c1, 1.0 / c2), 0.0, 1.0));
              		}
              		sky += (sky_gradient * LIGHT1_COLOR) * moon_energy * LIGHT1_ENERGY * (1.0 - angle / PI / 2.0);
              	}
              
              	vec4 sky_cover_texture = texture(sky_cover, SKY_COORDS);
              	sky += sky_cover_texture.rgb * sky_cover_texture.a * cover_energy;
              
              	COLOR = mix(sky, sky_gradient * sky_bottom_color.rgb, clamp(1.0 - pow(1.0 - a1, 1.0 / sky_bottom_curve), 0.0, 1.0));
              }

              One more algorithm for it to work, last thing and very simple at least for math experts 😅 On the GDScript side I need a float that tells me by what amount the player is standing under the sun based on the light's rotation: I'll be using it to change the color of the light so it gets whiter / yellower / redder, or even better plug it into the temperature which I see is an upcoming feature in Godot 4.3. No need to worry about the colorization part, my only issue is the vector conversion for extracting the 0 - 1 range indicating the player's location relative to the sun, it's always the complex mathematics I get stuck on.

              The variable should be 1.0 when the player is standing right under the sun and it's exactly above your head, 0.5 when the player is on the planet's equator relative to the sun meaning they're right on the rim of where the sun is shining and seeing it right on the horizon, 0.0 when the player is oppositely on the other side of the planet from the sun's perspective.

              • xyz replied to this.

                MirceaKitsune The variable should be 1.0 when the player is standing right under the sun and it's exactly above your head, 0.5 when the player is on the planet's equator relative to the sun meaning they're right on the rim of where the sun is shining and seeing it right on the horizon, 0.0 when the player is oppositely on the other side of the planet from the sun's perspective.

                It's simply a dot product between zenith vector and directional light (sun) reversed direction vector. This will give values in range (-1, 1) but just remap it to (0,1) range. So:

                zenith.dot(light.global_basis.z) * .5 + .5
                a month later

                Sorry again for the delay, was caught with other stuff but got back to this today. Wanted to confirm the last suggestion works perfectly and share some images of it, also my updated script which shows what I do with the lights. The sun and moon now change color and intensity together with the sky based on where on the planet the player is located, I even added stars with a procedural noise texture and the result is surprisingly realistic, edited to include movement code with support for a customizable wobble. The goal now would be to get a terrain shader so it's not just a white surface, I need to think how I can do biomes and detect cliffs relative to the center but I'm sure I'll figure out a way from here on.

                extends WorldEnvironment
                
                @export var sun_speed: Vector2
                @export var moon_speed: Vector2
                
                @onready var cam = get_viewport().get_camera_3d()
                
                func _ready():
                	pass
                
                func _process(_delta):
                	var pos = cam.global_position.normalized()
                	environment.sky.sky_material.set_shader_parameter("inv_horizon_matrix", Quaternion(pos, Vector3.UP))
                
                	var sun_zenith = pos.dot($Sun.global_basis.z) * 0.5 + 0.5
                	var sun_energy = $Sun.light_energy
                	var sun_h = clamp(sun_zenith * 0.1, 0, 1)
                	var sun_s = clamp(2 - sun_zenith * 2, 0, 1)
                	var sun_v = clamp(-0.5 + sun_zenith * sun_energy * 2, 0, 1)
                	$Sun.rotate_object_local(Vector3.RIGHT, _delta * sun_speed.x)
                	$Sun.rotate_object_local(Vector3.UP, _delta * sun_speed.y)
                	$Sun.set_color(Color.from_hsv(sun_h, sun_s, sun_v))
                	$Sun.set_visible(sun_v > 0)
                
                	var moon_zenith = pos.dot($Moon.global_basis.z) * 0.5 + 0.5
                	var moon_energy = $Moon.light_energy
                	var moon_h = clamp(moon_zenith * 0.1, 0, 1)
                	var moon_s = clamp(2 - moon_zenith * 2, 0, 1)
                	var moon_v = clamp(-0.5 + moon_zenith * moon_energy * 2, 0, 1)
                	$Moon.rotate_object_local(Vector3.RIGHT, _delta * moon_speed.x)
                	$Moon.rotate_object_local(Vector3.UP, _delta * moon_speed.y)
                	$Moon.set_color(Color.from_hsv(moon_h, moon_s, moon_v))
                	$Moon.set_visible(moon_v > 0)