mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
144 lines
6.1 KiB
Python
144 lines
6.1 KiB
Python
"""Antialiased torque arc, painted in one bounded pass without a polygon cache."""
|
|
|
|
import math
|
|
|
|
import pyray as rl
|
|
|
|
from openpilot.system.ui.lib.application import GL_VERSION
|
|
from openpilot.starpilot.ui.onroad_torque_geometry import MAX_HEIGHT, MAX_OFFSET, MIN_HEIGHT, MIN_OFFSET, RADIUS, TORQUE_ANGLE_SPAN
|
|
|
|
|
|
VERTEX_SHADER = GL_VERSION + """
|
|
in vec3 vertexPosition;
|
|
in vec2 vertexTexCoord;
|
|
uniform mat4 mvp;
|
|
out vec2 position;
|
|
void main() {
|
|
position = vertexTexCoord;
|
|
gl_Position = mvp * vec4(vertexPosition, 1.0);
|
|
}
|
|
"""
|
|
|
|
FRAGMENT_SHADER = GL_VERSION + """
|
|
in vec2 position;
|
|
out vec4 finalColor;
|
|
uniform vec4 geometry; // center, middle radius, half thickness
|
|
uniform vec4 backgroundNormals;
|
|
uniform vec4 indicatorNormals;
|
|
uniform vec4 style; // cap radius, background alpha, indicator alpha, dot alpha
|
|
uniform vec4 dotGeometry; // center, radius, reserved
|
|
uniform vec4 gradient; // signed inverse reach, center green, green change, blue
|
|
|
|
float capDistance(vec2 p, vec2 normal) {
|
|
vec2 tangent = vec2(-normal.y, normal.x);
|
|
float radial = abs(dot(p, normal) - geometry.z);
|
|
return length(vec2(dot(p, tangent), max(radial - (geometry.w - style.x), 0.0))) - style.x;
|
|
}
|
|
float barDistance(vec2 p, vec4 normals, float ringDistance) {
|
|
vec2 tangent0 = vec2(-normals.y, normals.x);
|
|
vec2 tangent1 = vec2(-normals.w, normals.z);
|
|
float sector = max(ringDistance, max(-dot(p, tangent0), dot(p, tangent1)));
|
|
return min(sector, min(capDistance(p, normals.xy), capDistance(p, normals.zw)));
|
|
}
|
|
float coverage(float distance) {
|
|
return clamp(0.5 - distance / max(fwidth(distance), 0.0001), 0.0, 1.0);
|
|
}
|
|
void main() {
|
|
vec2 p = position - geometry.xy;
|
|
float ringDistance = abs(length(p) - geometry.z) - geometry.w;
|
|
float b = coverage(barDistance(p, backgroundNormals, ringDistance)) * style.y;
|
|
float i = coverage(barDistance(p, indicatorNormals, ringDistance)) * style.z;
|
|
float d = coverage(length(position - dotGeometry.xy) - dotGeometry.z) * style.w;
|
|
float bi = b + i * (1.0 - b);
|
|
float alpha = d + bi * (1.0 - d);
|
|
float t = clamp(p.x * gradient.x, 0.0, 1.0);
|
|
vec3 indicatorColor = vec3(1.0, gradient.y + gradient.z * t, gradient.w);
|
|
vec3 premultiplied = (vec3(b * (1.0 - i)) + indicatorColor * i) * (1.0 - d) + vec3((182.0 / 255.0) * d);
|
|
finalColor = vec4(premultiplied / max(alpha, 0.00001), alpha);
|
|
}
|
|
"""
|
|
|
|
|
|
class TorqueBarPainter:
|
|
def __init__(self):
|
|
self._shader = None
|
|
self._uniforms = {}
|
|
self._previous = {}
|
|
|
|
def prepare(self) -> None:
|
|
if self._shader is not None:
|
|
return
|
|
shader = rl.load_shader_from_memory(VERTEX_SHADER, FRAGMENT_SHADER)
|
|
names = ('geometry', 'backgroundNormals', 'indicatorNormals', 'style', 'dotGeometry', 'gradient')
|
|
locations = {name: rl.get_shader_location(shader, name) for name in names}
|
|
if not shader.id or any(location < 0 for location in locations.values()):
|
|
if shader.id and shader.id != rl.rl_get_shader_id_default():
|
|
rl.unload_shader(shader)
|
|
raise RuntimeError('Torque bar shader unavailable')
|
|
self._uniforms = {name: (location, rl.ffi.new('float[4]')) for name, location in locations.items()}
|
|
self._previous.clear()
|
|
self._shader = shader
|
|
|
|
def render(self, rect: rl.Rectangle, torque: float, alpha: float, screen_width: float, cap_radius: float,
|
|
offset: tuple[float, float] = (0, 0)) -> None:
|
|
if alpha <= 0:
|
|
return
|
|
self.prepare()
|
|
scale = rect.height / 240.0 * (rect.width / screen_width)
|
|
strength = max(0.0, min(1.0, (abs(torque) - 0.5) * 2))
|
|
torque_offset = MIN_OFFSET * scale + (MAX_OFFSET * scale - MIN_OFFSET * scale) * strength
|
|
thickness = MIN_HEIGHT * scale + (MAX_HEIGHT * scale - MIN_HEIGHT * scale) * strength
|
|
radius = RADIUS * scale
|
|
middle_radius = radius + thickness / 2
|
|
cap = min(cap_radius, thickness / 2)
|
|
cx = rect.x + rect.width / 2 + 8 + offset[0]
|
|
cy = rect.y + rect.height + radius - torque_offset + offset[1]
|
|
half_span = math.radians(alpha * TORQUE_ANGLE_SPAN / 2)
|
|
sine, cosine = math.sin(half_span), math.cos(half_span)
|
|
end_sine, end_cosine = math.sin(half_span * torque), math.cos(half_span * torque)
|
|
start, end = ((end_sine, -end_cosine), (0, -1)) if torque < 0 else ((0, -1), (end_sine, -end_cosine))
|
|
dot_y = rect.y + rect.height - torque_offset - thickness / 2 + offset[1]
|
|
indicator_alpha = int(255 * 0.9 * alpha) / 255
|
|
warmth = max(0.0, min(1.0, (abs(torque) - 0.75) * 4))
|
|
extent = (middle_radius + thickness / 2 - cap) * sine + cap
|
|
reach = max(0.65 * extent, 0.0001)
|
|
values = {
|
|
'geometry': (cx, cy, middle_radius, thickness / 2),
|
|
'backgroundNormals': (-sine, -cosine, sine, -cosine),
|
|
'indicatorNormals': (*start, *end),
|
|
'style': (cap, int(255 * (0.25 + 0.25 * strength) * alpha) / 255,
|
|
indicator_alpha, indicator_alpha if abs(torque) < 0.5 else 0),
|
|
'dotGeometry': (int(cx), int(dot_y), 5 * scale, 0),
|
|
'gradient': (math.copysign(1 / reach, torque), 1 - warmth + warmth * (115 / 255),
|
|
warmth * (85 / 255), 1 - warmth),
|
|
}
|
|
# Cover subpixel edge samples without allocating an offscreen surface, including desktop downscaling.
|
|
extent += 4
|
|
top = rect.y + rect.height - torque_offset - thickness + offset[1] - 4
|
|
bottom = cy - (radius + cap) * cosine + cap + 4
|
|
rl.begin_shader_mode(self._shader)
|
|
try:
|
|
for name, value in values.items():
|
|
if self._previous.get(name) != value:
|
|
location, pointer = self._uniforms[name]
|
|
pointer[0:4] = value
|
|
rl.set_shader_value(self._shader, location, pointer, rl.ShaderUniformDataType.SHADER_UNIFORM_VEC4)
|
|
self._previous[name] = value
|
|
# rlgl may transform positions on the CPU; UVs keep distance calculations in logical pixels.
|
|
left, right = cx - extent, cx + extent
|
|
rl.rl_set_texture(0)
|
|
rl.rl_begin(rl.RL_QUADS)
|
|
for x, y in ((left, top), (left, bottom), (right, bottom), (right, top)):
|
|
rl.rl_tex_coord2f(x, y)
|
|
rl.rl_vertex2f(x, y)
|
|
rl.rl_end()
|
|
finally:
|
|
rl.end_shader_mode()
|
|
|
|
def close(self) -> None:
|
|
if self._shader is not None and rl.is_window_ready():
|
|
rl.unload_shader(self._shader)
|
|
self._shader = None
|
|
self._uniforms.clear()
|
|
self._previous.clear()
|