playscii/renderable_line.py

562 lines
18 KiB
Python

import ctypes
import math
import platform
import time
import numpy as np
from OpenGL import GL
from renderable import TileRenderable
class LineRenderable:
"Renderable comprised of GL_LINES"
vert_shader_source = "lines_v.glsl"
vert_shader_source_3d = "lines_3d_v.glsl"
frag_shader_source = "lines_f.glsl"
log_create_destroy = False
line_width = 1
# items in vert array: 2 for XY-only renderables, 3 for ones that include Z
vert_items = 2
# use game object's art_off_pct values
use_art_offset = True
visible = True
def __init__(self, app, quad_size_ref=None, game_object=None):
self.app = app
# we may be attached to a game object
self.go = game_object
self.unique_name = "{}_{}".format(int(time.time()), self.__class__.__name__)
self.quad_size_ref = quad_size_ref
self.x, self.y, self.z = 0, 0, 0
self.scale_x, self.scale_y = 1, 1
# handle Z differently if verts are 2D vs 3D
self.scale_z = 0 if self.vert_items == 2 else 1
self.build_geo()
self.width, self.height = self.get_size()
self.reset_loc()
if self.app.use_vao:
self.vao = GL.glGenVertexArrays(1)
GL.glBindVertexArray(self.vao)
if self.vert_items == 3:
self.vert_shader_source = self.vert_shader_source_3d
self.shader = self.app.sl.new_shader(
self.vert_shader_source, self.frag_shader_source
)
# uniforms
self.proj_matrix_uniform = self.shader.get_uniform_location("projection")
self.view_matrix_uniform = self.shader.get_uniform_location("view")
self.position_uniform = self.shader.get_uniform_location("objectPosition")
self.scale_uniform = self.shader.get_uniform_location("objectScale")
self.quad_size_uniform = self.shader.get_uniform_location("quadSize")
self.color_uniform = self.shader.get_uniform_location("objectColor")
# vert buffers
self.vert_buffer, self.elem_buffer = GL.glGenBuffers(2)
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.vert_buffer)
GL.glBufferData(
GL.GL_ARRAY_BUFFER,
self.vert_array.nbytes,
self.vert_array,
GL.GL_STATIC_DRAW,
)
GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, self.elem_buffer)
GL.glBufferData(
GL.GL_ELEMENT_ARRAY_BUFFER,
self.elem_array.nbytes,
self.elem_array,
GL.GL_STATIC_DRAW,
)
GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, 0)
self.vert_count = int(len(self.elem_array))
self.pos_attrib = self.shader.get_attrib_location("vertPosition")
GL.glEnableVertexAttribArray(self.pos_attrib)
offset = ctypes.c_void_p(0)
GL.glVertexAttribPointer(
self.pos_attrib, self.vert_items, GL.GL_FLOAT, GL.GL_FALSE, 0, offset
)
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, 0)
# vert colors
self.color_buffer = GL.glGenBuffers(1)
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.color_buffer)
GL.glBufferData(
GL.GL_ARRAY_BUFFER,
self.color_array.nbytes,
self.color_array,
GL.GL_STATIC_DRAW,
)
self.color_attrib = self.shader.get_attrib_location("vertColor")
GL.glEnableVertexAttribArray(self.color_attrib)
GL.glVertexAttribPointer(
self.color_attrib, 4, GL.GL_FLOAT, GL.GL_FALSE, 0, offset
)
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, 0)
if self.app.use_vao:
GL.glBindVertexArray(0)
if self.log_create_destroy:
self.app.log("created: {}".format(self))
def __str__(self):
"for debug purposes, return a unique name"
return self.unique_name
def build_geo(self):
"""
create self.vert_array, self.elem_array, self.color_array
"""
pass
def reset_loc(self):
pass
def update(self):
if self.go:
self.update_transform_from_object(self.go)
def reset_size(self):
self.width, self.height = self.get_size()
def update_transform_from_object(self, obj):
TileRenderable.update_transform_from_object(self, obj)
def rebind_buffers(self):
# resend verts
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.vert_buffer)
GL.glBufferData(
GL.GL_ARRAY_BUFFER,
self.vert_array.nbytes,
self.vert_array,
GL.GL_STATIC_DRAW,
)
GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, self.elem_buffer)
GL.glBufferData(
GL.GL_ELEMENT_ARRAY_BUFFER,
self.elem_array.nbytes,
self.elem_array,
GL.GL_STATIC_DRAW,
)
GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, 0)
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, 0)
self.vert_count = int(len(self.elem_array))
# resend color
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.color_buffer)
GL.glBufferData(
GL.GL_ARRAY_BUFFER,
self.color_array.nbytes,
self.color_array,
GL.GL_STATIC_DRAW,
)
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, 0)
def get_projection_matrix(self):
return np.eye(4, 4)
def get_view_matrix(self):
return np.eye(4, 4)
def get_loc(self):
return self.x, self.y, self.z
def get_size(self):
# overriden in subclasses that need specific width/height data
return 1, 1
def get_quad_size(self):
if self.quad_size_ref:
return self.quad_size_ref.quad_width, self.quad_size_ref.quad_height
else:
return 1, 1
def get_color(self):
return (1, 1, 1, 1)
def get_line_width(self):
return self.line_width
def destroy(self):
if self.app.use_vao:
GL.glDeleteVertexArrays(1, [self.vao])
GL.glDeleteBuffers(3, [self.vert_buffer, self.elem_buffer, self.color_buffer])
if self.log_create_destroy:
self.app.log("destroyed: {}".format(self))
def render(self):
if not self.visible:
return
GL.glUseProgram(self.shader.program)
GL.glUniformMatrix4fv(
self.proj_matrix_uniform, 1, GL.GL_FALSE, self.get_projection_matrix()
)
GL.glUniformMatrix4fv(
self.view_matrix_uniform, 1, GL.GL_FALSE, self.get_view_matrix()
)
GL.glUniform3f(self.position_uniform, *self.get_loc())
GL.glUniform3f(self.scale_uniform, self.scale_x, self.scale_y, self.scale_z)
GL.glUniform2f(self.quad_size_uniform, *self.get_quad_size())
GL.glUniform4f(self.color_uniform, *self.get_color())
# VAO vs non-VAO paths
if self.app.use_vao:
GL.glBindVertexArray(self.vao)
else:
offset = ctypes.c_void_p(0)
# attribs:
# pos
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.vert_buffer)
GL.glVertexAttribPointer(
self.pos_attrib,
self.vert_items,
GL.GL_FLOAT,
GL.GL_FALSE,
0,
ctypes.c_void_p(0),
)
GL.glEnableVertexAttribArray(self.pos_attrib)
# color
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.color_buffer)
GL.glVertexAttribPointer(
self.color_attrib, 4, GL.GL_FLOAT, GL.GL_FALSE, 0, offset
)
GL.glEnableVertexAttribArray(self.color_attrib)
# bind elem array - see similar behavior in Cursor.render
GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, self.elem_buffer)
GL.glEnable(GL.GL_BLEND)
GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA)
if platform.system() != "Darwin":
GL.glLineWidth(self.get_line_width())
GL.glDrawElements(GL.GL_LINES, self.vert_count, GL.GL_UNSIGNED_INT, None)
GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, 0)
GL.glDisable(GL.GL_BLEND)
if self.app.use_vao:
GL.glBindVertexArray(0)
GL.glUseProgram(0)
# common data/code used by various boxes
BOX_VERTS = [(0, 0), (1, 0), (1, -1), (0, -1)]
def get_box_arrays(vert_list=None, color=(1, 1, 1, 1)):
verts = np.array(vert_list or BOX_VERTS, dtype=np.float32)
elems = np.array([0, 1, 1, 2, 2, 3, 3, 0], dtype=np.uint32)
colors = np.array([color * 4], dtype=np.float32)
return verts, elems, colors
class UIRenderableX(LineRenderable):
"Red X used to denote transparent color in various places"
color = (1, 0, 0, 1)
line_width = 2
def build_geo(self):
self.vert_array = np.array([(0, 0), (1, 1), (1, 0), (0, 1)], dtype=np.float32)
self.elem_array = np.array([0, 1, 2, 3], dtype=np.uint32)
self.color_array = np.array([self.color * 4], dtype=np.float32)
class SwatchSelectionBoxRenderable(LineRenderable):
"used for UI selection boxes etc"
color = (0.5, 0.5, 0.5, 1)
line_width = 2
def __init__(self, app, quad_size_ref):
LineRenderable.__init__(self, app, quad_size_ref)
# track tile X and Y for cursor movement
self.tile_x, self.tile_y = 0, 0
def get_color(self):
return self.color
def build_geo(self):
self.vert_array, self.elem_array, self.color_array = get_box_arrays(
None, self.color
)
class ToolSelectionBoxRenderable(LineRenderable):
line_width = 2
def get_color(self):
return (1.0, 1.0, 1.0, 1.0)
def build_geo(self):
self.vert_array, self.elem_array, self.color_array = get_box_arrays(None)
class WorldLineRenderable(LineRenderable):
"any LineRenderable that draws in world, ie in 3D perspective"
def get_projection_matrix(self):
return self.app.camera.projection_matrix
def get_view_matrix(self):
return self.app.camera.view_matrix
class DebugLineRenderable(WorldLineRenderable):
"""
renderable for drawing debug lines in the world.
use set_lines and add_lines to replace and add to, respectively, the list
of 3D vertex locations (and, optionally, colors).
"""
color = (0.5, 0, 0, 1)
vert_items = 3
line_width = 3
def set_lines(self, new_verts, new_colors=None):
"replace current debug lines with new given lines"
self.vert_array = np.array(new_verts, dtype=np.float32)
elements = []
# TODO: possible OB1 bug here, sometimes draws line to object origin(?)
for i in range(1, len(new_verts)):
elements += [i - 1, i]
self.elem_array = np.array(elements, dtype=np.uint32)
self.color_array = np.array(
new_colors or self.color * len(new_verts), dtype=np.float32
)
self.rebind_buffers()
def set_color(self, new_color):
"changes all debug lines to given color"
self.color = new_color
lines = int(len(self.vert_array) / self.vert_items)
self.color_array = np.array(self.color * lines, dtype=np.float32)
self.rebind_buffers()
def get_quad_size(self):
return 1, 1
def add_lines(self, new_verts, new_colors=None):
"add lines to the current ones"
line_items = len(self.vert_array)
lines = int(line_items / self.vert_items)
# if new_verts is a list of tuples, unpack into flat list
if type(new_verts[0]) is tuple:
new_verts_unpacked = []
for x, y, z in new_verts:
new_verts_unpacked += [x, y, z]
new_verts = new_verts_unpacked
new_size = int(line_items + len(new_verts))
self.vert_array.resize(new_size)
self.vert_array[line_items:new_size] = new_verts
# grow elem buffer
old_elem_size = len(self.elem_array)
new_elem_size = int(old_elem_size + len(new_verts) / self.vert_items)
# TODO: "contiguous" parameter that joins new lines with previous
self.elem_array.resize(new_elem_size)
self.elem_array[old_elem_size:new_elem_size] = range(
old_elem_size, new_elem_size
)
# grow color buffer
old_color_size = len(self.color_array)
new_color_size = int(old_color_size + len(new_verts) / self.vert_items * 4)
self.color_array.resize(new_color_size)
self.color_array[old_color_size:new_color_size] = (
new_colors or self.color * int(len(new_verts) / self.vert_items)
)
self.rebind_buffers()
def reset_lines(self):
self.build_geo()
def build_geo(self):
# start empty
self.vert_array = np.array([], dtype=np.float32)
self.elem_array = np.array([], dtype=np.uint32)
self.color_array = np.array([], dtype=np.float32)
def render(self):
# only render if we have any data
if len(self.vert_array) == 0:
return
WorldLineRenderable.render(self)
class OriginIndicatorRenderable(WorldLineRenderable):
"classic 3-axis thingy showing location/rotation/scale"
red = (1.0, 0.1, 0.1, 1.0)
green = (0.1, 1.0, 0.1, 1.0)
blue = (0.1, 0.1, 1.0, 1.0)
origin = (0, 0, 0)
x_axis = (1, 0, 0)
y_axis = (0, 1, 0)
z_axis = (0, 0, 1)
vert_items = 3
line_width = 3
use_art_offset = False
def __init__(self, app, game_object):
LineRenderable.__init__(self, app, None, game_object)
def get_quad_size(self):
return 1, 1
def get_size(self):
return self.go.scale_x, self.go.scale_y
def update_transform_from_object(self, obj):
self.x, self.y, self.z = obj.x, obj.y, obj.z
self.scale_x, self.scale_y = obj.scale_x, obj.scale_y
if obj.flip_x:
self.scale_x *= -1
self.scale_z = obj.scale_z
def build_geo(self):
self.vert_array = np.array(
[
self.origin,
self.x_axis,
self.origin,
self.y_axis,
self.origin,
self.z_axis,
],
dtype=np.float32,
)
self.elem_array = np.array([0, 1, 2, 3, 4, 5], dtype=np.uint32)
self.color_array = np.array(
[self.red, self.red, self.green, self.green, self.blue, self.blue],
dtype=np.float32,
)
class BoundsIndicatorRenderable(WorldLineRenderable):
color = (1, 1, 1, 0.5)
line_width_active = 2
line_width_inactive = 1
def __init__(self, app, game_object):
self.art = game_object.renderable.art
LineRenderable.__init__(self, app, None, game_object)
def set_art(self, new_art):
self.art = new_art
self.reset_size()
def get_size(self):
art = self.go.art
w = (art.width * art.quad_width) * self.go.scale_x
h = (art.height * art.quad_height) * self.go.scale_y
return w, h
def get_color(self):
# pulse if selected
if self.go in self.app.gw.selected_objects:
color = 0.75 + (math.sin(self.app.get_elapsed_time() / 100) / 2)
return (color, color, color, 1)
else:
return (1, 1, 1, 1)
def get_line_width(self):
return (
self.line_width_active
if self.go in self.app.gw.selected_objects
else self.line_width_inactive
)
def get_quad_size(self):
if not self.go:
return 1, 1
return (
self.art.width * self.art.quad_width,
self.art.height * self.art.quad_height,
)
def build_geo(self):
self.vert_array, self.elem_array, self.color_array = get_box_arrays(
None, self.color
)
class CollisionRenderable(WorldLineRenderable):
# green = dynamic, blue = static
dynamic_color = (0, 1, 0, 1)
static_color = (0, 0, 1, 1)
def __init__(self, shape):
self.color = self.dynamic_color if shape.go.is_dynamic() else self.static_color
self.shape = shape
WorldLineRenderable.__init__(self, shape.go.app, None, shape.go)
def update(self):
self.update_transform_from_object(self.shape)
def update_transform_from_object(self, obj):
self.x = obj.x
self.y = obj.y
def get_circle_points(radius, steps=24):
angle = 0
points = [(radius, 0)]
for _ in range(steps):
angle += math.radians(360 / steps)
x = math.cos(angle) * radius
y = math.sin(angle) * radius
points.append((x, y))
return points
class CircleCollisionRenderable(CollisionRenderable):
line_width = 2
segments = 24
def get_quad_size(self):
return self.shape.radius, self.shape.radius
def get_size(self):
w = h = self.shape.radius * 2
w *= self.go.scale_x
h *= self.go.scale_y
return w, h
def build_geo(self):
verts, elements, colors = [], [], []
angle = 0
last_x, last_y = 1, 0
i = 0
while i < self.segments * 4:
angle += math.radians(360 / self.segments)
verts.append((last_x, last_y))
x = math.cos(angle)
y = math.sin(angle)
verts.append((x, y))
last_x, last_y = x, y
elements.append((i, i + 1))
i += 2
colors.append([self.color * 2])
self.vert_array = np.array(verts, dtype=np.float32)
self.elem_array = np.array(elements, dtype=np.uint32)
self.color_array = np.array(colors, dtype=np.float32)
class BoxCollisionRenderable(CollisionRenderable):
line_width = 2
def get_quad_size(self):
return self.shape.halfwidth * 2, self.shape.halfheight * 2
def get_size(self):
w, h = self.shape.halfwidth * 2, self.shape.halfheight * 2
w *= self.go.scale_x
h *= self.go.scale_y
return w, h
def build_geo(self):
verts = [(-0.5, 0.5), (0.5, 0.5), (0.5, -0.5), (-0.5, -0.5)]
self.vert_array, self.elem_array, self.color_array = get_box_arrays(
verts, self.color
)
class TileBoxCollisionRenderable(BoxCollisionRenderable):
"box for each tile in a CST_TILE object"
line_width = 1
def get_loc(self):
# draw at Z level of collision layer
return self.x, self.y, self.go.get_layer_z(self.go.col_layer_name)