What is the equivalent of UnityObjectToClipPos inside Shader Graph??

Hi, I’m pretty new with shaders and started to look more into them in Shader Graph, and trying to convert from a script shader is not resulting on the same results

v2f vert(appdata v) {
    v2f aux;
    aux.vertex = UnityObjectToClipPos(v.vertex);

    float delta = UNITY_Z_0_FAR_FROM_CLIPSPACE(aux.vertex.z);

    aux.vertex.y -= _Magnitude * delta * delta * _ProjectionParams.x;

    return aux;
}

I created this


I’m not really sure on the equivalent way of having the UnityObjectToClipPos and UNITY_Z_0_FAR_FROM_CLIPSPACE applied correctly

Here is a side by side (right road is with shader Graph version)

5373135--544626--Preview.JPG

The bending is more strong on the right and also is happening in more than one axis (intended was only on “y”)
5373135--544623--2.JPG

I searched the documentation and forum but couldn’t find any hint on whats the equivalent for this, any help would be appreciated!

1 Like

There’s a bit of complexity to this question because there are no straight equivalents in Shader Graph, but it can be recreated.

First, the UnityObjectToClipPos() function. That transforms the mesh vertex position from local object space to clip space, as the function implies. It does that by transforming from object to world space, which is easy enough to understand, and then world to clip space using the view projection matrix. Clip space, or more accurately, homogeneous clip space, is a special 4 component space that describes the screen space position of a vertex. If you want to understand that better, I’d search for homogenous clip space and follow that rabbit hole. But the crux of it is you need to multiply the world position (with a w of 1.0) by the View Projection matrix, which you can do easy enough.


Notice I’m reconstructing the Vector3 position into a Vector4. This is because of how matrix & vector multiplication works. Inputing a Vector3 or a Vector4 with a w of 0 will mean the output vector only takes the scale and rotation from the matrix, but not the translation (position). I’m also not using the built in Transform node because it doesn’t have an option for transforming into clip space (though that’d be nice to have).

Next is that UNITY_Z_0_FAR_FROM_CLIPSPACE. That’s to handle the case where Unity uses an reversed Z depth for precision reasons … which it does on all platforms but those running OpenGL. The reason why it does that can be read here:

But that particular macro changes what it does based on what the current build target is. Technically we can handle that in Shader Graph with nodes, but it’d be more expensive than just calling that macro to begin with. So just do that using a Custom Function node.
5375538--544269--upload_2020-1-15_14-47-54.png

Out = UNITY_Z_0_FAR_FROM_CLIPSPACE(In);

Note: For the “Name” field there I just reused the macro’s name. Unity uses the name as the name of the function it generates, which would normally be an issue since it’s the same as the macro, but the actual function name is UNITY_Z_0_FAR_FROM_CLIPSPACE_float so there’s no conflict. It does need a name though, so keep that in mind.

Then it’s just a matter of recreating the rest of the math in node form and you’re done! Right?

Nope.

The original vertex shader example is outputting the clip space position directly, as a vertex shader should. But in Shader Graph you need to output the position in local object space as it always applies the equivalent of the UnityObjectToClipPos() function to that. So, you need to convert to clip space, do the modifications, then convert back to object space so it can go back to clip space. Like before, there’s no nice Transform node to do this, but luckily Shader Graph does have access to the Inverse View Projection matrix, which transforms from clip space back to world space! Then you can use the built in Transform node to convert from world to object space.

So now we’re done!

Still nope. There’s one last wiggle, and to be honest I’m not entirely sure what the cause is. But for some reason going from object to clip space, then back from clip to object space, even without doing anything else, results in the Y being flipped. I’m not sure if this is a bug with Unity, or what. But I solved it by multiplying the Y by the Camera’s “Z Buffer Sign” (which happens to be the same as _ProjectionParam.x). This might not be correct, but it worked for Direct3D since that value is -1 since Direct3D (usually) renders upside down compared to OpenGL and Unity flips the projection matrix to handle that difference and I’m assuming the issue is related.

So here’s the final Shader Graph.


It’s possible that on OpenGL it’s upside down again, or that the magnitude is inverted, but … shrug … I’m honestly not going to spend the time to test this on OpenGL to find out. :wink:

10 Likes

@bgolus you are LEGEND!!
I’ve seen so many of your replies regarding shaders (Always learning from them to solve different issues I found) and was secretly hoping to get one from you for this one =)

The whole explanation is pretty clear and I got it working just needed to remove this multiply cause it was bending upwards instead
5377734--544629--Capture.JPG

I consider this closed and solved, the following part is out of curiosity:

Seeing how limited shadergraph seems regarding some functions and how many extra steps are required, will this have an impact on performance? (this is just a vertex displacement for horizon bending)
Should I stick to the script version that is literally 3 lines?

And the last one is it possible to include the script lines into a custom node and avoid the gigantic graph?
5377734--544647--Custom.JPG
Was to naive to think that the Macro was found maybe the function could resolve too xD

Once again thank you very much!!

2 Likes

Compared to the original hand written shader, yes, the 4 extra matrix multiplies will have something of a performance impact. How much? That depends on how bottle necked you are by vertex count. My guess is the perf difference between having the world bending code vs note will be negligible though, to the point that just looking at raw framerate between the two won’t show more than the usual inconsistency Unity already has between runs.

This was a great thread, thanks everybody for your contributions. I’ve been playing with this technique as well and am puzzled by how taking this approach with a PBR master output node appears to impact cast shadows.

First pic is my expected shadows from a directional light, second is the actual shadows created when applying the curve world shader material on the objects in the scene (except for the blob in the center). Note that there’s no curvature currently applied for comparison sake; when it is on, it doesn’t seem to make a difference in the resulting shadows.

Here’s the graph, which is more or less what’s been described in this thread.

If anyone has any insight to share it would be very appreciated!

The problem is the “view projection” matrix being used is currently rendering view’s view projection matrix. When you’re rendering shadow maps, it’s effectively the view projection matrix from the light, not the main camera, so it doesn’t match. Any bent wold shader that relies on the view and/or projection matrix then can’t properly cast shadows unless you pass in your own matrix from the camera you care about, or only use world space.

2 Likes

Thanks very much @bgolus , that makes sense.

Here’s a graph that seems to do the trick, using world space, for anyone curious:

1 Like

A shader with a custom function calling the UNITY_Z_0_FAR_FROM_CLIPSPACE macro in HDRP won’t compile. What is the proper way to do this in HDRP?

Here is a graph I put together that seems to reproduce what I was trying to do with UNITY_Z_0_ FAR_FROM_CLIPSPACE. I found a helpful explanation of the macro here: Unity3d Lightweight Rendering Pipeline (LWRP) Folk Document - Programmer Sought
I assume this still isn’t the most performant way to do this, or may actually be incorrect, so any input is still welcome.

7076026--841663--shader graph workaround.jpg

forgive-a-n00b, but if this is UnityObjectToClipPos(), which accepts a float3 value, where do these x,y,z values get inserted to that graph, i see 3 empty points on the left, but no indication of what goes where?