-
-
Notifications
You must be signed in to change notification settings - Fork 98
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Custom Camera3D
projection: a practical approach
#11436
Comments
Doing panini/fisheye with a vertex shader will have poor results on meshes that lack subdivision, so a per-pixel approach with oversampling is preferred here. It has a much greater performance cost, but it ensures you don't have to specifically build your assets for it. See also #3779. |
You're right, I should have mentioned it. At the time of writing this proposal I was thinking that some users might need the cheaper vertex-based implementation too (with assets tailored for this). As it is also much simpler to implement, I considered it a valid first step. My intention is to primarily focus this proposal on rectilinear projections support, but of course any views welcome on this too. |
Camera3D
projection : a practical approachCamera3D
projection: a practical approach
I'd like to get an intuition for this. Is there a demo or video somewhere that demonstrates these various effects, like sland and skew? The doom frustum thing would also be good if anyone has an example. (I tried playing with the setting myself but don't get it) |
Perspective
Orthographic
I also quickly bundled a demo project that may help you getting a sense of the frustum's shape in each case.
This is how the projection matrix is set in each case : func _process(delta):
if mode == Mode.Perspective :
if effect == Effect.Symmetrical : matrix = Projection.create_perspective(30, 1.3, 3, 10)
if effect == Effect.Shearing : matrix = create_frustum(30, 1.3, 3, 10, Vector2(0, -0.5))
if effect == Effect.Slanted : matrix = apply_oblique_plane(Vector4(sqrt(0.5)/2, 0, sqrt(3.5)/2, 2.5), Projection.create_perspective(30, 1.3, 3, 10))
if mode == Mode.Orthographic :
matrix = Projection.create_orthogonal(-2, 2, -1.5, 1.5, 3, 10)
if effect == Effect.Symmetrical : pass
if effect == Effect.Shearing : matrix = apply_shear(Vector2(0, -0.5/3), matrix)
if effect == Effect.Slanted : matrix = apply_oblique_plane(Vector4(sqrt(0.5)/2, 0, sqrt(3.5)/2, 5), matrix)
func create_frustum(p_fovy_degrees: float, p_aspect: float, p_z_near: float, p_z_far: float, p_shearing: Vector2, p_flip_fov: bool = false) :
if p_flip_fov :
p_fovy_degrees = Projection.get_fovy(p_fovy_degrees, 1.0 / p_aspect);
var ymax = p_z_near * tan(deg_to_rad(p_fovy_degrees / 2.0))
var xmax = ymax * p_aspect
return Projection.create_frustum(-xmax + p_shearing.x, xmax + p_shearing.x, -ymax + p_shearing.y, ymax + p_shearing.y, p_z_near, p_z_far);
func apply_shear(shear: Vector2, matrix: Projection) :
matrix[2][0] = shear.x
matrix[2][1] = shear.y
return matrix
# Borrowed from https://github.com/godotengine/godot/pull/89140
func apply_oblique_plane(p_oblique_plane: Vector4, matrix: Projection) :
# Here goes oblique magic!
# Eric Lengyel Solution: http://terathon.com/code/oblique.html
var q : Vector4
q.x = (sign(p_oblique_plane.x) + matrix[2][0]) / matrix[0][0]
q.y = (sign(p_oblique_plane.y) + matrix[2][1]) / matrix[1][1]
q.z = -1.0
q.w = (1.0 + matrix[2][2]) / matrix[3][2]
var c = p_oblique_plane * (2.0 / p_oblique_plane.dot(q))
matrix[0][2] = c.x - matrix[0][3]
matrix[1][2] = c.y - matrix[1][3]
matrix[2][2] = c.z - matrix[2][3]
matrix[3][2] = c.w - matrix[3][3]
return matrix |
This is very helpful, thank you! |
I've managed to create the matrix, but there's one thing I can't work out. It's inverted on the z-axis compared to godot's own Projection.create_*() functions. I've tried negating different fields but there's not any difference. Any ideas? Edit: I've discovered that changing the lambda is what's causing the inversion. The whole thing scales to nothing by the time lambda = 0.358 and reverses at 0.0. Edit 2: The lerp formula doesn't work fundamentally I think. Ortho and Perspective will need to remain separate. |
Would this allow for a reverse perspective effect (objects further away get bigger) like such? |
@huwpascoe you messed up with parenthesis at various places in your code. The updated code below works as expected. var p := Projection.IDENTITY
var ln := lerpf(near, 1.0, lambda)
var h := 2.0 * tan(deg_to_rad(fov) / 2.0)
p[0].x = ln * 2.0 / (near * h * aspect)
p[0].y = 0.0
p[0].z = ln * slant.x / near
p[0].w = 0.0
p[1].x = 0.0
p[1].y = ln * 2.0 / (near * h)
p[1].z = ln * slant.y / near
p[1].w = 0.0
p[2].x = ln * shear.x / near
p[2].y = ln * shear.y / near
p[2].z = -lerpf(far + near, 2.0, lambda) / (far - near)
p[2].w = lambda - 1.0
p[3].x = ln * slant.x / near
p[3].y = ln * slant.y / near
p[3].z = -lerpf(2.0 * far * near, far + near, lambda) / (far - near)
p[3].w = lambda There are probably a few details to iron out yet but it's good foundation you can build on I think. |
Great, it's working! May as well make it feature complete. @export var plane: NodePath
...
var obj := get_node_or_null(plane)
if obj is Node3D:
var p := get_camera_transform().affine_inverse() * Plane(-obj.global_basis.z, obj.global_position)
var v := Vector4(p.normal.x, p.normal.y, p.normal.z, -p.d)
matrix = apply_oblique_plane(matrix, v) I tried plugging a separate object into that apply_oblique_plane. It....works? It's certainly tracking the plane. But it's very fragile, only certain angles and positions look like a frustum, everything else explodes. |
Describe the project you are working on
Godot
Describe the problem or limitation you are having in your project
There is a longstanding and strong push from the community to get fine control over the
Camera3D
projection.The most plebiscited solution is to expose the camera’s
Projection
matrix to the end user (#2713, #4932).While it remains a very valid target state, it comes with a number of yet unresolved problems :
An arbitrary matrix may not meet these constraints and have visual and performance side effects. This is the main reason why no attempt has been successful so far (godotengine/godot#85529, godotengine/godot#84454).
Describe the feature / enhancement and how it helps to overcome the problem or limitation
Give back full control solely over the parts of the matrix that really matter for an usage with a camera.
Expose it through an interface that guarantees the above constraints cannot be broken.
Benefits
Use cases
Thorough reading through #2713, #501 and #3779 gives a pretty clear picture of what 💡 users need, what's ✅ already supported, and of what’s ❌ not needed :
💡 Interpolation between perspective and orthographic
💡 Non-rectilinear projections (panini, fisheye, etc...)
Impact on matrix components
Above user needs are intentionally categorized to match the projection matrix' components.
This gives a clear picture of what really needs to be accessed :
With :
💡 Access needed :
Light and dark blue require changes across rendering and picking code.
The rest only requires guardrails at input time regarding aspect ratio and inversibility.
❌ Access not needed :
Giving access to these ones would have extensive impacts across rendering and physics code as it breaks the above assumptions.
Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams
At server level
camera_set_projection()
that lets the user define the projection in terms of [height, near, far, skew, slant, and persp/ortho mix]. This function enforces inversibility and does not constrain aspect ratiocamera_set_projection_*
but reimplement them by wrapping calls to the aboveAt node level
Camera3D
attributes : height, fovy, near, far, persp/ortho mix (hidden by default), plus skew and slant sections :bool
and aVector2
NodePath
for reflect mode and aVector2
for manual modeNode3D
to copy forward direction from (as proposed here). This is convenient for setting up reflections without having to adjust near and far manuallyNon-rectilinear projections
Projections like panini, fisheye and similar require additional changes to the rendering pipeline.
Typically a specific code path in the scene vertex shader is needed to perform a non linear projection before / after the projection matrix is applied.
This is a bit outside of the scope of this proposal, but for later reference :
#ifdef
sections, exactly like dual paraboloid rendering is handled todayCamera3D
interface could have additional presets like PANINI, PARABOLOID, FISHEYE and moreExposing the current dual paraboloid mode to the end-user as an additional projection mode could be a first good place to start, as the bulk of the rendering code is already there.
XR support
Either left as is (the server bypasses entirely the user settings and retrieve the camera directly from the XR api) or reworked so that
XRCamera3D
extracts the camera attributes from the matrix provided by the API, then call the regular rendering server methods.If this enhancement will not be used often, can it be worked around with a few lines of script?
No
Is there a reason why this should be core and not an add-on in the asset library?
It cannot be an add-on
The text was updated successfully, but these errors were encountered: