A working hiresolution renderer

Godot Version

3.6, 4.3

Question

a working hiresolution screenshot renderer

working on 3d setup root Spatial, Camera, Mesh Instance with shaders, a node with the following script attached. well camera settings not working so well per tile calc, but all debug steps are coded in, should be easy to follow along, any leads for frustum calcs of tile sections from current viewport, lets make a working resolution screenshot renderer for godot programs. this comes from a handy modified Processing TileSaber we use all the time to render hires images of final viewport outputs.

would be nice to extend to 2d as well.

could you help upgrading this script?

extends Spatial

export var final_width = 3840  # Final image width
export var final_height = 2160  # Final image height
export var viewport_width = 1920  # Viewport width
export var viewport_height = 1080  # Viewport height
export var tile_num = 2  # Number of tiles in each dimension

var viewport: Viewport
var camera: Camera
var result_image: Image
var viewport_display: TextureRect

func _ready():
	setup_viewport()
	setup_viewport_display()
	call_deferred("capture_high_res_image")

func setup_viewport():
	viewport = Viewport.new()
	viewport.size = Vector2(viewport_width, viewport_height)
	viewport.usage = Viewport.USAGE_3D
	viewport.render_target_update_mode = Viewport.UPDATE_ALWAYS
	viewport.transparent_bg = false

	camera = Camera.new()
	camera.current = true
	viewport.add_child(camera)

	# Copy the main camera's transform to our new camera
	var main_camera = get_viewport().get_camera()
	if main_camera:
		camera.global_transform = main_camera.global_transform
		camera.fov = main_camera.fov
		camera.near = main_camera.near
		camera.far = main_camera.far
		print("copied the main camera's transform to our new camera")

	# Add your MeshInstance to the viewport
	var mesh_instance = get_node("../MeshInstance")
	if mesh_instance:
		var mesh_copy = mesh_instance.duplicate()
		viewport.add_child(mesh_copy)
		print("copied the main camera's transform to our new camera")
		

	add_child(viewport)

func setup_viewport_display():
	viewport_display = TextureRect.new()
	viewport_display.rect_size = Vector2(320, 180)
	viewport_display.expand = true
	viewport_display.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED

	var canvas_layer = CanvasLayer.new()
	canvas_layer.add_child(viewport_display)
	add_child(canvas_layer)

func capture_high_res_image():
	result_image = Image.new()
	result_image.create(final_width, final_height, false, Image.FORMAT_RGB8)

	var progress_label = Label.new()
	progress_label.rect_position = Vector2(10, 200)
	add_child(progress_label)

	var aspect = float(viewport_width) / viewport_height
	var fov_y = deg2rad(camera.fov)
	var fov_x = 2 * atan(tan(fov_y / 2) * aspect)

	for tile_y in range(tile_num):
		for tile_x in range(tile_num):
			progress_label.text = "Capturing tile %d,%d of %d,%d" % [tile_x+1, tile_y+1, tile_num, tile_num]
			
			for ynum in range(10):
				yield(get_tree(), "idle_frame")

			setup_camera_for_tile(tile_x, tile_y, fov_x, fov_y)

			viewport.render_target_update_mode = Viewport.UPDATE_ONCE
			yield(get_tree(), "idle_frame")
			yield(get_tree(), "idle_frame")

			var viewport_texture = viewport.get_texture()
			var viewport_image = viewport_texture.get_data()
			viewport_image.convert(Image.FORMAT_RGB8)
			viewport_image.flip_y()

			viewport_image.save_png("res://debug_tile_%d_%d.png" % [tile_x, tile_y])

			var target_x = tile_x * viewport_width
			var target_y = tile_y * viewport_height
			result_image.blit_rect(viewport_image, 
								   Rect2(Vector2.ZERO, viewport.size),
								   Vector2(target_x, target_y))

	restore_camera()
	result_image.save_png("res://high_res_image.png")
	print("High-resolution image saved!")

	remove_child(progress_label)
	progress_label.queue_free()

func setup_camera_for_tile(tile_x, tile_y, fov_x, fov_y):
	var near = camera.near
	var far = camera.far
	var tile_width = tan(fov_x / 2) * 2 * near
	var tile_height = tan(fov_y / 2) * 2 * near

	var left = -tile_width / 2 + tile_width * (float(tile_x) / tile_num)
	var right = -tile_width / 2 + tile_width * ((tile_x + 1.0) / tile_num)
	var bottom = tile_height / 2 - tile_height * ((tile_y + 1.0) / tile_num)
	var top = tile_height / 2 - tile_height * (float(tile_y) / tile_num)

	camera.set_frustum(1, Vector2(left, top), near, far)
	camera.set_frustum(0.01, Vector2(left, top), near, far)
	camera.set_frustum(0.055, Vector2(left, top), near, far)
	
#	void set_frustum(size: float, offset: Vector2, z_near: float, z_far: float)
#
#Sets the camera projection to frustum mode (see PROJECTION_FRUSTUM), by specifying a size, an offset, and the z_near and z_far clip planes in world space units. See also frustum_offset.


func restore_camera():
	var main_camera = get_viewport().get_camera()
	if main_camera:
		camera.fov = main_camera.fov
		camera.near = main_camera.near
		camera.far = main_camera.far

func _process(delta):
	viewport.render_target_update_mode = Viewport.UPDATE_ONCE
	viewport_display.texture = viewport.get_texture()
	
	if(Input.is_key_pressed(KEY_0)):
		capture_high_res_image() # esta a criar varios objectos






If the resolution is lower than or equal to 16384 pixels on either axis, you can render it in a single take. No need for a tiling approach, especially on today’s PCs.

You can also set get_viewport().scaling_3d_scale = 2.0 before taking the screenshot for 4× SSAA, which will greatly reduce aliasing. This can be combined with 3D MSAA or TAA for even greater quality (although if you use TAA, remember to wait at least 8 frames after changing scaling_3d_scale for it to converge).

@Calinou Thanks, that’s a nice feature to know about, and way simpler than my approach, should work for most cases. Will check it out, however we like the manual control and no antialiasing features to keep shiny interpolation artifacts, or whatever current settings we have in the renderer (see other paragraph). Plus, all our calcs & libs are already here, so, just a matter of pluggin them into Godot funcs and params. In setup_camera_for_tile func, left, right, top and bottom should be accurate, just no easy way to plug it into Godot’s camera frustum. And as you can tell from glancing the code, this can make A1, A0 or larger 300dpi poster pictures we plan to ship with special game editions, for people to frame them in their offices and walls, 16k resolution might fall a bit short. Even if the graphics card will fail surely for larger sizes, there are always debug tiles to manually sew into final images.

BTW, very nice sampling section on Godot 4 StandardMaterial3D, nearest interpolation is there, do you think it’s an easy backport to 3.6? I can’t seem to find it there (SpatialMaterial)… Very nice GradientTexture gui making tools, any chance i can fullscreen the inspector, or the gradient texture section in order to interact with the patterns ?(have already shifted inspector section to max width, still not enough micro resolution for some patterns, we just keep using GIMP for texture making if needed and not already present in the default coding patterns pool we like to boot up and swim within.)

Keep in mind that tiling approach can be marginally faster if you can get tiles to render in parallel.

@darkhog Thanks, your parallel suggestion can come in handy for videowalls or videomapping setups, but for now is simple offline printing hires graphics on demand.

In 3.x, the Filter property is defined in the Import dock on each texture.

In 4.x, you can turn any dock into a floating window which can then be maximized. This can’t be done in 3.x though (you can only resize the dock’s width to the maximum allowed value).