Skip to content
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

[idea] pack 4 surface lightmaps into one? #1095

Open
BraXi opened this issue Mar 23, 2024 · 8 comments
Open

[idea] pack 4 surface lightmaps into one? #1095

BraXi opened this issue Mar 23, 2024 · 8 comments
Labels

Comments

@BraXi
Copy link
Contributor

BraXi commented Mar 23, 2024

Digging through the lighting code I've noticed that there are 4 lightmap textures being created for each lit surface, which is understandable given thats the maximum count of lightmaps per surface, but It could be improved to build a single texture containing 4 lightmaps which in return would result in less texture binds when drawing lightmapped world geometry and likely improve fps a little. I've only checked GL3 renderpath and I assume GL4 does the same.

I think the benefits are pretty noticeable

  • one instead of four lightmap texture binds per lightmapped surface
  • four times less lightmap textures (MAX_LIGHTMAPS vs MAX_LIGHTMAPS*4)
  • no need to switch texture units (currently tmu1..4), lighting could be done on TMU 1 all the time
  • Seems like a good way to also implement missing r_lightmap (set tmu0 to white texture) and r_fullbright (set tmu1 to white) :)

Yamagi:

void
LM_UploadBlock(void)
{
	int map;

	// NOTE: we don't use the dynamic lightmap anymore - all lightmaps are loaded at level load
	//       and not changed after that. they're blended dynamically depending on light styles
	//       though, and dynamic lights are (will be) applied in shader, hopefully per fragment.

	GL3_BindLightmap(gl3_lms.current_lightmap_texture);

	// upload all 4 lightmaps
	for(map=0; map < MAX_LIGHTMAPS_PER_SURFACE; ++map)
	{
		GL3_SelectTMU(GL_TEXTURE1+map); // this relies on GL_TEXTURE2 being GL_TEXTURE1+1 etc
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

		glTexImage2D(GL_TEXTURE_2D, 0, GL_LIGHTMAP_FORMAT,
		             BLOCK_WIDTH, BLOCK_HEIGHT, 0, GL_LIGHTMAP_FORMAT,
		             GL_UNSIGNED_BYTE, gl3_lms.lightmap_buffers[map]);
	}

	if (++gl3_lms.current_lightmap_texture == MAX_LIGHTMAPS)
	{
		Com_Error(ERR_DROP, "%s: MAX_LIGHTMAPS exceeded\n", __func__);
	}
}

For reference, in my own project I upload all 4 surface lightmaps into a single texture:

static void R_LightMap_UploadBlock()
{
	R_BindTexture(gl_state.lightmap_textures + gl_lms.current_lightmap_texture);

	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

	//glTexImage2D(GL_TEXTURE_2D, 0, LIGHTMAP_GL_FORMAT, LM_BLOCK_WIDTH, LM_BLOCK_HEIGHT, 0, LIGHTMAP_GL_FORMAT, GL_UNSIGNED_BYTE, gl_lms.lightmap_buffers[0]);
	glTexImage2D(GL_TEXTURE_2D, 0, LIGHTMAP_GL_FORMAT, LM_BLOCK_WIDTH*2, LM_BLOCK_HEIGHT*2, 0, LIGHTMAP_GL_FORMAT, GL_UNSIGNED_BYTE, 0);
	glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, LM_BLOCK_WIDTH, LM_BLOCK_HEIGHT, LIGHTMAP_GL_FORMAT, GL_UNSIGNED_BYTE, gl_lms.lightmap_buffers[0]);
	glTexSubImage2D(GL_TEXTURE_2D, 0, LM_BLOCK_WIDTH, 0, LM_BLOCK_WIDTH, LM_BLOCK_HEIGHT, LIGHTMAP_GL_FORMAT, GL_UNSIGNED_BYTE, gl_lms.lightmap_buffers[1]);
	glTexSubImage2D(GL_TEXTURE_2D, 0, 0, LM_BLOCK_HEIGHT, LM_BLOCK_WIDTH, LM_BLOCK_HEIGHT, LIGHTMAP_GL_FORMAT, GL_UNSIGNED_BYTE, gl_lms.lightmap_buffers[2]);
	glTexSubImage2D(GL_TEXTURE_2D, 0, LM_BLOCK_WIDTH, LM_BLOCK_HEIGHT, LM_BLOCK_WIDTH, LM_BLOCK_HEIGHT, LIGHTMAP_GL_FORMAT, GL_UNSIGNED_BYTE, gl_lms.lightmap_buffers[3]);

	if (++gl_lms.current_lightmap_texture == MAX_LIGHTMAPS)
		ri.Error(ERR_DROP, "%s - MAX_LIGHTMAPS exceeded\n", __FUNCTION__);
}

and later blend them together in shader:

	// lightmap 0
	vec2 tc = vec2(LMTexCoord.x / 2.0, LMTexCoord.x / 2.0);
	lmfrag = texture2D(lightmap, tc) * lightmap_scales.r;	

	// lightmap 1
	tc.x += 0.5;
	lmfrag += texture2D(lightmap, tc) * lightmap_scales.g;
	
	// lightmap 2
	tc.x -= 0.5;
	tc.y += 0.5;
	lmfrag += texture2D(lightmap, tc) * lightmap_scales.b;	

	// lightmap 3
	tc.x += 0.5;
	lmfrag += texture2D(lightmap, tc) * lightmap_scales.a;
	
	gl_FragColor = frag * lmfrag;
	gl_FragColor.a = 1.0;```
	
	
Looking for comments on this before I dive deeper into the code
@protocultor
Copy link
Contributor

I have a couple of questions, that come from a deep ignorance of the matter:

1.- From what I understand, in practical terms you did an "atlassed" texture from the 4 lightmaps.
How did you recalculate texture coordinates for the lightmap to stay "aligned" to the environment?
The section of the fragment shader you pasted here (which, by the way, looks like a GL 2.0 shader) doesn't leave it clear.

2.- Something that only may affect the Remaster project. The Remaster (or Rerelease) uses a 4 times bigger lightmap according to this. Could it have a negative effect on graphics cards that do not have a very high resolution available for each texture uploaded? I'm talking about ancient hardware here, but it's just to know.

@BraXi
Copy link
Contributor Author

BraXi commented Mar 24, 2024

  1. Engine code doesn't have to adjust lightmap texcoords, since we pack 4 textures into one we can reuse LmTexCoord by dividing it by 2, thus our lightmap coords in shaders are as follows [S0-0.5 T0-0.5] is map 0, [S0.5-1.0 T0-0.5] is map2 and so on, and we can then offset texcoords by 0.5 to match atlas

  2. Lightmaps are built internaly during level load and are converted into blocks of 256x256 and uploaded to GPU, in our case the 4in1 lightmap is 512x512px (which is the regular texture size in a 2004 year game). Basicaly, the higher the lightmap resolution in map is the more lightmap blocks (textures) we build (max_lightmaps). Original Quake2 lightmap density is 1/16 and it used 128*128px blocks internaly, in rerelease its 1/8 (decoupledlm with texel density 8) and needs twice as many lightmaps as the same level compiled with 1/16. I have compiled a level with absurdy high 1/2 texel density and it loaded fine. This implementation doesn't change the limits in any way.

@0lvin
Copy link
Contributor

0lvin commented Mar 24, 2024

I suppose size of light map and count of light map textures for gl1 was just most common limit size of textures of available gpu's.

Gl3 render use bigger textures and less light maps textures as gpu's has much bigger memory limit value.

I have found only one map in Rerelease that requires change count of light maps.

@protocultor
Copy link
Contributor

Thanks for the answers @BraXi !

I have found only one map in Rerelease that requires change count of light maps.

This was what worried me a bit, how a change like that would behave on older hardware.
I think it's something to keep in mind, after all GL 3.2 is supported on 2007 hardware, even if it appears 2 years later.

@0lvin
Copy link
Contributor

0lvin commented Mar 24, 2024

I have not tested on old hardware, in same time I suppose it should be supported without issues as I have not changed light map sizes in gl1 render and render loads all Rerelease maps without issues related to textures count or size .

@BraXi
Copy link
Contributor Author

BraXi commented Mar 24, 2024

2024-03-24.20-15-03.mp4

I have implemented lightmaps that way in my own engine and done a test by pushing lightmap scale to the max and running many lightstyles at once, seems pretty solid and no graphical glitches happening, level compiled with light -threads 8 -extra -bounce -dirt -world_units_per_luxel 2 which is multiple times higher lightmap than remaster

the lightmap bindings count is pretty huge at that texel density anyway , with default lightmap resolution its much much lower

bump2

@protocultor
Copy link
Contributor

I was originally being confused about the 4 different lightmaps and their usage in GL3.

As @0lvin said, reading the GL3 code makes clear that the size of one lightmap is 1024x512, which are equivalent to 32 lightmaps in the old 128x128 dimensions.
This is why there are "4 lightmaps max", because with 4 of these big ones, you cover the same area as 128 lightmaps with the old 128x128 dimensions (128 was the old value of MAX_LIGHTMAPS).

The usage of 4 "lightmaps per surface" I don't fully get, but are supposed to be "used for switching on/off light and stuff like that", according to comments on the code. But after that, they recognize that "most surfaces only have one really and the remaining for are filled with dummy data". So, they are empty in most cases:
https://github.com/yquake2/yquake2/blob/master/src/client/refresh/gl3/gl3_light.c#L390

Number of lightmaps come from here:
https://github.com/yquake2/yquake2/blob/master/src/client/refresh/gl3/gl3_light.c#L314

...but after having done multitexturing in GL1, I agree with @BraXi that 2 TMUs (one for color, the other for light) are enough for most use cases in Quake 2. Didn't dynamic lights on GL3 supposed to come from a fragment shader? AFAIK that was the main motivation behind having 4 "styles" for each surface.

Still, seeing all of this was a good excuse for my new PR, where I implemented 512x512 lightmaps in GL1 by just generating them in that size, so we save ourselves the trouble of converting coordinates for each "sub-lightmap" afterwards.

@apartfromtime
Copy link
Contributor

apartfromtime commented May 25, 2024

...
The usage of 4 "lightmaps per surface" I don't fully get, but are supposed to be "used for switching on/off light and stuff like that", according to comments on the code. But after that, they recognize that "most surfaces only have one really and the remaining for are filled with dummy data". So, they are empty in most cases: https://github.com/yquake2/yquake2/blob/master/src/client/refresh/gl3/gl3_light.c#L390

...but after having done multitexturing in GL1, I agree with @BraXi that 2 TMUs (one for color, the other for light) are enough for most use cases in Quake 2. Didn't dynamic lights on GL3 supposed to come from a fragment shader? AFAIK that was the main motivation behind having 4 "styles" for each surface.

This relates to the tools source, there is a max number of 32 static light styles (normal, flicker, pulse, strobe, etc) that can be applied to a surface, where the engine only ever applied 4 per surface. Likely for performance, memory or time constraints.

Max number of "lightstyles" MAX_STYLES = 32.
#L1132

Anything over MAXLIGHTMAPS = 4, gets clamped.
#L1258

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants