Source-like/3D models skybox & rendering sky shader to texture

Using Godot 4.3 dev 3

I have two questions regarding sky shader & skies. The quick version:

  1. Is there any way to make a skybox made from ingame 3D models that appear far into the distance, behind all of the regular geometry (like source engine games/maps) ?

  2. Is there a way to “render” the current sky shader to a (panorama) texture, so that it can be render once and then used in a static PanoramaSky later ?

More details:
1/ All tutorials on the subject point to the usage of multiple viewports, which seems really costly/ineffective to me (i tried it in godot 3 and having multiple camera was really costly on my poor ipgu, even with minimal cull masks).
I know for first person viewmodels, it’s common to pull a reprojection shader trick to make the model appear in front of everything, but I don’t know if it would be applicable for skyboxes.

2/ My basic sky shader (that depends on TIME) is very expensive on my laptop with an igpu, even with minimal radiance size, high quality incremental process mode, and a flat color for AT_CUBEMAP_PASS. So I figured I could have a “static sky” option that’s just a PanoramaSky.
But I can’t really see how to render a scene as a 360° panorama texture, is there a way to do this in godot ?

To answer your questions:

  1. Why do you want to render 3D models that are far away into the skybox? What you can do instead are the following options:
    • Use LODs to save up on the vertices you are rendering. Since the models are further away, less detail is visible and thereby not needed. You can use the built-in LOD options for this
    • Render the models into sprites and render those as billboards when reaching a specific distance (similar to 1.)
  2. If I am understanding you correctly you are using simply a flar color sky shader which is taking too much of a toll on your integrated GPU? Am I getting this right? What you CAN do is use Blender to create a highly detailed environment and then render that into a texture. You can find a tutorial for this here.

Thanks for the answers !

  1. It would allow to easily share a “background” (landscapes, buildings, etc) between multiple scenes, that stretch to the horizon (and even move a bit when the camera move to give the illusion that’s it’s really there). Without having to depend on an huge draw distance.

  2. Not exactly flat, but yeah that’s the idea, the “setup sky” step in visual profiler is very costly (shader posted below, but I don’t think any optimization would make a difference, even if i removed the few ‘if’, it’s a simple “scrolling grid” background).
    I’m asking if it’s possible in Godot directly, because that would allow me to keep the exact same “look” as the game for the skybox : same materials, same sky shaders.

Bonus, the culprit cool scrolling grid / virtual-training-room-like shader (that’s way too expensive for what it is):

shader_type sky;
render_mode use_half_res_pass, use_quarter_res_pass;

uniform vec3 sky_color : source_color = vec3( 0.05, 0.05, 0.05 );
uniform vec3 line_color : source_color = vec3( 0.08, 0.2, 0.1 );
uniform vec3 cube_size = vec3(1.,1.,1.);
uniform vec3 cube_offset = vec3(1.,1.,1.);
uniform float line_spacing = 0.01;
uniform float time_scale = 0.5;
uniform float line_width = 0.01;


void sky() {
	if (AT_CUBEMAP_PASS || AT_HALF_RES_PASS || AT_QUARTER_RES_PASS) {
        COLOR = (1.-line_width) * sky_color + line_width*line_color;
    }
	else
	{
		vec2 sky_uv_x = EYEDIR.yz / EYEDIR.x * cube_size.x;
	   	vec2 sky_uv_y = EYEDIR.xz / EYEDIR.y * cube_size.y;
		vec2 sky_uv_z = EYEDIR.xy / EYEDIR.z * cube_size.z;
		
		vec2 selected_uv = sky_uv_x;
		float other_axis = EYEDIR.x;
		if (length(sky_uv_y) < length(selected_uv))
		{
			selected_uv = sky_uv_y;
			other_axis = EYEDIR.y;
		}
		if (length(sky_uv_z) < length(selected_uv))
		{
			selected_uv = sky_uv_z;
			other_axis = EYEDIR.z;
		}
		
		float lines = (sin(TIME * time_scale + selected_uv * sign(other_axis) / line_spacing).x+1.)/2.;
		lines = (lines+1.)/2.;
		lines = step(1.-line_width, lines);
		float lines_2 = (sin(TIME * time_scale + selected_uv  * sign(other_axis) / line_spacing).y+1.)/2.;
		lines_2 = (lines_2+1.)/2.;
		lines_2 = step(1.-line_width, lines_2); 
		lines = max(lines, lines_2);
	    COLOR = (1.-lines) * sky_color + lines * line_color;
	}
}
  1. I don’t see an advantage of this when you can have the advantage of the models actually in the scene like I suggested above. But I am not sure what kind of game you are working on.
  2. Your shader seems to be extremely costly as you are doing the very expensive calculations outside of the cubemap passes contrary to what the documentation suggests. I would recommend reading this through first. Also, “simple” if’s are very costly in shaders if the input value changes during execution. Furthermore length() functions are also extremely expensive in shaders so I am not surprised that your shader is not that performant.
  1. I don’t need the models to really be in the scene. It’s just to give the impression there is a landscape. I’ll try to experiment and i’ll reply here if i manage to achieve this without multiple cameras. In any case, if anyone wants to achieve it, there is an example implementation with multiple camera/viewports here.

  2. I think the documentation does advise to do cheap computation in the cubemap (only used for reflections and stuff), and expensive computation outside (used to display the actual sky background when rendering).
    But my bad, I hadn’t tested it again on igpu after reducing the radiance size to 32, and it really helps a lot.
    I also replaced any branching/ifs with mix/step, but i think it’s basically nothing compared to the cost induced by a larger radiance size.

shader_type sky;
render_mode use_half_res_pass, use_quarter_res_pass;

uniform vec3 sky_color : source_color = vec3( 0.05, 0.05, 0.05 );
uniform vec3 line_color : source_color = vec3( 0.08, 0.2, 0.1 );
uniform vec3 cube_size = vec3(1.,1.,1.);
uniform vec3 cube_offset = vec3(1.,1.,1.);
uniform float line_spacing = 0.01;
uniform float time_scale = 0.5;
uniform float line_width = 0.01;

float lengthSquared(vec2 v)
{
	return dot(v,v);
}

void sky() {
	if (AT_CUBEMAP_PASS || AT_HALF_RES_PASS || AT_QUARTER_RES_PASS) {
        COLOR = (1.-line_width) * sky_color + line_width*line_color;
    }
	else
	{
		vec2 sky_uv_x = EYEDIR.yz / EYEDIR.x * cube_size.x;
	   	vec2 sky_uv_y = EYEDIR.xz / EYEDIR.y * cube_size.y;
		vec2 sky_uv_z = EYEDIR.xy / EYEDIR.z * cube_size.z;
		
		float sky_uv_x_len = lengthSquared(sky_uv_x);
		float sky_uv_y_len = lengthSquared(sky_uv_y);
		float sky_uv_z_len = lengthSquared(sky_uv_z);
		
		vec2 selected_uv = sky_uv_x;
		float other_axis = EYEDIR.x;
		float selected_len = sky_uv_x_len;
		
		float choose_y = step(sky_uv_y_len, selected_len);
		selected_len = mix(selected_len, sky_uv_y_len, choose_y);
		selected_uv = mix(selected_uv, sky_uv_y, choose_y);
		other_axis = mix(other_axis, EYEDIR.y, choose_y);
		
		float choose_z = step(sky_uv_z_len, selected_len);
		selected_len = mix(selected_len, sky_uv_z_len, choose_z);
		selected_uv = mix(selected_uv, sky_uv_z, choose_z);
		other_axis = mix(other_axis, EYEDIR.z, choose_z);
		
		float lines = (sin(TIME * time_scale + selected_uv * sign(other_axis) / line_spacing).x+1.)/2.;
		lines = (lines+1.)/2.;
		lines = step(1.-line_width, lines);
		float lines_2 = (sin(TIME * time_scale + selected_uv  * sign(other_axis) / line_spacing).y+1.)/2.;
		lines_2 = (lines_2+1.)/2.;
		lines_2 = step(1.-line_width, lines_2); 
		lines = max(lines, lines_2);
	    COLOR = (1.-lines) * sky_color + lines * line_color;
	}
}

STILL I would be interested if anyone finds a way to render a godot scene to a panorama texture/360° screenshot.

we used such techniques a long time ago. The method was simple. a different scene than the environment was rendered to the viewport
the first pass rendered the scene first and then rendered the level. and it was visually the same as a cubemap. It was faster, of course.

no shader was needed, not to mention that it worked on old Intel Pentiums when there weren’t even 3D cards yet, maybe voodoo appeared then :slight_smile:

1 Like

dunno if you were able to do what you wanted, but i kinda do the same stuff, so with shaders we can make a panorama of the scene, with help of 6 cameras, then we put that thing inside of TextureRect for example, then, because we use shader, we cant just get texture, get image or whatever, we need to use stupid viewport, but somebody had mercy on us, and gave us subviewport, and with that, we can make texture, which we can store, and apply into skybox

I think using a viewport is the easiest and the best way. Maybe you could try and compare how many fps it can be on your pc running your own game with multiple viewports and running half life 2.
Or you could just include the skybox model in each map. just scale the skybox model to a proper size that looks fine.

The problem with viewport is that in Godot 4.4 dev (and earlier 4.x) TAA breaks transparent viewports so you can’t render one viewport on top of another. And TAA is a pretty basic feature, so giving it up for your game to have a 3D skybox is not a tradeoff I want to make.
The viewport approach also does nothing for sky reflections. Normally sky provides a cubemap that is used when shading reflective surfaces. With a viewport we don’t have that.
I tried using a scaled down scene with a special shader that make it render underneath, other geometry. The skybox also replicates player movement in 3D space at scale to make it seem like the skybox objects are really “there”.

I also have a reflection probe that tries to replace the sky reflection, but Godot so far doesn’t properly handle small refprobes inside large refprobes, so having a large all-encompassing reflection probe that is supposed to act as sky breaks all interior reflection probes.

This is my best prototype so far:

(use godot 4.4 dev4 or later):

1 Like

hmm, transparent viewport? you mean one on top of each other? i personally see the 2d scene as one big viewport of some kind? you can “infinitely” add these viewport next to each other, like stacking monitors to you pc, you dont put second one on top of the first one, you put it to the side, on top, maybe bottom, you can even add additional viewport outside of 3d scene viewport? dunno if it helps but thats my first thought after hearing transparent viewport whatever that additional viewport would render, you probably can also add camera2d, and from that feed, somehow get an texture, and queue free them 2d nodes, if not needed right now, to keep them trees clean from unused apples