🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

OpenGL 4 reflections

Started by
110 comments, last by taby 1 year, 7 months ago

Not sure if it’s against the rules to say so… I have $150 USD in credits from a long time ago at codementor.io. if anyone solves the problem, I’ll set up a job for you, and you can claim the reward.

Advertisement

The code is at:

float height = distance(board_mesh.get_y_min(), board_mesh.get_y_max(x, y));
float normalized_height = height / board_mesh.get_y_extent();

vec3 centre = board_mesh.get_centre(x, y);
model = translate(model, -centre);

// How do I mirror around an arbitrary height?
model = scale(model, vec3(1, -1, 1));

centre.y = -centre.y;

model = translate(model, centre);

model = translate(model, vec3(0, board_mesh.get_y_extent(x, y) * 2, 0));

I also took off the sides of the board:

Lots of culling went on during load

taby said:
Doesn't matter if the character is standing on firm ground, the reflection will always happen under its feet. Kind of a hack?

Yep, it's a hack ofc. This is the problems i expect, noticeable mostly if the character is in motion:

Hack: Set the reflection plane to the height of terrain found under the characters feet.
Problem: If there are discontinuities of height in the terrain, like when going from one column to the next, The reflection will jump up and down.
This not only reveals the hack, but causes a distracting interruption of the players immersion. Likely not acceptable.

Improved hack: Use a filtered estimation of height, so if the character moves, height will change smoothly, even if the terrain has sharp discontinuities.
Problem: The reflection moves up and down even if we walk on a patch of constant height but some cliff is nearby.
We get the impression of a floating reflection, disconnected from the terrain.

To make it work acceptable, you have to use a smooth terrain, or enable reflections only on smooth patches of terrain. Cliffs might need a material which is not reflective.
To get reflections of a nearby cliff on the smooth terrain, the cliff column needs to search for nearby reflecting terrain at lower heights, and calculate some average surface height from them to get a ‘best fit’ reflection plane for itself.
This will tear adjacent cliff columns apart in the reflections, because they calculate slightly different reflection plane heights.
So, for the purpose of reflections, you want to divide your terrain into chunks at higher resolutions horizontally, to minimize the tearing effect (if that's a visual problem). So a compromise of visual accuracy vs. draw call count.

If you are cautious with content generation, the trick can work well enough i guess.
And i have never seen this done. It's an interesting idea worth to explore further. But it will constrain your world design to some degree.

taby said:
float height = distance(board_mesh.get_y_min(), board_mesh.get_y_max(x, y)); float normalized_height = height / board_mesh.get_y_extent(); vec3 centre = board_mesh.get_centre(x, y); model = translate(model, -centre);

It looks you calculate the local reflection plane only from the columns own bounding box.
But i think this can't work, because it ignores the actual surfaces where the reflection most likely appears, which are the adjacent columns.

That's what i meant with this:

JoeJ said:
To get reflections of a nearby cliff on the smooth terrain, the cliff column needs to search for nearby reflecting terrain at lower heights, and calculate some average surface height from them to get a ‘best fit’ reflection plane for itself.

But we can do better than that, if we accept some runtime cost. So here's the (eventually) improved idea (all on CPU):

On level load, make a heightmap data structure, containing the max height of each column per texel. (or better the average of all max heights per voxel column)

On draw, for each column, calculate a ‘reflection’ vector from the current column towards the current camera position, and set the vector to the length of one texel.
Sample the height map at the position (current column texel center + reflection vector), to get the averaged surface height where most of the reflection should show up, and use this for the local reflection plane for our current column.

This should give you results at minimized error. If you rotate the camera around the scene, reflected columns will wobble slowly up and down, which i guess is acceptable, depending on content.
To minimize discontinuities in this wobbling motion, you need a texture filter which has no discontinuities. So no bilinear filter (2x2 kernel), but a cubic filter (3x3 kernel).

I can give an example for such filter:



template <typename T>
	static T SampleTexture3 (
		const T *texture, // image data
		const int *gridI, // floored integer coords of sample position
		const float *gridF, // the fraction of the sample position we add to the integer coord
		const int *size) // 2d array containing the texture dimension
	{
		using vec = Vectormath::Aos::Vector3; // should be a vec2, but i only have vec3

		auto square = [] (const vec &v)
		{
			return mulPerElem(v,v); // = return Vec3(v.x*v.x, v.y*v.y, v.z*v.z or 0 as it's never used); 
		};

		vec p (gridF[0], gridF[1], 0); 
		vec fw[3]; // cubic weights of the filter kernel
		{
			fw[0] = mulPerElem (vec(.5f), square(vec(1.f) - p));
			fw[1] = vec(.75f) - square(p - vec(.5f));
			fw[2] = mulPerElem (vec(.5f), square(p));
		}

		T sum (0);
		for (int j=0; j<3; j++)
		for (int i=0; i<3; i++)
		{
			float w = fw[i][0] * fw[j][1]; // weight of the current texel
			
			int X = gridI[0] + i;
			int Y = gridI[1] + j;
			
			X = max(0, min(size[0]-1, X));
			Y = max(0, min(size[1]-1, Y));
			{
				int g = X + Y * size[0];
				sum += texture[g] * w;
			}
		}
		return sum;
	}

Notice the input sample coords do not go from 0 to 1, but from 0 to the size of the texture in texels, which differs from the usual convention we have on GPU.

Notice this also gives you a smooth ground height for the character (and other dynamic objects). Just sample the height map texture with it's coordinates.

OK, yes, I'll definitely take into account adjacent reflection plane heights. How would I go about flipping things using a custom reflection plane? I know it's a simple question, but I sort of suck at linear algebra. :(

These lines of code that you gave are just brilliant!

X = max(0, min(size[0]-1, X));
Y = max(0, min(size[1]-1, Y));

So far, I've added this code:

		for (size_t x = 0; x < board_mesh.num_cells_wide; x++)
		{
			for (size_t y = 0; y < board_mesh.num_cells_wide; y++)
			{
				mat4 model = board_mesh.model_mat;




				float height = distance(board_mesh.get_y_min(), board_mesh.get_y_max(x, y));
				float normalized_height = height / board_mesh.get_y_extent();

				long long signed int x_plus_start = x + 1;
				long long signed int x_minus_start = x - 1;

				if (x_minus_start < 0)
					x_minus_start = 0;

				if (x_plus_start > board_mesh.num_cells_wide - 1)
					x_plus_start = board_mesh.num_cells_wide - 1;

				long long signed int y_plus_start = y + 1;
				long long signed int y_minus_start = y - 1;

				if (y_minus_start < 0)
					y_minus_start = 0;

				if (y_plus_start > board_mesh.num_cells_wide - 1)
					y_plus_start = board_mesh.num_cells_wide - 1;



				float average_height = 0;
				float current_height = distance(board_mesh.get_y_min(), board_mesh.get_y_max(x, y));

//				average_height += current_height;
				size_t count = 0;

				for (long long unsigned int i = x_minus_start; i <= x_plus_start; i++)
				{
					for (long long unsigned int j = y_minus_start; j <= y_plus_start; j++)
					{
						size_t cell_index = i * board_mesh.num_cells_wide + j;

						float neighbour_height = distance(board_mesh.get_y_min(), board_mesh.get_y_max(i, j));

						if (neighbour_height <= current_height)
						{
							average_height += neighbour_height;

							count++;
						}
					}
				}

				average_height /= count;

				//board_mesh.mirror_y(x, y);

				model = translate(model, vec3(0, -board_mesh.get_y_extent(x, y) * 2, 0));

				//vec3 centre = board_mesh.get_centre(x, y);
				//model = translate(model, -centre);

				//// How do I mirror around an arbitrary height?
				model = scale(model, vec3(1, -1, 1));

				//centre.y = -centre.y;

				//model = translate(model, centre);





				//vec3 centre = board_mesh.get_centre(x, y);
				//model = translate(model, -centre);

				//// How do I mirror around an arbitrary height?
				//model = scale(model, vec3(1, -1, 1));

				//centre.y = -centre.y;

				//model = translate(model, centre);




				glUniformMatrix4fv(glGetUniformLocation(point_shader.get_program(), "model"), 1, GL_FALSE, &model[0][0]);

				board_mesh.draw(point_shader.get_program(), x, y, win_x, win_y, "board.png", "board_specular.png");

				//board_mesh.mirror_y(x, y);
			}
		}

I moved that code into mesh.h. Now the draw code is:

mat4 model = board_mesh.model_mat;

vector<float> neighbour_heights;
board_mesh.get_lower_neighbour_heights(x, y, neighbour_heights);

cout << neighbour_heights.size() << endl;

float current_height = distance(board_mesh.get_y_min(), board_mesh.get_y_max(x, y));

model = translate(model, vec3(0, -board_mesh.get_y_extent(x, y) * 2, 0));

//// How do I mirror around an arbitrary height?
model = scale(model, vec3(1, -1, 1));

P.S. Your thinking is infectious. I think I see what you mean now… I must draw each cell once per neighbour, based on neighbour height. Correct?

taby said:
These lines of code that you gave are just brilliant! X = max(0, min(size[0]-1, X));

What's brilliant about clamping a number?

taby said:
How would I go about flipping things using a custom reflection plane? I know it's a simple question, but I sort of suck at linear algebra. :(

Hard to help because i don't know what's your initial matrices, what the math functions precisely do, etc.
E.g. i have no idea what this does: float height = distance(board_mesh.get_y_min(), board_mesh.get_y_max(x, y));

But it's easy to figure out if you visualize your matrices.
I use functions like this:

inline void RenderMat3 (sVec3 pos, sMat3 m, float scale = 1.0f)
	{
		RenderVector (pos, sVec3(m[0]*scale), 1,0,0); // draws a red line starting at pos
		RenderVector (pos, sVec3(m[1]*scale), 0,1,0);
		RenderVector (pos, sVec3(m[2]*scale), 0,0,1);
	}
	inline void RenderMat4 (sMat4 m, float scale = 1.0f)
	{
		RenderVector (m[3], sVec3(m[0]*scale), 1,0,0);
		RenderVector (m[3], sVec3(m[1]*scale), 0,1,0);
		RenderVector (m[3], sVec3(m[2]*scale), 0,0,1);
	}

This draws the usual RGB axis associated to XYZ orientation vectors making the 3x3 matrix (upper left of a 4x4), at the position given at the 4th column (or row?) of a 4x4.
I think i saw this in your screenshots, so you do the same already?
I do such visualizations all the time when working on geometrical things. It takes no effort to look at visualizations and understanding what's going on.
But it takes a lot of effort if i only look at code and numbers, and i have to imagine what it does.

Anyway, it shows you all information a transfrom matrix has, so there is no real magic going on requiring advanced linear algebra knowledge. I look at those things purely geometrically, and it works.
If i want to translate a matrix, i do just mat[3] += displacementVector.
If i want to flip it along Y, i do mat[1] *= -1;

Maybe it helps to get more familiar with it this way, as tooling functions like translate() or scale() create a form of abstraction preventing to understand the simple details,
and also creating uncertainties like ‘If i do scale(), does this affect the position too or only the 3x3 part defining orientation?’

The tooling is only needed for rotations. But you don't need this here.


LOL. You're too modest. I am working on the code. You've helped tremendously.

P.S. boardmesh.model_mat is initiallly mat4(1.0f). where the mesh data are centred around the origin. So all I'm dong is one translation then one scale.

P.P.S. height = distance(board_mesh.get_y_min(), board_mesh.get_y_max(x, y)); gets the distance from the game board's minimum y to the cell's maximum y.

The code is now:

mat4 model = board_mesh.model_mat; // This is equal to matf4(1.0f);


vector<float> neighbour_heights;
board_mesh.get_lower_neighbour_heights(x, y, neighbour_heights);

float current_height = distance(board_mesh.get_y_min(), board_mesh.get_y_max(x, y));

model = translate(model, vec3(0, -board_mesh.get_y_extent(x, y) * 2, 0));

//// How do I mirror around an arbitrary height?
model = scale(model, vec3(1, -1, 1));

I am absolutely convinced that drawing reflections once per neighbour will do the trick! You're a genius, man.

This topic is closed to new replies.

Advertisement