What's Godot's equivalent to a StructuredBuffer for shaders?

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By 19PHOBOSS98

Hello, does anyone know an equivalent to “StructuredBuffers” that I can use to pass an array of vec3s and floats to a spatial shader? Imma use it to pass object properties (sphere radius, position, etc). I can’t just dedicate uniforms for each object property cause I’ll be using a LOT of objects.

:bust_in_silhouette: Reply From: Zylann

AFAIK this doesn’t exist yet in the Godot 3 API. Communicating with shaders is done only with uniform parameters, and arrays are not supported. The closest you can get is to provide a texture and sample its raw data without interpolation.
There has been requests to add support for array parameters: Support uniform arrays in the shader language · Issue #931 · godotengine/godot-proposals · GitHub

Thanks bro. But I think I figured it out. A guy at the discord channel named MathiasDrgon helped me understand how to pass an array of floats to a shader thru textures. Imma post the rest of it as an answer real quick.

19PHOBOSS98 | 2021-08-23 22:51

:bust_in_silhouette: Reply From: 19PHOBOSS98

That was a hot minute :slight_smile:
Big thanks to MathiasDrgon and MobiusTrap (2021) from Discord for breaking it down for me. I wrote my data in a texture and read it’s rgba channels in the shader.
Here’s the code I used:

ROOT NODE SCRIPT:

    func update_RTX_sphere_pos_r():
    	var sphere_list_length = rb_spheres.size()

    """
         Each point on an image has RGBA channels. This is where I store and read my data as floats. Since an image is created with 4 channels, might as well use the alpha channel (rgbA) and throw in my sphere's radius in there. First, I saved all of my floats to an array in-order. This makes it easier to iterate thru later in the shader.
    """
    	var sphere_data = []
    	for r in rb_spheres:
    		var sphere_pos = r.global_transform.origin
    		var sphere_radius = r.radius
    	        sphere_data.append_array([sphere_pos.x,sphere_pos.y
,sphere_pos.z,sphere_radius])
    	#print("sphere_data: ",sphere_data)
    """
         To turn our saved data into an image we need to first convert 'sphere_data' into a PoolByteArray. I used StreamPeerBuffer to stream in my data as bytes.
    """
    	var sphere_data_bytes = PoolByteArray([])
        #var sphere_data_bytes = PoolByteArray(sphere_data)#idk, I tried :)
    	var stream = StreamPeerBuffer.new()

    	for i in sphere_data:
    		stream.clear()
    		stream.put_float(i)
    		stream.seek(0)
    		for j in range(4):
    			sphere_data_bytes.append(stream.get_8())
     """
         Here, I "create an image from data" saving 4 elements of "sphere_data" into the RGBA channels respectively (technically the sphere_data_byte is getting saved but  you know what I mean :) ). The first 2 parameters in "create_from_data" dictates the size of the image. That's how many cells (each with RGBA channels) we need to store our data.
  
         Say, I have 3 spheres that I want to mess with in my shader. Each with it's own position (vec3(floats)) value and radius (float). Lucky me, that perfectly adds up to 4 components (floats) that I can easily store in one cell (RGBA). I need 3 cells to store the data from 3 of my spheres. If I want to add more spheres I just need more cells in the image.
    """
    	var sphere_img1 = Image.new()
    	var sphere_img1Tx = ImageTexture.new()

    	#sphere_img1.create_from_data(3,1,false, Image.FORMAT_RGBF, data_in_bytes)
#saves 3 components as rgba into one texel but the image would still use the alpha channel regardless so might as well use it.

    	sphere_img1.create_from_data(sphere_list_length,1,false, Image.FORMAT_RGBAF, sphere_data_bytes) 
#saves 4 components as rgba into one texel 

sphere_img1Tx.create_from_image(sphere_img1) #as I learned shaders can only take textures in a uniform so I turned the image into an ImageTexture
    	
    	get_tree().call_group("SCREENS","update_sphere_buffer",sphere_img1Tx)

SCREENS GDSCRIPT:

...
func update_sphere_buffer(sb):
    self.get_active_material(0).set_shader_param("sphere_buffer", sb)
...

SCREEN SHADER CODE:

 ...
 /*
inside the shader, there's a function called "texelFetch" that we can use to fetch a "texel"(texture pixel)(our cells). It returns a vec4 for each cell we point at (ivec2(x,y)).
Since I saved my data in a 3X1 image, I iterate through my texels with ivec2(s,0)
*/
    uniform sampler2D sphere_buffer;
    ...
    //Instead of texture() I used texelFetch(), sparing me some math
    for (int s = 0; s < textureSize(sphere_buffer,0).x; s++){
        IntersectSphere(ray, bestHit, texelFetch(sphere_buffer,ivec2(s,0),0),vec3(0.20, 0.0, 0.80),vec3(0.0),0f);
    }
...

EDIT:
Ok… for anyone interested, if you want speed and performance, fetching a texture in a loop each frame isn’t the best way to go. I found this. I learned that texture fetching is expensive and uniform arrays are faster. So, I guess I still need to wait for Godot.