Monday, April 27, 2020

Infinitely scrolling starfields in Godot Engine 3.2.1

In my previous post, I mentioned that I wanted to create infinitely-scrolling starfields in Godot Engine, but I could not get it working within the time limits of the jam. Yesterday, I spent some time investigating different ways to accomplish this, and I was able to come up with a workable solution. This morning, I started recording a video tutorial about it, but I found certain steps hard to explain, and so I stepped back and looked at alternatives. Turns out, there was a much easier way to do it than I was going to record. Here, I want to share both approaches.

First Attempt: Using Shaders

It seemed to me that a solution should lie in Godot's Parallax Background feature, which I had never used before. As I perused tutorials during the jam, it looked to me like the background could only be used over a fixed area—not an infinite plane. This got me thinking about the classic, old-school game programming problem: should I move the player or should I move the world?

The structure of my solution, then, was this:
  • Keep a single copy of the background image under the camera
  • As the player moves, move the the world in the opposite direction, thereby keeping equilibrium.
  • Pan the background using a custom shader to give the illusion of motion
This approach works. Here's a screenshot demonstrating the scene layout:
The Background is a TextureRect showing the starfield, and under it in the tree is a Node2D called World. Actors can move within this world, but it won't affect the background.

The script that brings these together is this:

 extends Node2D  

 export var speed : float = 200  

 func _process(delta:float):  
      # Calculate movement direction  
      var direction = Vector2()  
      if Input.is_action_pressed("ui_up"):  
           direction += Vector2(0,-1)  
      if Input.is_action_pressed("ui_down"):  
           direction += Vector2(0,1)  
      if Input.is_action_pressed("ui_right"):  
           direction += Vector2(1,0)  
      if Input.is_action_pressed("ui_left"):  
           direction += Vector2(-1,0)  

      # Determine velocity from the direction, speed, and elapsed time  
      var velocity = direction.normalized() * speed * delta  

      # Update player's position based on its speed  
      $World/Player.position += velocity  

      # Update the world's offset based on player's speed  
      $World.position -= velocity;  

      # Track the background shader with the world's offset  
      $Background.material.set_shader_param("offset", -$World.position / $Background.texture.get_width())  

The background has a custom shader that I lightly modified from a common approach I found online. Incidentally, it is the first shader I have ever written.
 shader_type canvas_item;  
 uniform vec2 offset;  
 void fragment() {  
      vec2 shifted_uv = UV;  
      shifted_uv += offset;  
      vec4 color = texture(TEXTURE, shifted_uv);  
      COLOR = color;  
 }  

This is simply taking in the offset and shifting the UVs by the appropriate amount. Note that the TextureRect has to be set with a Stretch Mode of Tile for this to work.

An advantage of this approach is that all the in-game actors are still expressed in game coordinates. The obvious disadvantage of this approach is the need to have a separate World node—an organizational layer separate from the background. This complicates the process of adding nodes, since you cannot just add them to the root as one might expect, but rather to the World branch of the root.

Using Parallax Background and Layers

As I was tinkering with ParallaxLayers, I found this:


"Useful for creating an infinite scrolling background" you say? 

I had seen this configuration option earlier but had dismissed it. I did not want to mirror anything, so that couldn't possibly be what I want. Turns out, it is exactly what I want. Simply adding a mirroring value equal to the size of the texture created an infinitely-scrollable view of the starfield.

This led to my next question: Why is it called "mirroring"? In computer graphics, "mirroring" is a wrapping setting that can be given to a texture to determine how it is rendered when the UV coordinates exceed the normal [0,1] range. This is clearly explained in the official OpenGL documentation. This figure, taken from the official documentation, demonstrates the four wrapping options:

As you can see, mirroring ... well, it mirrors, to no big surprise. When I look at what the Motion Mirroring does in Godot Engine's ParallaxLayer, I don't see any mirroring at all—neither in the OpenGL texture wrapping sense nor in the informal, colloquial sense. I even turned to the source code of ParallaxLayer for a hint. That just took me to the source for rendering_service_canvas, which justifies its need for mirroring by cyclically referring back to the ParallaxLayer's need for mirroring. Nowhere did I see any code that actually looked like it mirrored anything. It looks to me like it's just the word the developers used to describe this feature, but I suggest that this is simply the wrong word.

If "mirroring" is the wrong word, what is the right word? It's probably something more like "wrapping', which is the one that OpenGL uses, or "repeating", which is what it looks like it's doing when it's working right. 

I was glad to find this feature, and hopefully by using it and writing this post, I will remember it next time I need it. At the very least, perhaps Google will bring me back to this post when I search for help. For a while there, it looked like I was going to make my first Godot Engine video tutorial for my series. I'm glad I found the easy answer before posting a video about my convoluted alternative, although I still wish the feature had a better name.

No comments:

Post a Comment