Vertex shader to distort a flat world into a round planet or ring
xyz Will do, I should try at least a small project at some point. At very least it will help others looking for answers to this exact question. But obviously I asked because I plan on using it just need to find the right setup with everything else.
- Edited
MirceaKitsune Setting this up would require less effort than typing your original post. Not telling you what to do but since you went to some length to solicit the example code, it'd seem reasonable to at least try to run it.
xyz I plan to. I'm actually thinking what the best way to attempt simulating either a heightmap or voxel planet would be, without having to use one of the terrain plugins which are designed to be flat and most require C# to run: Was thinking of (ab)using CSG spheres but what I'm thinking of would be so slow it will likely just freeze.
MirceaKitsune How big would that planet be compared to the player?
- Edited
xyz The plan is to go for acceptable realism, not ridiculously small but obviously not life sized either. If the player is 2m tall, I'd need to have the sphere at very least 1.000 units large and see how that works, after that see if I can push it to 10.000 and beyond.
Obviously it will need some form of LOD, which is harder to do out of square patches for a bent sphere unlike a flat ground, another reason why I initially wanted a deform shader for roundness. Currently my idea is to use noise to add CSG spheres based on camera distance, larger units / spheres and thus lower LOD the further away something is; I still risk having to use thousands of them even so, and to my knowledge the CSG system doesn't like so many items which cause slowness and random tares in the surface.
It would be a lot easier if Godot had a tesselation shader, but as it stands you can't add or remove vertices through GLSL only displace them. There's also collisions: Those can only be calculated on the CPU to my knowledge, you can't have a collision mesh read the displacements from a shader which would have been really amazing.
MirceaKitsune If the planet is only seen from player's perspective, then there's no point in making it spherical, or at least the sphere can be approximated using planar patches.
I haven't forgotten about this and plan to come back with an example later! I'm first focusing on the most flexible terrain setup and figuring how to get displacement right: Here's my separate question on that, if anyone knows how to set that up I'll take a look at how the player physics script works.
https://godotforums.org/d/40249-displace-vertices-along-normals-on-csg-nodes
- Edited
Sorry for the delay. I integrated your code with my old player script and am happy to say it works wonderfully! 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()
Here's the full player script, it contains more features including distance scaling and inverted gravity for inside-out planets or rings:
extends CharacterBody3D
@export var Sensitivity_X = 0.01
@export var Sensitivity_Y = 0.01
@export var Minimum_Y_Look = -90
@export var Maximum_Y_Look = 90
@export var Accelaration = 100
@export var Decelaration = 25
@export var Air_Accelaration = 50
@export var Air_Decelaration = 5
@export var Jump_Speed = 500
@export var Jump_Jetpack = true
@export var Gravity = 2500
@export var Gravity_Sphere = true
func _ready():
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
func _physics_process(delta):
var input_dir = Input.get_vector(&"ui_left", &"ui_right", &"ui_up", &"ui_down")
var accel = Accelaration if is_on_floor() else Air_Accelaration
var decel = Decelaration if is_on_floor() else Air_Decelaration
# Apply velocity: Gravity, jump, movement, friction
if not is_on_floor():
var dist = 1 / (1 + global_position.distance_to(Vector3i(0, 0, 0)))
velocity -= global_basis.y * abs(Gravity) * dist * delta
if (Jump_Jetpack or is_on_floor()) and Input.is_action_pressed(&"ui_accept"):
velocity += global_basis.y * abs(Jump_Speed) * delta
velocity += (global_basis.x * input_dir.x + global_basis.z * input_dir.y) * accel * delta
velocity /= 1 + decel * delta
move_and_slide()
# Apply sphere orientation, overrides up_direction for move_and_slide to work correctly
if Gravity_Sphere:
var dir = +1 if Gravity >= 0 else -1
global_basis.y = global_position.normalized() * dir
global_basis.x = global_basis.y.cross(global_basis.z)
global_basis = global_basis.orthonormalized()
up_direction = global_basis.y
func _input(event):
if event is InputEventMouseMotion:
rotate_object_local(Vector3.DOWN, event.relative.x * Sensitivity_X)
$PlayerCamera.rotate_x(-event.relative.y * Sensitivity_Y)
$PlayerCamera.rotation.x = min(deg_to_rad(Maximum_Y_Look), max(deg_to_rad(Minimum_Y_Look), $PlayerCamera.rotation.x))
There is but one issue left, and it would be amazing if anyone could give me a hand with it as well. As can be seen in the image, the atmosphere doesn't rotate with the player when using a PhysicalSkyMaterial
, which causes the horizon to tilt and doesn't look realistic from behind: The horizon should always be down from the perspective of the player, the sun should shine from the same direction but moved to the new position on the rotated sky. If I create a script for my WorldEnvironment
node to rotate it and its DirectionalLight3D
child, what should it contain to get a proper results? It should probably have an offset for specifying the time of day, I could plug the time into it to get a daytime cycle and run it at a different phase for the moon.
Rather than transforming the character maybe transform the 'world'(planet)?
- Edited
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.
- Edited
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.
- Edited
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?
- Edited
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.
- Edited
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.
- Edited
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
witheyedir_horiz
- In the last line in the
sky()
function replaceEYEDIR
witheyedir_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