the thing is, i’m going to have multiple color palettes for different environments, so i need a way to be able to set this color at runtime, with a script
okay this is approaching the solution but something is missing. i’m setting the clear color to magenta here on the camera’s process and what happens is the background becomes magenta for the very first frame that a clear pixel is drawn, and then it immediately becomes black again. so something else is turning this region black retroactively…
Huh, that’s strange. I guess the black pixels aren’t actually the clear color, then. I can take another look at it but this might require someone who knows more about the internals of Godot to figure out.
For people trying to do this in 3.x you can use VisualServer.black_bars_set_images. This does not seem to have an equivalent in 4.x though, probably because the rewrite of the whole rendering system.
You can however create this yourself using a main ui scene which embeds your game using a subviewport container. This would be you best bet for 4.x I’d say. It also has the advantage that you can use the node system to style the bars which allows for fancy transitions were the bars overlap the viewport and such.
Found my own implementation for colored aspect bars:
Scene structure:
The script (it replicates canvas item resizing with aspect keep. But adapting it for pixel games should be possible):
class_name BlackBars
extends MarginContainer
@export var color := Color.BLACK
@onready var __base_size := Vector2(
ProjectSettings.get_setting("display/window/size/viewport_width"),
ProjectSettings.get_setting("display/window/size/viewport_height"),
)
@onready var __oversampling: bool = ProjectSettings.get_setting("gui/fonts/dynamic_fonts/use_oversampling", true)
# Layer used for custom drawing the bars. You could also just change the bg color. This script originated from a version without subviewport. Were drawing above the scene was important.
@onready var __layer := CanvasLayer.new()
@onready var __base := Control.new()
func _ready() -> void:
%SubViewport.size_2d_override = __base_size
if __oversampling:
# Handled by this script
ProjectSettings.set_setting("gui/fonts/dynamic_fonts/use_oversampling", false)
add_child(__layer, false, Node.INTERNAL_MODE_BACK)
__layer.layer = 120
__layer.add_child(__base)
__base.set_anchors_preset(Control.PRESET_FULL_RECT, true)
__base.mouse_filter = Control.MOUSE_FILTER_IGNORE
__base.draw.connect(__draw_black_bars)
func __draw_black_bars() -> void:
if (__oversampling):
var factor = %SubViewport.size.x / __base_size.x
TextServerManager.get_primary_interface().font_set_global_oversampling(factor)
propagate_call(&"queue_redraw")
var aspect: float = __base_size.x / __base_size.y
if size.x / size.y > aspect:
var bar_width: float = (size.x - aspect * size.y) / 2.0
add_theme_constant_override(&"margin_left", bar_width)
add_theme_constant_override(&"margin_right", bar_width)
add_theme_constant_override(&"margin_top", 0)
add_theme_constant_override(&"margin_bottom", 0)
# Scaling of the viewport is handled by the margins. You could use anything to draw the bars. (e.g. textures or custom scenes)
__base.draw_rect(Rect2(0, 0, bar_width, size.y), color)
__base.draw_rect(Rect2(size.x - bar_width, 0, bar_width, size.y), color)
elif size.x / size.y < aspect:
var bar_height: float = (size.y - size.x / aspect) / 2.0
add_theme_constant_override(&"margin_top", bar_height)
add_theme_constant_override(&"margin_bottom", bar_height)
add_theme_constant_override(&"margin_left", 0)
add_theme_constant_override(&"margin_right", 0)
__base.draw_rect(Rect2(0, 0, size.x, bar_height), color)
__base.draw_rect(Rect2(0, size.y - bar_height, size.x, bar_height), color)
i was really trying to avoid just drawing black bars onto the screen because i don’t want the camera to have to render stuff that’s out of frame anyway and then draw over it. it seems like this implementation creates some kind of sub-canvas though? is the camera still rendering the same amount of space here, or is it doing all the extra work on the sides and then clearing it?
what is this script doing exactly, it seems to just lock the window at a minimum size? when stretch mode is disabled i don’t get those black bars at all, it just expands the camera range. my question is how to keep those black bars but change their color
okay follow-up question, where does this actually go in the scene structure? like where is that game that it is rendering in your example, is it in ‘holder2D’? i have a Node3D as the root of my game’s scene, can I just put that into the subviewport?
2D Cameras are really just a wrapper around canvas transforms. Not sure if and what clipping godot uses in 2d, but if it uses some clipping method it should apply them in the same way in the subviewport. (Same for 3d but there I know that godot uses clipping).
You can ignore the Holder2D and HolderUI thats just how I structure my games. The example is very integrated with my own main scene architecture, that’s why I don’t share the project.
Caveats to note: you will have to use a custom solution for scene changing. get_tree().change_scene won’t do, since you want to insert the new scene into the subviewport, not replace the subviewport structure. Should be easy enough to have an autoload which provides the same functions though.
Yeah, it sets the minimum size so full game remains visible and after window is resized it changes nodes position to center. And You can set clear color to match the theme of your game.
I don’t think there is really any way to change color of those black bars.
Once I have also tried to fix that but ended up creating this workaround. Please let me know if you find a way to change its color
what are the settings you are using for this, do you have window stretching disabled? also, i’m not sure what those percentage signs in the inspector mean but i suspect they are important. i’ve changed all the references to %Subviewport to $SupviewportContainer/Subviewport because i was getting nullptr errors, maybe i put the script on the wrong node?