561 lines
18 KiB
Python
561 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 = f"{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(f"created: {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(f"destroyed: {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)
|
|
# 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)
|