Move all root .py files into playscii/ package directory. Rename playscii.py to app.py, add __main__.py entry point. Convert bare imports to relative (within package) and absolute (in formats/ and games/). Data dirs stay at root.
170 lines
6.5 KiB
Python
170 lines
6.5 KiB
Python
import os.path
|
|
import platform
|
|
import time
|
|
|
|
from OpenGL import GL
|
|
from OpenGL.GL import shaders
|
|
|
|
SHADER_PATH = "shaders/"
|
|
|
|
|
|
class ShaderLord:
|
|
# time in ms between checks for hot reload
|
|
hot_reload_check_interval = 2 * 1000
|
|
|
|
def __init__(self, app):
|
|
"AWAKENS THE SHADERLORD"
|
|
self.app = app
|
|
self.shaders = []
|
|
|
|
def new_shader(self, vert_source_file, frag_source_file):
|
|
self.last_check = 0
|
|
for shader in self.shaders:
|
|
if (
|
|
shader.vert_source_file == vert_source_file
|
|
and shader.frag_source_file == frag_source_file
|
|
):
|
|
# self.app.log('%s already uses same source' % shader)
|
|
return shader
|
|
s = Shader(self, vert_source_file, frag_source_file)
|
|
self.shaders.append(s)
|
|
return s
|
|
|
|
def check_hot_reload(self):
|
|
if (
|
|
self.app.get_elapsed_time() - self.last_check
|
|
< self.hot_reload_check_interval
|
|
):
|
|
return
|
|
self.last_check = self.app.get_elapsed_time()
|
|
for shader in self.shaders:
|
|
vert_shader_updated, frag_shader_updated = shader.has_updated()
|
|
if vert_shader_updated:
|
|
shader.recompile(GL.GL_VERTEX_SHADER)
|
|
if frag_shader_updated:
|
|
shader.recompile(GL.GL_FRAGMENT_SHADER)
|
|
|
|
def destroy(self):
|
|
for shader in self.shaders:
|
|
shader.destroy()
|
|
|
|
|
|
class Shader:
|
|
log_compile = False
|
|
"If True, log shader compilation"
|
|
# per-platform shader versions, declared here for easier CFG fiddling
|
|
glsl_version_windows = 130
|
|
glsl_version_unix = 130
|
|
glsl_version_macos = 150
|
|
glsl_version_es = 100
|
|
|
|
def __init__(self, shader_lord, vert_source_file, frag_source_file):
|
|
self.sl = shader_lord
|
|
# vertex shader
|
|
self.vert_source_file = vert_source_file
|
|
self.last_vert_change = time.time()
|
|
vert_source = self.get_shader_source(self.vert_source_file)
|
|
if self.log_compile:
|
|
self.sl.app.log(f"Compiling vertex shader {self.vert_source_file}...")
|
|
self.vert_shader = self.try_compile_shader(
|
|
vert_source, GL.GL_VERTEX_SHADER, self.vert_source_file
|
|
)
|
|
if self.log_compile and self.vert_shader:
|
|
self.sl.app.log(
|
|
f"Compiled vertex shader {self.vert_source_file} in {time.time() - self.last_vert_change:.6f} seconds"
|
|
)
|
|
# fragment shader
|
|
self.frag_source_file = frag_source_file
|
|
self.last_frag_change = time.time()
|
|
frag_source = self.get_shader_source(self.frag_source_file)
|
|
if self.log_compile:
|
|
self.sl.app.log(f"Compiling fragment shader {self.frag_source_file}...")
|
|
self.frag_shader = self.try_compile_shader(
|
|
frag_source, GL.GL_FRAGMENT_SHADER, self.frag_source_file
|
|
)
|
|
if self.log_compile and self.frag_shader:
|
|
self.sl.app.log(
|
|
f"Compiled fragment shader {self.frag_source_file} in {time.time() - self.last_frag_change:.6f} seconds"
|
|
)
|
|
# shader program
|
|
if self.vert_shader and self.frag_shader:
|
|
self.program = shaders.compileProgram(self.vert_shader, self.frag_shader)
|
|
|
|
def get_shader_source(self, source_file):
|
|
src = open(SHADER_PATH + source_file, "rb").read()
|
|
# prepend shader version for different platforms
|
|
if self.sl.app.context_es:
|
|
shader_version = self.glsl_version_es
|
|
elif platform.system() == "Windows":
|
|
shader_version = self.glsl_version_windows
|
|
elif platform.system() == "Darwin":
|
|
shader_version = self.glsl_version_macos
|
|
else:
|
|
shader_version = self.glsl_version_unix
|
|
version_string = f"#version {shader_version}\n"
|
|
src = bytes(version_string, "utf-8") + src
|
|
return src
|
|
|
|
def try_compile_shader(self, source, shader_type, source_filename):
|
|
"Catch and print shader compilation exceptions"
|
|
try:
|
|
shader = shaders.compileShader(source, shader_type)
|
|
except Exception as e:
|
|
self.sl.app.log(f"{source_filename}: ")
|
|
lines = e.args[0].split("\\n")
|
|
# salvage block after "shader compile failure" enclosed in b""
|
|
pre = lines.pop(0).split('b"')
|
|
for line in pre + lines[:-1]:
|
|
self.sl.app.log(" " + line)
|
|
return
|
|
return shader
|
|
|
|
def has_updated(self):
|
|
vert_mod_time = os.path.getmtime(SHADER_PATH + self.vert_source_file)
|
|
frag_mod_time = os.path.getmtime(SHADER_PATH + self.frag_source_file)
|
|
# return two values: vert shader changed, frag shader changed
|
|
vert_changed = vert_mod_time > self.last_vert_change
|
|
frag_changed = frag_mod_time > self.last_frag_change
|
|
# store new last modified time if changed
|
|
if vert_changed:
|
|
self.last_vert_change = time.time()
|
|
if frag_changed:
|
|
self.last_frag_change = time.time()
|
|
return vert_changed, frag_changed
|
|
|
|
def recompile(self, shader_type):
|
|
file_to_reload = self.vert_source_file
|
|
if shader_type == GL.GL_FRAGMENT_SHADER:
|
|
file_to_reload = self.frag_source_file
|
|
new_shader_source = self.get_shader_source(file_to_reload)
|
|
try:
|
|
new_shader = shaders.compileShader(new_shader_source, shader_type)
|
|
# TODO: use try_compile_shader instead here, make sure exception passes thru ok
|
|
self.sl.app.log(f"ShaderLord: success reloading {file_to_reload}")
|
|
except Exception:
|
|
self.sl.app.log(f"ShaderLord: failed reloading {file_to_reload}")
|
|
return
|
|
# recompile program with new shader
|
|
if shader_type == GL.GL_VERTEX_SHADER:
|
|
self.vert_shader = new_shader
|
|
else:
|
|
self.frag_shader = new_shader
|
|
self.program = shaders.compileProgram(self.vert_shader, self.frag_shader)
|
|
|
|
def get_uniform_location(self, uniform_name):
|
|
return GL.glGetUniformLocation(self.program, uniform_name)
|
|
|
|
def get_attrib_location(self, attrib_name):
|
|
return GL.glGetAttribLocation(self.program, attrib_name)
|
|
|
|
def destroy(self):
|
|
GL.glDeleteProgram(self.program)
|
|
|
|
|
|
class ShaderUniform:
|
|
# MAYBE-TODO: class for remembering uniform name, type, index.
|
|
# a Shader keeps a list of these, Renderables tell their Shader to set
|
|
# them. set methods use correct type and "try" to avoid crashes on
|
|
# hot-reload when a uniform is commented out of live GLSL.
|
|
# (try same for attributes? more data)
|
|
pass
|