Metaballs Normals/Specular
Asked Answered
T

10

0

I've been attempting to convert this shader over to Godot: https://www.shadertoy.com/view/NlGcDh

The original shader uses its own lighting calculations, but I'm trying to make it work with Godot's built-in lighting. As far as I can tell all I really need is to give Godot the proper normals and it should "just work". But I'm having issues with that. I'm close, but I've been banging up against the wall for a while now trying to get it fully working.

Here is the code I have at the moment:

shader_type spatial;

#define INF 1e5f

uniform vec4 albedo : source_color = vec4( 1.0f, 0.0f, 0.0f );
uniform float specular : hint_range( 0.0f, 1.0f ) = 1.0f;
uniform float metallic : hint_range( 0.0f, 1.0f ) = 0.0f;
uniform float roughness : hint_range( 0.0f, 1.0f ) = 0.2f;

uniform vec3 positions[5];
uniform int amount = 1;
uniform float radius : hint_range( 0.0f, 100.0f ) = 1.0f;

uniform float max_distance : hint_range( 0.0f, 1000.0f ) = 20.0f;
uniform int max_iterations : hint_range( 0, 5000 ) = 200;
uniform float step_size : hint_range( 0.0f, 1.0f ) = 0.005f;

struct Hit
{
	float t;
	vec3 point;
	vec3 normal;
	vec3 color;
	int objId;
};

void swap( inout float x0, inout float x1 )
{
	float tmp = x0;
	x0 = x1;
	x1 = tmp;
}

bool solveQuadratic( float a, float b, float c, out float x0, out float x1 )
{
	float delta = b * b - 4.0f * a * c;

	if ( delta < 0.0f )
	{
		return false;
	}
	else if ( delta == 0.0f )
	{
		x0 = x1 = -0.5f * b / a;
	}
	else
	{
		float q = ( b > 0.0f ) ? -0.5f * ( b + sqrt( delta ) ) : -0.5f * ( b - sqrt( delta ) );
		x0 = q / a;
		x1 = c / q;
	}

	return true;
}

bool traceSphere( vec3 eye, vec3 ray, vec3 center, out Hit hit )
{
	float t0, t1;

	vec3 L = eye - center;
	float a = dot( ray, ray );
	float b = 2.0f * dot( ray, L );
	float rad = radius * 0.9f;
	float c = dot( L, L ) - ( rad * rad );
	if ( ! solveQuadratic( a, b, c, t0, t1 ) ) return false;

	if ( t0 > t1 ) swap( t0, t1 );

	if ( t0 < 0.0f )
	{
		t0 = t1;
		if ( t0 < 0.0f ) return false;
	}

	hit.t = t0;
	hit.point = eye + t0 * ray;
	hit.normal = normalize( hit.point - center );

	return true;
}

void getFalloff( float x, out float f )
{
	f = 0.0f;
	if ( x < 0.01f || x > 1.0f ) return;

	f = 1.0f -( x * x * x * ( 6.0f * x * x -15.0f * x + 10.0f ) );
}

void getFalloffDerivative( float x, out float df )
{
	df = 0.0f;
	if ( x < 0.01f || x > 1.0f ) return;

	df = -( x * x * ( 30.0f * x * x - 60.0f * x + 30.0f ) );
}

bool getFieldValues( vec3 p, float threshold, out float value, out vec3 normal, out vec3 color )
{
	normal = vec3( 0.0f );
	float f, df;
	value = 0.0f;
	float rad = radius * 0.9f;

	for ( int i = 0; i < amount; i++ )
	{
		float r = length( p - positions[ i ] );

		if ( length( p - positions[ i ] ) > rad ) continue;

		float d = length( ( p - positions[ i ] ) / rad );

		getFalloff( d, f );

		value += f;

		color = vec3( 1.0f );
	}

	if ( value >= threshold )
	{
		float df = 0.0f;

		for( int i = 0; i < amount; i++ )
		{
			float d = length( ( p - positions[ i ] ) / rad );
			getFalloffDerivative( d, df );

			normal += df * normalize( positions[ i ] - p );
		}

		normal = normalize( normal );

		return true;
	}

	return false;
}

Hit rayTraceFieldBoudingSpheres( vec3 origin, vec3 dir )
{
	float minT = INF;
	float point;
	Hit hit, result;

	result.t = INF;
	for ( int i = 0; i < amount; i++ )
	{
		if ( traceSphere( origin, dir, positions[ i ], hit ) && hit.t < result.t )
		{
			result.t = hit.t;
			result.point = origin + result.t * dir;
			result.objId = 1;
		}
	}

	return result;
}

vec4 castRay( vec3 origin, vec3 dir, out Hit hit )
{
	vec3 fieldNormal;

	hit = rayTraceFieldBoudingSpheres( origin, dir );

	float t = hit.t;
	if ( t == INF ) return vec4( 0.0f );

	vec3 p;
	vec3 color;
	float value = 0.0f;
	float threshold = 0.4f;
	for ( int i = 0; i < max_iterations; i++ )
	{
		if ( t > max_distance ) break;
		t += step_size;
		p = origin + t * dir;

		if ( getFieldValues( p, threshold, value, fieldNormal, color ) )
		{
			hit.t = t;
			hit.point = p;
			hit.normal = fieldNormal;
			hit.objId = 1;
			hit.color = color;
			break;
		}
	}

	if ( value < 0.4f ) return vec4( 0.0f );

	return vec4( 1.0f );
}

void vertex()
{
	POSITION = vec4( VERTEX, 1.0f );
}

void fragment()
{
	vec3 ndc = vec3( SCREEN_UV, 0.0f ) * 2.0f - 1.0f;
	vec4 view_coords = INV_PROJECTION_MATRIX * vec4( ndc, 1.0f );
	view_coords.xyz /= view_coords.w;
	vec3 world_cam_pos = ( INV_VIEW_MATRIX * vec4( 0.0f, 0.0f, 0.0f, 1.0f ) ).xyz;
	vec4 world_coords = INV_VIEW_MATRIX * vec4( view_coords.xyz, 1.0f );
	vec3 ray_origin = world_coords.xyz;
	vec3 ray_dir = normalize( world_coords.xyz - world_cam_pos );

	Hit ray_hit;
	vec4 ray_result = castRay( ray_origin, ray_dir, ray_hit );

	ALBEDO = albedo.rgb;
	ALPHA = ray_result.a * albedo.a;

	vec4 sdf_ndc = PROJECTION_MATRIX * VIEW_MATRIX * vec4( ray_origin + ray_dir * ray_hit.t, 1.0f );
	DEPTH = ( sdf_ndc.z / sdf_ndc.w );
	NORMAL = ( VIEW_MATRIX * vec4( ray_hit.normal, 0.0f ) ).xyz;

	SPECULAR = specular;
	METALLIC = metallic;
	ROUGHNESS = roughness;
}

As I said, it's mostly working, but there is some sort of issue with the normals (or something). It is visible when the specular is turned way up and roughness way down. Basically, it does have specular on it, but it's not quite correctly positioned on the ball and it has some odd distortions when you move the camera around to look at the ball(s) from different angles. Putting a standard sphere mesh into the scene and using the standard shader with specular turned way up shows correct results, so that is what I've been comparing to.

It's a metaballS shader, but for testing I've just been setting it to 1 ball to keep things simple.

If needed, I could maybe try to get a video of the issue, but I guess someone who is more adept at shaders will probably be able to see issues just looking at the code.
Also if any other info would help, just let me know.

I would greatly appreciate any help. Thanks! 🙂

Tawannatawdry answered 23/10, 2023 at 12:27 Comment(0)
M
0

Tawannatawdry Please post a video or better yet an example project.

One way to check normals is to set render mode to unshaded and assign the normal vector to albedo. You can check both, world space normals and view space normals.

Millardmillboard answered 23/10, 2023 at 12:38 Comment(0)
T
0

Ok, I have captured a video (not great quality, sorry for that). The sphere on the right is the metaball shader, the one on the left is a standard mesh sphere with the standard shader.

I can make an example project if really needed. It's pretty simple though. Just a 2x2 quad with flip faces on and the shader added to a material on the quad.

Tawannatawdry answered 24/10, 2023 at 1:54 Comment(0)
M
0

Tawannatawdry Certainly looks like something went askew with normals.

Millardmillboard answered 24/10, 2023 at 2:10 Comment(0)
M
0

Tawannatawdry The quad needs to have identity model matrix. Eliminate any translation, rotation and scaling from the mesh instance. Or alternatively make it a camera facing sprite (or a standard billboard) and remove the assignment to POSITION in vertex function.

Millardmillboard answered 24/10, 2023 at 2:59 Comment(0)
T
0

Millardmillboard Hmm... Well I definitely don't have any translation/rotation on the quad mesh. It's at 0,0,0 position, 0,0,0 rotation, and 1,1,1 scale.

As for billboards, how would that help? I believe the idea is that the quad is fullscreen because this is sort of like a post-process effect over the whole screen, not applied to individual meshes.

Tawannatawdry answered 24/10, 2023 at 5:19 Comment(0)
M
0

Tawannatawdry Upload the scene.
Something in it is messing up the model matrix. Although the shader ignores it, Godot uses it further down the pipeline to calculate view dependent illumination components i.e. specular and environment reflections.

Millardmillboard answered 24/10, 2023 at 11:29 Comment(0)
T
0

So I think I actually found something that fixes it. Now I only have this in the vertex function:

VERTEX = vec3( VERTEX.xy * VIEWPORT_SIZE, ( INV_PROJECTION_MATRIX * vec4( 0.0f, 0.0f, 0.0f, 1.0f ) ).z );

It now seems to work and the normals issue is gone. Is this an ok solution or will it possibly cause other issues?

I'm guessing that code might not work if you don't have the quad as a child of the camera, but in my case I do so I guess that is ok.

Thanks for the help!

Tawannatawdry answered 24/10, 2023 at 15:41 Comment(0)
M
0

Tawannatawdry If the quad is a child of the camera then its matrix is not identity as it inherits transformations from camera's model matrix. That's what's causing the problem, as I already stated in my previous post.

But if the quad is parented to camera, you actually don't need to do anything in the vertex function because the quad will always cover the entire view. And that was the sole purpose of intervening in the vertex function in the first place.

The core of the problem is the discrepancy of the model transformation used in vertex function (where you interfere with regular model matrix transformation) and that used by Godot's specular calculation that happens after the shader code, where regular model matrix is used.

Millardmillboard answered 24/10, 2023 at 15:56 Comment(0)
T
0

Millardmillboard Yeah, the issue though is even when I didn't have it parented to the camera it still had the issue. I had it in the root of the scene with no transformations at all, and it still didn't work.

Tawannatawdry answered 25/10, 2023 at 11:17 Comment(0)
M
0

Tawannatawdry There shouldn't be issues if quad's global model matrix is identity. Upload the project and I can take a look if you want.

Millardmillboard answered 25/10, 2023 at 13:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.