Skip to content

Instantly share code, notes, and snippets.

@partybusiness
Last active October 6, 2024 19:21
Show Gist options
  • Save partybusiness/d60f32317ed0a0548021dccbe304a9e8 to your computer and use it in GitHub Desktop.
Save partybusiness/d60f32317ed0a0548021dccbe304a9e8 to your computer and use it in GitHub Desktop.
Godot shader raycast FPS
#[compute]
#version 450
// Invocations in the (x, y, z) dimension
layout(local_size_x = 8, local_size_y = 1, local_size_z = 1) in;
layout(rgba16f, set = 0, binding = 0) uniform restrict image2D distances; //distances to walls along each raycast, as well as data for wall texture
layout(rgba16f, set = 0, binding = 1) uniform restrict image2D map; //map to raycast on
layout(push_constant, std430) uniform Params {
float x;
float y;
float angle;
} view_pos;
//based on this: https://theshoemaker.de/2016/02/ray-casting-in-2d-grids/
vec4 Intersect(vec2 startPos, vec2 forw) {
vec2 currentPos = startPos;
vec2 tile = floor(startPos)+1.0;
vec2 dTile = sign(forw);
vec2 dt = (tile + vec2(forw.x>0.0?0.0:-1.0, forw.y>0.0?0.0:-1.0) - startPos) / forw;
vec2 ddt = dTile / forw;
float t = 0;
if (length(forw)>0.0) {
while (t<64.0) {
if (dt.x < dt.y) {
tile.x = tile.x + dTile.x;
float sdt = dt.x;
t = t + sdt;
dt.x = dt.x + ddt.x - dt.x;
dt.y = dt.y - sdt;
}
else {
tile.y = tile.y + dTile.y;
float sdt = dt.y;
t = t + sdt;
dt.x = dt.x - sdt;
dt.y = dt.y + ddt.y - sdt;
}
vec4 col = imageLoad(map, ivec2(tile));// map.SampleLevel(samplermap, Pos2UV(tile), 0);
if (col.r > 0.1) {
vec2 pos = startPos + forw * t;
float tileU = (pos.x + pos.y) - floor(pos.x + pos.y);
return vec4(t, col.gb, tileU); //distance, colour data, and v for uv
} //else should we store empty tiles somewhere?
}
}
return vec4(512, 0,0,0);
}
void main() {
float fov = 0.5; //half of horizontal fov in radians
float numstrips = 512.0; //number of strips tested along the horizontal
float halfstrips = numstrips / 2.0;
float stripangle = gl_GlobalInvocationID.x / halfstrips - 1.0; //angle offset of this strip ranging from -1.0 to 1.0
float angle = view_pos.angle + stripangle * fov; //combine viewing angle with angle of strip
vec2 forward = vec2(cos(angle), sin(angle));
vec2 position = vec2(view_pos.x, view_pos.y);
imageStore(distances, ivec2(gl_GlobalInvocationID.x, 0), Intersect(position, forward));
}
shader_type canvas_item;
render_mode unshaded;
// displays a first-person view based on distances calculated in find_distances.glsl
// texture of map, used for floor colour
uniform sampler2D map:filter_nearest, source_color;
// vertical strips calculated by the raycast compute shader
uniform sampler2D strips:filter_nearest;
// tiles displayed on any visible walls
uniform sampler2D wall_texture:source_color, filter_nearest;
// number of tiles across in wall_texture
// with value of 8, it's an 8x8 tile map
uniform int tile_count = 8;
// multiplier on distance when fading wall to black
uniform float fade_strength = 0.3;
// colour of ceiling
// this is just a single colour rather than sampling a texture like the floor does
uniform vec3 ceiling_colour:source_color;
// used to pass current camera position and direction to the display shader
uniform vec2 forward;
uniform vec2 position;
void vertex() {
// Called for every vertex the material is visible on.
}
float inverse_lerp(float a, float b, float c) {
return (a-c) / (a-b);
}
void fragment() {
// get info about current vertical strip
vec4 sampled = texture(strips,UV);
float dist = sampled.r;
float ypos = abs(UV.y - 0.5);
float wallHeight = (0.5 / dist);
float isWall = (ypos < wallHeight) ? 1.0: 0.0;
float B = (UV.x - 0.5); //horizontal fov in radians = 1.0, should match compute shader
// forward of
vec2 newForward = vec2(forward.x * cos(B) - forward.y * sin(B), forward.x * sin(B) + forward.y * cos(B)); //rotate vector by B
float yt = 1.0/(abs(ypos)*2.0);//how to project y angle ypos to distance to floor
vec2 floorCoord = position + newForward * yt;
float checkVal = float(int(round(floor(floorCoord.x) + floor(floorCoord.y))) % 2);
vec2 floorUV = (floorCoord + vec2(1.0)) / 512.0; // match size of map
vec4 floorColour = texture(map, floorUV);
vec3 noWallColour = mix(floorColour.rgb, ceiling_colour, (UV.y < 0.5)?1.0:0.0 );
vec2 singleWallUv = vec2(sampled.a, inverse_lerp(0.5 - wallHeight, 0.5 + wallHeight, UV.y));
vec2 wallUv = (floor(sampled.gb*float(tile_count)) + singleWallUv) / float(tile_count); //invlerp (0.5-wallHeight,0.5+wallHeight, yt); ??
vec4 wallColour = texture(wall_texture, wallUv) / clamp((dist)*fade_strength,1,512);
vec4 result = mix(vec4(noWallColour,1.0), wallColour, isWall);
COLOR.rgb = result.rgb;
}
extends ColorRect
class_name Renderer
var display_material:ShaderMaterial
var rd:RenderingDevice # = RenderingServer.create_local_rendering_device()
var buffer:RID
var ray_pipeline:RID
var ray_uniform_set:RID
var ray_calc_shader:RID
@export var pos:Vector2
@export var angle:float
@export var rotation_speed:float = 1.5
@export var walking_speed:float = 5.0
var distances:Texture2DRD
var map:Texture2DRD
var block_map:Image
@export var map_source:Texture2D
var position_bytes:PackedByteArray
func _ready():
rd = RenderingServer.get_rendering_device()
#set up position data
position_bytes.resize(16)
#set up map
#create texture
var map_texture : RDTextureFormat = RDTextureFormat.new()
#FORMAT_RGB8
#DATA_FORMAT_R8G8B8_SINT
map_texture.format = RenderingDevice.DATA_FORMAT_R32G32B32A32_SFLOAT
map_texture.texture_type = RenderingDevice.TEXTURE_TYPE_2D
map_texture.width = map_source.get_width()
map_texture.height = map_source.get_height()
map_texture.depth = 1
map_texture.array_layers = 1
map_texture.mipmaps = 1
map_texture.usage_bits = RenderingDevice.TEXTURE_USAGE_SAMPLING_BIT + RenderingDevice.TEXTURE_USAGE_COLOR_ATTACHMENT_BIT + RenderingDevice.TEXTURE_USAGE_STORAGE_BIT + RenderingDevice.TEXTURE_USAGE_CAN_UPDATE_BIT
#var source_rid = rd.texture_2d_create(map_source.get_data())
map = Texture2DRD.new()
var source_map_image:Image = map_source.get_image()
source_map_image.convert(Image.FORMAT_RGBAF)
print(source_map_image.get_format()," = ",map_texture.format)
#map_texture.format = source_map_image.get_format()
var source_map_data:PackedByteArray = source_map_image.get_data()
print("blah",source_map_data.size())
map.texture_rd_rid = rd.texture_create(map_texture, RDTextureView.new(), [source_map_data])
var map_uniform = RDUniform.new()
map_uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_IMAGE
map_uniform.binding = 1
map_uniform.add_id(map.texture_rd_rid)
#set up distances
var dis_texture : RDTextureFormat = RDTextureFormat.new()
dis_texture.format = RenderingDevice.DATA_FORMAT_R16G16B16A16_SFLOAT
# RenderingDevice.DATA_FORMAT_R16G16_SFLOAT
dis_texture.texture_type = RenderingDevice.TEXTURE_TYPE_2D
dis_texture.width = 512
dis_texture.height = 1
dis_texture.depth = 1
dis_texture.array_layers = 1
dis_texture.mipmaps = 1
dis_texture.usage_bits = RenderingDevice.TEXTURE_USAGE_SAMPLING_BIT + RenderingDevice.TEXTURE_USAGE_COLOR_ATTACHMENT_BIT + RenderingDevice.TEXTURE_USAGE_STORAGE_BIT + RenderingDevice.TEXTURE_USAGE_CAN_UPDATE_BIT
distances = Texture2DRD.new()
distances.texture_rd_rid = rd.texture_create(dis_texture, RDTextureView.new(), [])
var distances_uniform = RDUniform.new()
distances_uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_IMAGE
distances_uniform.binding = 0
distances_uniform.add_id(distances.texture_rd_rid)
#set up distance calculator
ray_calc_shader = rd.shader_create_from_spirv(load("res://raycastFPS/find_distances.glsl").get_spirv())
ray_uniform_set = rd.uniform_set_create([map_uniform,distances_uniform],ray_calc_shader,0)
ray_pipeline = rd.compute_pipeline_create(ray_calc_shader)
#set up display material
display_material = material
display_material.set_shader_parameter("map", map)
display_material.set_shader_parameter("strips", distances)
#await get_tree().process_frame
block_map = map_source.get_image()
func _process(delta):
#handle input to move
angle += Input.get_axis("Rotate_Left", "Rotate_Right") * delta * rotation_speed
var forward = Input.get_axis("Backward", "Forward")
var move_vector:Vector2 = Vector2(cos(angle), sin(angle)) * delta * forward * walking_speed
# do pixel test of map to see if there's a wall in the way
if block_map!=null:
var test_pos:Vector2i = Vector2i(ceil(pos.x + move_vector.x), ceil(pos.y + move_vector.y))
var curr_pos:Vector2i = Vector2i(ceil(pos.x), ceil(pos.y))
# if we're moving to a new pixel, check map for walls
if curr_pos.x != test_pos.x || curr_pos.y != test_pos.y:
var wall_check = block_map.get_pixel(test_pos.x,test_pos.y)
if wall_check.r > 0.1:
# there is a wall, now to decide whether to block along x or y axis
if curr_pos.x != test_pos.x && curr_pos.y != test_pos.y:
# corner case, test neighbours to pick direction
var wall_check1 = block_map.get_pixel(test_pos.x,curr_pos.y)
var wall_check2 = block_map.get_pixel(curr_pos.x,test_pos.y)
if wall_check1.r > 0.1:
move_vector.x = 0.0
if wall_check2.r > 0.1:
move_vector.y = 0.0
else: #simple cases
if curr_pos.x != test_pos.x:
move_vector.x = 0.0
else:
move_vector.y = 0.0
pos += move_vector
run_tick()
func run_tick():
# assign position and forward to display shader
display_material.set_shader_parameter("forward", Vector2(cos(angle), sin(angle)))
display_material.set_shader_parameter("position", pos)
# set up compute shader
var view_pos:PackedFloat32Array
view_pos.resize(4) # needs to be multiple of 4
view_pos[0] = pos.x
view_pos[1] = pos.y
view_pos[2] = angle
position_bytes = view_pos.to_byte_array()
var compute_list := rd.compute_list_begin()
rd.compute_list_bind_compute_pipeline(compute_list, ray_pipeline)
rd.compute_list_bind_uniform_set(compute_list, ray_uniform_set, 0)
rd.compute_list_set_push_constant(compute_list, position_bytes, 16)
rd.compute_list_dispatch(compute_list, 512, 1, 1) #tests 512 columns
rd.compute_list_end()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment