You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
315 lines
10 KiB
315 lines
10 KiB
import pyray as rl
|
|
import numpy as np
|
|
from typing import Any
|
|
|
|
MAX_GRADIENT_COLORS = 15
|
|
|
|
FRAGMENT_SHADER = """
|
|
#version 300 es
|
|
precision mediump float;
|
|
|
|
in vec2 fragTexCoord;
|
|
out vec4 finalColor;
|
|
|
|
uniform vec2 points[100];
|
|
uniform int pointCount;
|
|
uniform vec4 fillColor;
|
|
uniform vec2 resolution;
|
|
|
|
uniform int useGradient;
|
|
uniform vec2 gradientStart;
|
|
uniform vec2 gradientEnd;
|
|
uniform vec4 gradientColors[15];
|
|
uniform float gradientStops[15];
|
|
uniform int gradientColorCount;
|
|
|
|
vec4 getGradientColor(vec2 pos) {
|
|
vec2 gradientDir = gradientEnd - gradientStart;
|
|
float gradientLength = length(gradientDir);
|
|
if (gradientLength < 0.001) return gradientColors[0];
|
|
|
|
vec2 normalizedDir = gradientDir / gradientLength;
|
|
float t = clamp(dot(pos - gradientStart, normalizedDir) / gradientLength, 0.0, 1.0);
|
|
|
|
if (gradientColorCount <= 1) return gradientColors[0];
|
|
for (int i = 0; i < gradientColorCount - 1; i++) {
|
|
if (t >= gradientStops[i] && t <= gradientStops[i+1]) {
|
|
float segmentT = (t - gradientStops[i]) / (gradientStops[i+1] - gradientStops[i]);
|
|
return mix(gradientColors[i], gradientColors[i+1], segmentT);
|
|
}
|
|
}
|
|
|
|
return gradientColors[gradientColorCount-1];
|
|
}
|
|
|
|
bool isPointInsidePolygon(vec2 p) {
|
|
if (pointCount < 3) return false;
|
|
int crossings = 0;
|
|
for (int i = 0, j = pointCount - 1; i < pointCount; j = i++) {
|
|
vec2 pi = points[i];
|
|
vec2 pj = points[j];
|
|
if (distance(pi, pj) < 0.001) continue;
|
|
if (((pi.y > p.y) != (pj.y > p.y)) &&
|
|
(p.x < (pj.x - pi.x) * (p.y - pi.y) / (pj.y - pi.y + 0.001) + pi.x)) {
|
|
crossings++;
|
|
}
|
|
}
|
|
return (crossings & 1) == 1;
|
|
}
|
|
|
|
float distanceToEdge(vec2 p) {
|
|
float minDist = 1000.0;
|
|
|
|
for (int i = 0, j = pointCount - 1; i < pointCount; j = i++) {
|
|
vec2 edge0 = points[j];
|
|
vec2 edge1 = points[i];
|
|
|
|
if (distance(edge0, edge1) < 0.0001) continue;
|
|
|
|
vec2 v1 = p - edge0;
|
|
vec2 v2 = edge1 - edge0;
|
|
float l2 = dot(v2, v2);
|
|
|
|
if (l2 < 0.0001) {
|
|
float dist = length(v1);
|
|
minDist = min(minDist, dist);
|
|
continue;
|
|
}
|
|
|
|
float t = clamp(dot(v1, v2) / l2, 0.0, 1.0);
|
|
vec2 projection = edge0 + t * v2;
|
|
float dist = length(p - projection);
|
|
minDist = min(minDist, dist);
|
|
}
|
|
|
|
return minDist;
|
|
}
|
|
|
|
void main() {
|
|
vec2 pixel = fragTexCoord * resolution;
|
|
|
|
// Compute pixel size for anti-aliasing
|
|
vec2 pixelGrad = vec2(dFdx(pixel.x), dFdy(pixel.y));
|
|
float pixelSize = length(pixelGrad);
|
|
float aaWidth = max(0.5, pixelSize * 1.5);
|
|
|
|
bool inside = isPointInsidePolygon(pixel);
|
|
if (inside) {
|
|
finalColor = useGradient == 1 ? getGradientColor(pixel) : fillColor;
|
|
return;
|
|
}
|
|
|
|
float sd = -distanceToEdge(pixel);
|
|
float alpha = smoothstep(-aaWidth, aaWidth, sd);
|
|
if (alpha > 0.0){
|
|
vec4 color = useGradient == 1 ? getGradientColor(pixel) : fillColor;
|
|
finalColor = vec4(color.rgb, color.a * alpha);
|
|
} else {
|
|
finalColor = vec4(0.0);
|
|
}
|
|
}
|
|
"""
|
|
|
|
# Default vertex shader
|
|
VERTEX_SHADER = """
|
|
#version 300 es
|
|
in vec3 vertexPosition;
|
|
in vec2 vertexTexCoord;
|
|
out vec2 fragTexCoord;
|
|
uniform mat4 mvp;
|
|
|
|
void main() {
|
|
fragTexCoord = vertexTexCoord;
|
|
gl_Position = mvp * vec4(vertexPosition, 1.0);
|
|
}
|
|
"""
|
|
|
|
|
|
UNIFORM_INT = rl.ShaderUniformDataType.SHADER_UNIFORM_INT
|
|
UNIFORM_FLOAT = rl.ShaderUniformDataType.SHADER_UNIFORM_FLOAT
|
|
UNIFORM_VEC2 = rl.ShaderUniformDataType.SHADER_UNIFORM_VEC2
|
|
UNIFORM_VEC4 = rl.ShaderUniformDataType.SHADER_UNIFORM_VEC4
|
|
|
|
|
|
class ShaderState:
|
|
_instance: Any = None
|
|
|
|
@classmethod
|
|
def get_instance(cls):
|
|
if cls._instance is None:
|
|
cls._instance = cls()
|
|
return cls._instance
|
|
|
|
def __init__(self):
|
|
if ShaderState._instance is not None:
|
|
raise Exception("This class is a singleton. Use get_instance() instead.")
|
|
|
|
self.initialized = False
|
|
self.shader = None
|
|
self.white_texture = None
|
|
|
|
# Shader uniform locations
|
|
self.locations = {
|
|
'pointCount': None,
|
|
'fillColor': None,
|
|
'resolution': None,
|
|
'points': None,
|
|
'useGradient': None,
|
|
'gradientStart': None,
|
|
'gradientEnd': None,
|
|
'gradientColors': None,
|
|
'gradientStops': None,
|
|
'gradientColorCount': None,
|
|
'mvp': None,
|
|
}
|
|
|
|
# Pre-allocated FFI objects
|
|
self.point_count_ptr = rl.ffi.new("int[]", [0])
|
|
self.resolution_ptr = rl.ffi.new("float[]", [0.0, 0.0])
|
|
self.fill_color_ptr = rl.ffi.new("float[]", [0.0, 0.0, 0.0, 0.0])
|
|
self.use_gradient_ptr = rl.ffi.new("int[]", [0])
|
|
self.gradient_start_ptr = rl.ffi.new("float[]", [0.0, 0.0])
|
|
self.gradient_end_ptr = rl.ffi.new("float[]", [0.0, 0.0])
|
|
self.color_count_ptr = rl.ffi.new("int[]", [0])
|
|
self.gradient_colors_ptr = rl.ffi.new("float[]", MAX_GRADIENT_COLORS * 4)
|
|
self.gradient_stops_ptr = rl.ffi.new("float[]", MAX_GRADIENT_COLORS)
|
|
|
|
def initialize(self):
|
|
if self.initialized:
|
|
return
|
|
|
|
self.shader = rl.load_shader_from_memory(VERTEX_SHADER, FRAGMENT_SHADER)
|
|
|
|
# Create and cache white texture
|
|
white_img = rl.gen_image_color(2, 2, rl.WHITE)
|
|
self.white_texture = rl.load_texture_from_image(white_img)
|
|
rl.set_texture_filter(self.white_texture, rl.TEXTURE_FILTER_BILINEAR)
|
|
rl.unload_image(white_img)
|
|
|
|
# Cache all uniform locations
|
|
for uniform in self.locations.keys():
|
|
self.locations[uniform] = rl.get_shader_location(self.shader, uniform)
|
|
|
|
# Setup default MVP matrix
|
|
mvp_ptr = rl.ffi.new("float[16]", [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0])
|
|
rl.set_shader_value_matrix(self.shader, self.locations['mvp'], rl.Matrix(*mvp_ptr))
|
|
|
|
self.initialized = True
|
|
|
|
def cleanup(self):
|
|
if not self.initialized:
|
|
return
|
|
|
|
if self.white_texture:
|
|
rl.unload_texture(self.white_texture)
|
|
self.white_texture = None
|
|
|
|
if self.shader:
|
|
rl.unload_shader(self.shader)
|
|
self.shader = None
|
|
|
|
self.initialized = False
|
|
|
|
|
|
def _configure_shader_color(state, color, gradient, clipped_rect, original_rect):
|
|
use_gradient = 1 if gradient else 0
|
|
state.use_gradient_ptr[0] = use_gradient
|
|
rl.set_shader_value(state.shader, state.locations['useGradient'], state.use_gradient_ptr, UNIFORM_INT)
|
|
|
|
if use_gradient:
|
|
start = np.array(gradient['start']) * np.array([original_rect.width, original_rect.height]) + np.array([original_rect.x, original_rect.y])
|
|
end = np.array(gradient['end']) * np.array([original_rect.width, original_rect.height]) + np.array([original_rect.x, original_rect.y])
|
|
start = start - np.array([clipped_rect.x, clipped_rect.y])
|
|
end = end - np.array([clipped_rect.x, clipped_rect.y])
|
|
state.gradient_start_ptr[0:2] = start.astype(np.float32)
|
|
state.gradient_end_ptr[0:2] = end.astype(np.float32)
|
|
rl.set_shader_value(state.shader, state.locations['gradientStart'], state.gradient_start_ptr, UNIFORM_VEC2)
|
|
rl.set_shader_value(state.shader, state.locations['gradientEnd'], state.gradient_end_ptr, UNIFORM_VEC2)
|
|
|
|
colors = gradient['colors']
|
|
color_count = min(len(colors), MAX_GRADIENT_COLORS)
|
|
state.color_count_ptr[0] = color_count
|
|
for i, c in enumerate(colors[:color_count]):
|
|
base_idx = i * 4
|
|
state.gradient_colors_ptr[base_idx:base_idx+4] = [c.r / 255.0, c.g / 255.0, c.b / 255.0, c.a / 255.0]
|
|
rl.set_shader_value_v(state.shader, state.locations['gradientColors'], state.gradient_colors_ptr, UNIFORM_VEC4, color_count)
|
|
|
|
stops = gradient.get('stops', [i / max(1, color_count - 1) for i in range(color_count)])
|
|
stops = np.clip(stops[:color_count], 0.0, 1.0)
|
|
state.gradient_stops_ptr[0:color_count] = stops
|
|
rl.set_shader_value_v(state.shader, state.locations['gradientStops'], state.gradient_stops_ptr, UNIFORM_FLOAT, color_count)
|
|
rl.set_shader_value(state.shader, state.locations['gradientColorCount'], state.color_count_ptr, UNIFORM_INT)
|
|
else:
|
|
color = color or rl.WHITE
|
|
state.fill_color_ptr[0:4] = [color.r / 255.0, color.g / 255.0, color.b / 255.0, color.a / 255.0]
|
|
rl.set_shader_value(state.shader, state.locations['fillColor'], state.fill_color_ptr, UNIFORM_VEC4)
|
|
|
|
def draw_polygon(origin_rect: rl.Rectangle, points: np.ndarray, color=None, gradient=None):
|
|
"""
|
|
Draw a complex polygon using shader-based even-odd fill rule
|
|
|
|
Args:
|
|
rect: Rectangle defining the drawing area
|
|
points: numpy array of (x,y) points defining the polygon
|
|
color: Solid fill color (rl.Color)
|
|
gradient: Dict with gradient parameters:
|
|
{
|
|
'start': (x1, y1), # Start point (normalized 0-1)
|
|
'end': (x2, y2), # End point (normalized 0-1)
|
|
'colors': [rl.Color], # List of colors at stops
|
|
'stops': [float] # List of positions (0-1)
|
|
}
|
|
"""
|
|
if len(points) < 3:
|
|
return
|
|
|
|
state = ShaderState.get_instance()
|
|
if not state.initialized:
|
|
state.initialize()
|
|
|
|
# Find bounding box
|
|
min_xy = np.min(points, axis=0)
|
|
max_xy = np.max(points, axis=0)
|
|
clip_x = max(origin_rect.x, min_xy[0])
|
|
clip_y = max(origin_rect.y, min_xy[1])
|
|
clip_right = min(origin_rect.x + origin_rect.width, max_xy[0])
|
|
clip_bottom = min(origin_rect.y + origin_rect.height, max_xy[1])
|
|
|
|
# Check if polygon is completely off-screen
|
|
if clip_x >= clip_right or clip_y >= clip_bottom:
|
|
return
|
|
|
|
clipped_rect = rl.Rectangle(clip_x, clip_y, clip_right - clip_x, clip_bottom - clip_y)
|
|
|
|
# Transform points relative to the CLIPPED area
|
|
transformed_points = points - np.array([clip_x, clip_y])
|
|
|
|
# Set shader values
|
|
state.point_count_ptr[0] = len(transformed_points)
|
|
rl.set_shader_value(state.shader, state.locations['pointCount'], state.point_count_ptr, UNIFORM_INT)
|
|
|
|
state.resolution_ptr[0:2] = [clipped_rect.width, clipped_rect.height]
|
|
rl.set_shader_value(state.shader, state.locations['resolution'], state.resolution_ptr, UNIFORM_VEC2)
|
|
|
|
flat_points = np.ascontiguousarray(transformed_points.flatten().astype(np.float32))
|
|
points_ptr = rl.ffi.cast("float *", flat_points.ctypes.data)
|
|
rl.set_shader_value_v(state.shader, state.locations['points'], points_ptr, UNIFORM_VEC2, len(transformed_points))
|
|
|
|
_configure_shader_color(state, color, gradient, clipped_rect, origin_rect)
|
|
|
|
# Render
|
|
rl.begin_shader_mode(state.shader)
|
|
rl.draw_texture_pro(
|
|
state.white_texture,
|
|
rl.Rectangle(0, 0, 2, 2),
|
|
clipped_rect,
|
|
rl.Vector2(0, 0),
|
|
0.0,
|
|
rl.WHITE,
|
|
)
|
|
rl.end_shader_mode()
|
|
|
|
|
|
def cleanup_shader_resources():
|
|
state = ShaderState.get_instance()
|
|
state.cleanup()
|
|
|