Reputation: 86
I have been trying to create a 2D "fake planet" kind of effect using shaders, but the result I have, while it works, is not great.
void vert(inout appdata_full v)
{
//Get vertex world coordinates
float4 worldV = mul(unity_ObjectToWorld, v.vertex);
//Get target coordinates relative to vertex world
worldV.xyz -= _TargetPos.xyz;
//Transform vertex based on x distance from target
worldV = float4(0.0f, (worldV.x * worldV.x) * -_Curvature, 0.0f, 0.0f);
//Add this offset to vertex
v.vertex += mul(unity_WorldToObject, worldV);
}
The code above is the vertex function that I am using to produce this effect, but since it only offsets the vertices on the Y based on the player's X, it produces a distortion near the edge of the screen. It just looks off, since all vertical lines stay completely vertical.
What I would like it to do is make it appear more like a planet:
But I'm not sure how I would go about this.
EDIT: I have been trying for hours to figure this out, and I thought I had figured it out:
.
But no, this did not work. The level just seemed to curve and dip in random places.
Upvotes: 2
Views: 2495
Reputation: 1
TL;DR:
d = sqrt((x - t_x) * (x - t_x) + (z - t_z) * (z - t_z))
(x', y', z') = sin(d / R) * (R + y - t_y) / d * (x - t_x, 0, z - t_z) + (t_x, cos(d / R) * (R + y - t_y) - R + t_y, t_z)
Where (x', y', z') is the transformation of (x, y, z) when curving the world around a sphere with radius R whose top is at (t_x, t_y, t_z).
This question is already very old but seeing as the answer might still be useful to people I am gonna explain how I would solve this. I'm only providing the mathematical perspective but it should be fairly easy to implement in e.g. a vertex shader.
I'm assuming that the y-Axis is the up direction and that you want a transformation that projects the entire xz-plane onto the surface of a sphere with radius R at (0, -R, 0), such that (0, 0, 0) is mapped to itself.
First of all, we only need x, z to compute the point on the sphere's surface and will be adding in the height later. Compute the vertical distance as
d = sqrt(x * x + z * z)
Instead of subtracting this distance from the y-component we instead use d as the parameter of a circle with radius R, centered at (0, -R):
p = R * (sin(d / R), cos(d / R) - R)
We use d / R such that one unit in distance corresponse to one unit of distance on the circle of radius R.
Writing this point as a transformation of some point with horizontal distance d and no vertical distance, i. e. (d, 0), we get:
p = (d, 0) * sin(d / R) * R / d + (0, cos(d / R) * R - R)
This tells us that the horizontal distance will be scaled by sin(d / R) * R / d, whereas the vertical distance will be cos(d / R) * R - R.
Implementing this in 3D we get:
(x', y', z') = sin(d / R) * R / d * (x, 0, z) + (0, cos(d / R) * R - R, 0)
Adding the height y back in we get:
(x', y', z') = sin(d / R) * (R + y) / d * (x, 0, z) + (0, cos(d / R) * (R + y) - R, 0)
If you do not want the origin of the world to be the tip of the sphere but you instead want some point (x_t, y_t, z_t) to be the tip you can first transform the world such that (x_t, y_t, z_t) becomes the origin, then do the projection and then transform back.
Here are some points at y-level 0.1 being projected according to a sphere with radius R = 2 and a tip at (2, 0, 3): https://i.sstatic.net/iH5DBRj8.png
Upvotes: 0
Reputation: 1
I was searching for the same shader for the Godot Engine. Couldn't find one, but your nice sketches and comments gave me the right idea how to approach it.
Here's my solution in Godots shader language (similar to GLSL ES 3.0):
shader_type canvas_item;
uniform float radius = 2.0;
void fragment() {
vec2 uv = SCREEN_UV;
vec2 surface = vec2(0.5, 0.2);
vec2 center = surface - vec2(0, radius);
float base = length(uv - center);
float height = base - radius;
float xdiff = (uv.x - surface.x) / base * height;
uv = clamp(vec2 (uv.x - xdiff, surface.y + height), vec2(0.0, 0.0), vec2(1.0, 1.0));
COLOR.rgb = textureLod(SCREEN_TEXTURE, uv, 0.0).rgb;
}
Screenshot without shader:
Screenshot with shader:
Upvotes: 0
Reputation: 367
Try this:
float dist = sqrt((vv.x * vv.x) + (vv.z * vv.z));
vv = float4(0.0f, dist * dist * -_Curvature , 0.0f, 0.0f);
also you can add offset:
float dist = sqrt((vv.x * vv.x) + (vv.z * vv.z));
dist = max(0, dist - offset);
vv = float4(0.0f, dist * dist * -_Curvature , 0.0f, 0.0f);
EDIT: I should mention - you can't make a sphere from plane. To make believable "planet" with this method, you need to make chunks and load them while player will be close. Even then it will be just illusion. Greater angle will looks bad too.
Upvotes: 0