Обычные методы рисования дают готовые фигуры: прямоугольник, текст, картинку. Шейдер это программа, которая считает цвет каждой точки сама, прямо на видеокарте. Ей можно написать плавную волну, свечение, переливающийся градиент, искажение картинки мира. Такое на обычных фигурах не собрать.
Шейдеры пишут на языке GLSL. В скрипте это просто текст в кавычках, компилирует его клиент при загрузке скрипта.
Текст шейдера отдают в Shader(имя, текст). Делать это надо один раз, в теле скрипта, а не внутри рисования. Дальше готовый объект рисуют прямоугольником нужного размера.
mod = Module("MyShader", "Visuals")
wave = Shader("wave", """
void main() {
float v = sin(FragCoord.x * 12.0 + Time * 2.0) * 0.5 + 0.5;
fragColor = vec4(0.2, 0.6 * v + 0.4, 1.0, 1.0);
}
""")
@events.render_2d
def draw(event):
if not mod.isEnabled():
return
wave.rect(event.getContext(), 20, 20, 200, 60)Внутри у шейдера обязательно есть функция void main(), а результат он кладёт в fragColor: четыре числа от 0 до 1, это красный, зелёный, синий и прозрачность.
Клиент сам заполняет эти имена перед каждой отрисовкой.
| Имя | Тип | Что это |
|---|---|---|
FragCoord | vec2 | Точка внутри фигуры от 0 до 1. Слева сверху (0, 0), справа снизу (1, 1) |
Size | vec2 | Ширина и высота фигуры в тех же единицах, что у обычного рисования |
Time | float | Секунды с запуска игры. По нему делают любую анимацию |
Color | vec4 | Цвет, который скрипт передал методом color() |
Resolution | vec2 | Размер экрана |
MousePos | vec2 | Положение курсора |
GuiScale | float | Масштаб интерфейса из настроек игры |
Sampler0..Sampler3 | sampler2D | Картинки, которые скрипт передал методом texture() |
CamPos | vec3 | Где стоит камера. Только для эффектов по миру |
InvViewProj | mat4 | Матрица для перевода точки экрана в точку мира. Только для эффектов по миру |
Ещё доступны две функции клиента: roundedBoxSDF(точка, половина размера, радиусы) считает расстояние до скруглённого прямоугольника, а sdfAA(расстояние, мягкость) даёт ширину полосы сглаживания. Вместе они делают ровные скруглённые углы.
panel = Shader("panel", """
void main() {
vec2 center = Size * 0.5;
float dist = roundedBoxSDF(center - FragCoord * Size, center - 1.0, vec4(8.0));
float shape = 1.0 - smoothstep(1.0 - sdfAA(dist, 1.0), 1.0, dist);
vec3 tint = mix(vec3(0.1, 0.4, 1.0), vec3(1.0, 0.3, 0.7), FragCoord.x);
fragColor = vec4(tint, shape);
}
""")Всё, что скрипт хочет передать шейдеру, объявляют словом uniform и заполняют методом set. Значение живёт до следующей смены, каждый кадр заново ставить не обязательно.
glow = Shader("glow", """
uniform float Power;
uniform vec2 Focus;
void main() {
float d = distance(FragCoord, Focus);
fragColor = vec4(Color.rgb, Color.a * exp(-d * Power));
}
""")
speed = Slider(mod, "Сила").min(1).max(20).step(0.5).set(6)
@events.render_2d
def draw(event):
glow.set("Power", speed.get())
glow.set("Focus", 0.5, 0.5)
glow.color(Color(120, 200, 255, 220))
glow.rect(event.getContext(), 40, 40, 160, 80)| Метод | Что делает |
|---|---|
set(имя, a[, b, c, d]) | Число, пара, тройка или четвёрка чисел. Это float, vec2, vec3, vec4 |
set_int(имя, значение) | Целое число (int в шейдере) |
color(цвет[, имя]) | Цвет в vec4. По умолчанию попадает во встроенную переменную Color |
texture(номер, картинка) | Картинка из assets.image в Sampler0..Sampler3 |
matrix(имя, матрица) | Матрица 4 на 4 |
Один и тот же шейдер умеет рисоваться тремя способами.
| Метод | Что делает |
|---|---|
rect(ctx, x, y, ширина, высота) | Прямоугольник на экране. ctx это кисточка из render_2d |
fullscreen(event) | Эффект по всему кадру мира, из события render_3d |
quad3d(event, точка, ширина[, высота, mode, additive, depth]) | Квадрат в мире: у ног игрока, вокруг сущности, в воздухе |
В элементе HUD есть короткая запись: у кисточки элемента тоже есть shader, и координаты в нём считаются от угла элемента, а размер по умолчанию равен размеру элемента.
el = Hud("Glow", width=150, height=44, x=8, y=180)
el.visible_when(lambda: mod.isEnabled())
@el.render
def draw(d):
panel.color(Color(90, 180, 255).mulAlpha(d.alpha))
d.shader(panel)
d.text("свой шейдер", 8, 8, size=7, weight="bold")fullscreen берёт уже нарисованный кадр игры и пропускает его через шейдер. Сам кадр лежит в Sampler0, глубина (насколько далеко от камеры каждая точка) в Sampler1. Так делают виньетку, тепловую дымку, искажение при низком здоровье, цветокоррекцию.
post = Shader("post", """
uniform float Warp;
void main() {
vec2 uv = FragCoord;
vec2 fromCenter = uv - 0.5;
float edge = dot(fromCenter, fromCenter);
// чем дальше от центра экрана, тем сильнее расходятся цветовые каналы
float spread = edge * 0.006 * Warp;
vec3 color = vec3(
texture(Sampler0, uv + fromCenter * spread).r,
texture(Sampler0, uv).g,
texture(Sampler0, uv - fromCenter * spread).b
);
fragColor = vec4(color * (1.0 - edge * 0.45), 1.0);
}
""")
@events.render_3d
def world(event):
if not mod.isEnabled():
return
post.set("Warp", 1.0)
post.fullscreen(event)quad3d ставит прямоугольник прямо в мире: под ногами цели, вокруг сундука, в точке взрыва. Режим говорит, как он повёрнут.
| Режим | Как стоит |
|---|---|
billboard | Всегда лицом к камере. Подходит для свечения и вспышек |
ground | Плашмя на земле. Кольца под целью, круги отметок |
wall | Стоит вертикально и поворачивается за камерой только вбок |
ring = Shader("ring", """
void main() {
vec2 p = FragCoord * 2.0 - 1.0;
float r = length(p);
float line = smoothstep(0.06, 0.0, abs(r - 0.72));
fragColor = vec4(Color.rgb, line * Color.a);
}
""")
@events.render_3d
def world(event):
victim = target.current()
if victim is None:
return
pos = victim.getPos()
ring.color(Color(120, 200, 255))
ring.quad3d(event, (pos.x, pos.y + 0.02, pos.z), 2.4,
mode="ground", additive=True, depth=True)Аргумент additive включает сложение цветов: то, что нарисовано, не закрывает картинку под собой, а подсвечивает её. Так рисуют огонь, магию, свечение.
depth решает, видно ли квадрат сквозь стены. По умолчанию False: он рисуется поверх всего, как метки ESP. depth=True включает проверку глубины, и квадрат честно прячется за блоками и существами, как обычная геометрия игры. Кольцу под целью обычно нужен именно он, иначе кольцо просвечивает сквозь холм.
Прямоугольник и квадрат это готовые формы. Если нужна своя (куб, сфера, спираль, модель), вершины считает сам скрипт и отдаёт их в mesh3d (в мире) или mesh2d (на экране).
Вершины идут плоским списком чисел. По 5 чисел на вершину: x, y, z, u, v, где u, v это те самые FragCoord внутри шейдера. С colors=True чисел девять: после u, v идут r, g, b, a от 0 до 255, и в шейдере они приходят как VertexColor (уже поделённые на 255 и плавно размазанные между вершинами).
# грань куба это два треугольника; ниже только передняя, остальные строят так же
verts = [
# x y z u v r g b a
-0.5, -0.5, 0.5, 0, 0, 255, 90, 90, 255,
0.5, -0.5, 0.5, 1, 0, 255, 90, 90, 255,
0.5, 0.5, 0.5, 1, 1, 255, 90, 90, 255,
-0.5, -0.5, 0.5, 0, 0, 255, 90, 90, 255,
0.5, 0.5, 0.5, 1, 1, 255, 90, 90, 255,
-0.5, 0.5, 0.5, 0, 1, 255, 90, 90, 255,
]
@events.render_3d
def draw(event):
me = world.self()
if me is None:
return
box.mesh3d(event, verts, origin=(me.getX(), me.getY() + 2.6, me.getZ()),
mode="triangles", colors=True, depth=True, cull=True)| Аргумент | Что делает |
|---|---|
origin | Точка мира, от которой считаются координаты вершин. По умолчанию (0, 0, 0), то есть вершины сразу мировые |
mode | Из чего собирать: triangles, triangle_strip, triangle_fan, lines, line_strip, line_loop, points |
colors | True если в вершине есть цвет (9 чисел вместо 5) |
depth | Проверка глубины: прячется ли фигура за миром. Для mesh3d по умолчанию включена |
cull | Отсекать задние грани. Для замкнутых тел вроде куба ставь True |
additive | Складывать цвета вместо перекрытия |
На экране всё то же самое, только вершина короче: x, y, u, v (или восемь чисел с цветом), координаты обычные экранные.
@events.render_2d
def draw(event):
tri.mesh2d(event.getContext(), [
20, 100, 0, 0,
120, 100, 1, 0,
70, 20, 0.5, 1,
], mode="triangles")Вершины можно давать и списком кортежей ([(x, y, u, v), ...]) для читаемости. Тысячи вершин лучше собирать в один плоский список: он уходит в игру одним куском, и меш на пять тысяч вершин стоит столько же, сколько одна панель.
Если в шейдере опечатка, скрипт не загрузится, а в чат придёт сообщение с номером строки. Номер считается от твоего текста, заголовок клиента на нумерацию не влияет.
shader.create зовут один раз при загрузке скрипта. Один и тот же текст повторно не компилируется, но привычка звать его в рисовании рано или поздно съест память видеокарты.dispose().shader.load("имя", "effects/glow.frag").