Skip to content

Instantly share code, notes, and snippets.

@partybusiness
Last active May 18, 2024 15:34
Show Gist options
  • Save partybusiness/e51b71ee873cf3b56c62a9f377eb2e15 to your computer and use it in GitHub Desktop.
Save partybusiness/e51b71ee873cf3b56c62a9f377eb2e15 to your computer and use it in GitHub Desktop.
Displays tile sheet of characters as tiles stored in a byte array.
shader_type spatial;
//render_mode unshaded;
uniform uint tilemap[1024]; // up to 128 8-byte 8x8 tiles
uniform uint screen[1000];// 40 x 25 screen map, 7 bits index one of the 128 tiles, highest bit inverts
uniform uint screenWidth = 40;
uniform uint screenHeight = 25;
//gets whether a pixel within a given tile is foreground or background
bool getPixel (uint x, uint y, uint tileIndex) { //x and y within 8x8 tile, index of tile in tilemap
uint tileAddress = (tileIndex << 3u); //8 bytes per tile
uint chunk = tilemap[tileAddress + y];
uint sample = chunk >> x & 1u;
return sample > 0u;
}
void fragment() {
float sw = float(screenWidth);
float sh = float(screenHeight);
//get current tile
uint tilex = uint(floor(UV.x*sw));
uint tiley = uint(floor(UV.y*sh));
//get x and y pixel within current tile
vec2 tileUV = (UV - vec2(float(tilex)/sw, float(tiley)/sh)) * vec2(sw,sh);
uint xindex = uint(floor(tileUV.x*8.));
uint yindex = uint(floor(tileUV.y*8.)); //max = 4294967295 //2147483648
uint screenTile = screen[tilex + tiley * screenWidth];
//get colours
uint index = screenTile & 0x7Fu; //bottom 7 bits
bool ispixel = getPixel(xindex, yindex, index);
bool invert = (screenTile & 0x80u) > 0u;
ispixel = (ispixel &&!invert) || (!ispixel &&invert);
ALBEDO.rgb = vec3(0.05);
EMISSION.rgb = vec3(ispixel?2.:0.0); //goes above 1 to get a bloom effect
}
extends Node
class_name ComputerScreenComm
#data to define character bitmaps, 128 characters, eight bytes per character
var characterSheet:PackedByteArray = [0x3C,0x66,0x76,0x76,0x06,0x46,0x3C,0x00,0x18,0x3C,0x66,0x7E,0x66,0x66,0x66,0x00,0x3E,0x66,0x66,0x3E,0x66,0x66,0x3E,0x00,0x3C,0x66,0x06,0x06,0x06,0x66,0x3C,0x00,
0x1E,0x36,0x66,0x66,0x66,0x36,0x1E,0x00,0x7E,0x06,0x06,0x1E,0x06,0x06,0x7E,0x00,0x7E,0x06,0x06,0x1E,0x06,0x06,0x06,0x00,0x3C,0x66,0x06,0x76,0x66,0x66,0x3C,0x00,
0x66,0x66,0x66,0x7E,0x66,0x66,0x66,0x00,0x3C,0x18,0x18,0x18,0x18,0x18,0x3C,0x00,0x78,0x30,0x30,0x30,0x30,0x36,0x1C,0x00,0x66,0x36,0x1E,0x0E,0x1E,0x36,0x66,0x00,
0x06,0x06,0x06,0x06,0x06,0x06,0x7E,0x00,0xC6,0xEE,0xFE,0xD6,0xC6,0xC6,0xC6,0x00,0x66,0x6E,0x7E,0x7E,0x76,0x66,0x66,0x00,0x3C,0x66,0x66,0x66,0x66,0x66,0x3C,0x00,
0x3E,0x66,0x66,0x3E,0x06,0x06,0x06,0x00,0x3C,0x66,0x66,0x66,0x66,0x3C,0x70,0x00,0x3E,0x66,0x66,0x3E,0x1E,0x36,0x66,0x00,0x3C,0x66,0x06,0x3C,0x60,0x66,0x3C,0x00,
0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x00,0x66,0x66,0x66,0x66,0x66,0x66,0x3C,0x00,0x66,0x66,0x66,0x66,0x66,0x3C,0x18,0x00,0xC6,0xC6,0xC6,0xD6,0xFE,0xEE,0xC6,0x00,
0x66,0x66,0x3C,0x18,0x3C,0x66,0x66,0x00,0x66,0x66,0x66,0x3C,0x18,0x18,0x18,0x00,0x7E,0x60,0x30,0x18,0x0C,0x06,0x7E,0x00,0x3C,0x0C,0x0C,0x0C,0x0C,0x0C,0x3C,0x00,
0x30,0x48,0x0C,0x3E,0x0C,0x46,0x3F,0x00,0x3C,0x30,0x30,0x30,0x30,0x30,0x3C,0x00,0x00,0x18,0x3C,0x7E,0x18,0x18,0x18,0x18,0x00,0x08,0x0C,0xFE,0xFE,0x0C,0x08,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x18,0x18,0x00,0x00,0x18,0x00,0x66,0x66,0x66,0x00,0x00,0x00,0x00,0x00,0x66,0x66,0xFF,0x66,0xFF,0x66,0x66,0x00,
0x18,0x7C,0x06,0x3C,0x60,0x3E,0x18,0x00,0x46,0x66,0x30,0x18,0x0C,0x66,0x62,0x00,0x3C,0x66,0x3C,0x1C,0xE6,0x66,0xFC,0x00,0x60,0x30,0x18,0x00,0x00,0x00,0x00,0x00,
0x30,0x18,0x0C,0x0C,0x0C,0x18,0x30,0x00,0x0C,0x18,0x30,0x30,0x30,0x18,0x0C,0x00,0x00,0x66,0x3C,0xFF,0x3C,0x66,0x00,0x00,0x00,0x18,0x18,0x7E,0x18,0x18,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x0C,0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x00,0x00,0xC0,0x60,0x30,0x18,0x0C,0x06,0x00,
0x3C,0x66,0x76,0x6E,0x66,0x66,0x3C,0x00,0x18,0x18,0x1C,0x18,0x18,0x18,0x7E,0x00,0x3C,0x66,0x60,0x30,0x0C,0x06,0x7E,0x00,0x3C,0x66,0x60,0x38,0x60,0x66,0x3C,0x00,
0x60,0x70,0x78,0x66,0xFE,0x60,0x60,0x00,0x7E,0x06,0x3E,0x60,0x60,0x66,0x3C,0x00,0x3C,0x66,0x06,0x3E,0x66,0x66,0x3C,0x00,0x7E,0x66,0x30,0x18,0x18,0x18,0x18,0x00,
0x3C,0x66,0x66,0x3C,0x66,0x66,0x3C,0x00,0x3C,0x66,0x66,0x7C,0x60,0x66,0x3C,0x00,0x00,0x00,0x18,0x00,0x00,0x18,0x00,0x00,0x00,0x00,0x18,0x00,0x00,0x18,0x18,0x0C,
0x70,0x18,0x0C,0x06,0x0C,0x18,0x70,0x00,0x00,0x00,0x7E,0x00,0x7E,0x00,0x00,0x00,0x0E,0x18,0x30,0x60,0x30,0x18,0x0E,0x00,0x3C,0x66,0x60,0x30,0x18,0x00,0x18,0x00,
0x00,0x00,0x00,0xFF,0xFF,0x00,0x00,0x00,0x10,0x38,0x7C,0xFE,0xFE,0x38,0x7C,0x00,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x00,0x00,0x00,0xFF,0xFF,0x00,0x00,0x00,
0x00,0x00,0xFF,0xFF,0x00,0x00,0x00,0x00,0x00,0xFF,0xFF,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0xFF,0x00,0x00,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,
0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x00,0x00,0x00,0x07,0x0F,0x1C,0x18,0x18,0x18,0x18,0x38,0xF0,0xE0,0x00,0x00,0x00,0x18,0x18,0x1C,0x0F,0x07,0x00,0x00,0x00,
0x03,0x03,0x03,0x03,0x03,0x03,0xFF,0xFF,0x03,0x07,0x0E,0x1C,0x38,0x70,0xE0,0xC0,0xC0,0xE0,0x70,0x38,0x1C,0x0E,0x07,0x03,0xFF,0xFF,0x03,0x03,0x03,0x03,0x03,0x03,
0xFF,0xFF,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0x00,0x3C,0x7E,0x7E,0x7E,0x7E,0x3C,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0xFF,0x00,0x6C,0xFE,0xFE,0xFE,0x7C,0x38,0x10,0x00,
0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x00,0x00,0x00,0xE0,0xF0,0x38,0x18,0x18,0xC3,0xE7,0x7E,0x3C,0x3C,0x7E,0xE7,0xC3,0x00,0x3C,0x7E,0x66,0x66,0x7E,0x3C,0x00,
0x18,0x18,0x66,0x66,0x18,0x18,0x3C,0x00,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x10,0x38,0x7C,0xFE,0x7C,0x38,0x10,0x00,0x18,0x18,0x18,0xFF,0xFF,0x18,0x18,0x18,
0x03,0x03,0x0C,0x0C,0x03,0x03,0x0C,0x0C,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x00,0x00,0xC0,0x7C,0x6E,0x6C,0x6C,0x00,0xFF,0xFE,0xFC,0xF8,0xF0,0xE0,0xC0,0x80,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0F,0x0F,0x0F,0x0F,0x0F,0x0F,0x0F,0x0F,0x00,0x00,0x00,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x33,0x33,0xCC,0xCC,0x33,0x33,0xCC,0xCC,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,
0x00,0x00,0x00,0x00,0x33,0x33,0xCC,0xCC,0xFF,0x7F,0x3F,0x1F,0x0F,0x07,0x03,0x01,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0x18,0x18,0x18,0xF8,0xF8,0x18,0x18,0x18,
0x00,0x00,0x00,0x00,0xF0,0xF0,0xF0,0xF0,0x18,0x18,0x18,0xF8,0xF8,0x00,0x00,0x00,0x00,0x00,0x00,0x1F,0x1F,0x18,0x18,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0xFF,
0x00,0x00,0x00,0xF8,0xF8,0x18,0x18,0x18,0x18,0x18,0x18,0xFF,0xFF,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0xFF,0x18,0x18,0x18,0x18,0x18,0x18,0x1F,0x1F,0x18,0x18,0x18,
0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x07,0x07,0x07,0x07,0x07,0x07,0x07,0x07,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xFF,0xFF,0x00,0x00,0x00,0x00,0x00,0x00,
0xFF,0xFF,0xFF,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0xFF,0xFF,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xFF,0xFF,0x00,0x00,0x00,0x00,0x0F,0x0F,0x0F,0x0F,
0xF0,0xF0,0xF0,0xF0,0x00,0x00,0x00,0x00,0x18,0x18,0x18,0x1F,0x1F,0x00,0x00,0x00,0x0F,0x0F,0x0F,0x0F,0x00,0x00,0x00,0x00,0x0F,0x0F,0x0F,0x0F,0xF0,0xF0,0xF0,0xF0]
#used to generate indices for common characters
# ^ and { fill in for arrows, I will need some other method for
var characterList:String = "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[£]^{ !\"#$%&'()*+,-./0123456789:;<=>?_`~}|\\"
#index of space character
const SPACE:int = 32
var characterDict:Dictionary
@export var screenMaterial:ShaderMaterial
var screenContents:PackedByteArray = []
const screenWidth:int = 40
const screenHeight:int = 25
#whether the screenContents has been recently updated
var screenUpdated:bool = false
#current cursor position
var cX:int = 0
#whether the screen should scroll upward if you carriage return past the bottom of the screen
# turn this off if you want to be able to set the final character in the bottom left corner without scrolling the screen up
var scrollMode:bool = false
#whether to display the cursor position
var displayCursor:bool = true
#counter for blinking the cursor
var blinkCounter:float = 0.
#blink rate
const blinkThreshold:float = 0.5
#character you can use
var copiedCharacter:int = SPACE
func _ready():
screenContents.resize(screenWidth*screenHeight)
screenContents.fill(SPACE)
screenMaterial.set_shader_parameter("tilemap", characterSheet)
screenMaterial.set_shader_parameter("screen", screenContents)
screenMaterial.set_shader_parameter("screenWidth", screenWidth)
screenMaterial.set_shader_parameter("screenheight", screenHeight)
populate_dictionary ()
#var demoBytes:PackedByteArray = []
#demoBytes.resize(25)
#demoBytes.fill(90)
#draw_bytes(demoBytes, 4, 4, 5)
func _process(delta):
if (screenUpdated):
screenUpdated = false
screenMaterial.set_shader_parameter("screen", screenContents)
if (displayCursor):
blinkCounter += delta
if blinkCounter>blinkThreshold:
blinkCounter -= blinkThreshold
invert_character(cX, 0)
#typing demo, remove if you don't want the player to type directly to the screen
func _unhandled_input(event):
if event is InputEventKey:
if event.pressed:
match event.keycode:
KEY_RIGHT:
move_cursor(1,0)
KEY_LEFT:
move_cursor(-1,0)
KEY_UP:
move_cursor(0,-1)
KEY_DOWN:
move_cursor(0,1)
KEY_ENTER:
print_string("\n", false)
KEY_BACKSPACE:
move_cursor(-1,0)
set_character(SPACE, cX, 0, false)
KEY_DELETE:
set_character(SPACE, cX, 0, false)
KEY_ESCAPE:
randomize_screen() #if you want typing on the computer to be a temp overide to regular input you could use this to leave typing mode
KEY_PAGEDOWN: #some functions to help get values that aren't the basic characters
set_character(screenContents[cX] + 1, cX, 0, false)
KEY_PAGEUP:
set_character(screenContents[cX] - 1, cX, 0, false)
KEY_HOME: #lets you copy and paste a character for drawing
copiedCharacter = screenContents[cX]
KEY_INSERT:
set_character(copiedCharacter, cX, 0, false)
_:
var character = char(event.unicode)
print(event.as_text_key_label())
print_string(character.to_upper(), false)
#populates a character dictionary from the contents of characterList
func populate_dictionary () :
characterDict.clear()
for i in range(0, characterList.length()):
characterDict[characterList[i]] = i
#gets the index of a character from the string of that character using the characterDict
func character_to_index(string:String) -> int:
if (characterDict.has(string)):
return characterDict[string]
return 0
#sets a given character tile within the screenContents
func set_character(cIndex:int, x:int, y:int, invert:bool):
if (x + y * screenWidth >= screenHeight * screenWidth):
return
var tileValue:int = cIndex | (0x80 if invert else 0)
screenContents [x + y * screenWidth] = tileValue
screenUpdated = true
func randomize_character(x:int, y:int):
if (x + y * screenWidth >= screenHeight * screenWidth):
return
screenContents [x + y * screenWidth] = randi_range(0, 0xFF)
screenUpdated = true
func randomize_screen():
for x in range(0, screenWidth * screenHeight):
randomize_character(x, 0)
#inverts the character at a given position
func invert_character(x:int, y:int):
if (x + y * screenWidth >= screenHeight * screenWidth):
return
screenContents [x + y * screenWidth] = screenContents [x + y * screenWidth] ^ 0x80
screenUpdated = true
#sets a given character to either be inverted or not
func set_invert_character(x:int, y:int, value:bool):
if (x + y * screenWidth >= screenHeight * screenWidth):
return
if value:
screenContents [x + y * screenWidth] = screenContents [x + y * screenWidth] | 0x80 #set to on
else:
screenContents [x + y * screenWidth] = screenContents [x + y * screenWidth] & 0x7F #set to off
screenUpdated = true
#will assign the array of bytes starting at x,y as a rectangle of w width
# could be used essentially as a blit function if you have a pre-defined image to display
func draw_bytes(newBytes:PackedByteArray, x:int, y:int, w:int):
var cursor = x + y * screenWidth
var wCount = 0
for i in range(0,newBytes.size()):
screenContents[cursor] = newBytes[i]
wCount += 1
if wCount == w:
cursor += screenWidth - w + 1
wCount = 0
else:
cursor += 1
if cursor >= screenWidth * screenHeight:
cursor -= screenWidth * screenHeight
screenUpdated = true
#helper function to generate an array that can be passed to the draw_bytes function
func copy_bytes(x:int, y:int, w:int, h:int) -> PackedByteArray:
var result:PackedByteArray = []
var cursor = x + y * screenWidth
var wCount = 0
for i in range(0,w*h):
result[i] = screenContents[cursor]
wCount += 1
if wCount == w:
cursor += screenWidth - w + 1
wCount = 0
else:
cursor += 1
if cursor >= screenWidth * screenHeight:
cursor -= screenWidth * screenHeight
return result
#adds one line to the bottom of the screen and removes one line from the top
func scroll():
cX -= screenWidth
var blankRow:PackedByteArray = [] #do I create it every time or should I have a blank row on hand?
blankRow.resize(screenWidth)
blankRow.fill(SPACE)
screenContents = screenContents.slice(screenWidth)
screenContents.append_array(blankRow)
screenUpdated = true
#prints the given string at the current cX and cY
func print_string(newText:String, inverted: bool):
for i in range(0,newText.length()):
var newChar = newText[i]
match (newChar):
"\n": # special case for newline
carriage_return()
_: #default
var index:int = character_to_index(newChar)
set_character(index, cX, 0, inverted)
cX+=1
if (cX>=screenWidth*screenHeight):
if scrollMode:
scroll()
else:
cX -= screenWidth * screenHeight #instead wrap cursor back to top
#sets current cursor position
func set_cursor(x:int, y:int):
refresh_cursor()
cX = (x + y * screenWidth) % (screenHeight * screenWidth)
if (cX <0): #no negative values
cX += (screenHeight * screenWidth)
#moves current cursor position
func move_cursor(dX:int, dY:int):
set_cursor(cX + dX, dY)
#moves cursor position to new line and scrolls screen if scrollMode is true
func carriage_return():
refresh_cursor()
cX = (cX + screenWidth) / screenWidth * screenWidth
if cX >= screenWidth * screenHeight:
if scrollMode:
scroll()
else:
cX -= screenWidth * screenHeight #instead wrap cursor back to top
#erases the current cursor position
func refresh_cursor():
if displayCursor:
set_invert_character(cX, 0, false)
blinkCounter = blinkThreshold
@tool
extends EditorScript
class_name GenerateComputerCharacters
#an image of pixel font you want to convert for use with ComputerScreen.gdshader and ComputerScreenComm.gd
const sourceImage:String = "res://computers/commodoretext.png" #image we get tiles from
const width:int = 6 # how many tiles wide is the source image
const height:int = 22 # how many tiles tall is the source image
const fileName:String = "res://computers/com_char_set.rom" #file the resulting array is saved to
const textfileName:String = "res://computers/com_char_set.txt" #textfile the resulting array is saved to
#in my scene DisplayTiles is a MeshInstance2D using a material with the DisplayTiles shader
const displayNode:String = "DisplayTiles"
# name of the uniform uint parameter on the shader that accepts the resulting array of tiles
# tilemap matches what I used in the DiplayTiles shader
const shaderParameterName:String = "tilemap"
#converts 8x8 tile from image into 8 bytes where each byte is one row and each pixel is one bit
func get_tile(image:Image, x:int, y:int) -> PackedByteArray:
const ts:int = 8 #8x8 pixels
var result:PackedByteArray = PackedByteArray()
result.resize(8) #8 bytes to store 8x8 pixels
result.fill(0)
var ds:String #debug string
for yy in range(0,ts):
for xx in range(0,ts):
var colour = image.get_pixel(x*ts + xx,y*ts + yy)
var ispixel:bool = colour.r > 0.5
if (ispixel):
var bitmask:int = 0x1 << xx
#print("%d %d %x" % [xx, yy, bitmask])
result[yy] |= bitmask
ds = ds + "@"
else:
ds = ds + " "
ds = ds + "\n"
#printing the contents of the tile
print(ds)
for m in result:
print("%02X" % m)
return result
#gets index of given tile
func get_index(x:int, y:int) -> int:
return y * width + x
#gets a row of indices from x1 to x2
func get_index_range(x1:int, x2:int, y:int) -> Array:
var result:Array
for x in range(x1,x2):
result.append(get_index(x,y))
return result
func _run():
var image:Image = Image.load_from_file(sourceImage)
var tileData:PackedByteArray = PackedByteArray()
for y in range(0,height-1):
for x in range(0,width):
tileData.append_array (get_tile(image,x,y))
for x in range(0,2): #my bitmap had just two characters on the last line because the numbers didn't work out
tileData.append_array (get_tile(image,x,height-1))
#assigns result to a shader on the displayNode
#var currentScene = get_scene()
#var display = currentScene.find_child(displayNode, true)
#display.material.set_shader_parameter(shaderParameterName, tileData)
#stores result in a binary file
var filePath = ProjectSettings.globalize_path(fileName)
var file = FileAccess.open(filePath, FileAccess.WRITE)
file.store_buffer ( tileData )
file.close()
print("Saved Bytes")
#stores result in a text file, which can be used for default values in a gdscript
filePath = ProjectSettings.globalize_path(textfileName)
file = FileAccess.open(filePath, FileAccess.WRITE)
var count:int =0
for byte in tileData:
file.store_string("0x%02X," % byte)
count+=1
if count==32: #insert arbitrary newlines to keep lines from being too long
count =0
file.store_string("\n")
file.close()
print("Saved Text")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment