Изучаю постепенно как работать с WebGL, читаю статьи, смотрю видео курсы и просто экспериментирую разное. Пока больше всяких нюансов с самим браузером, чем с WebGL.
Наткнулся на вот такую крутую штуку, где можно посмотреть состояние WebGL на разных этапах выполнения. Удобно в нод-блоках все отображается, хорошо помогает разобраться что и как работает.
Ссылка: https://webglfundamentals.org/webgl/lessons/resources/webgl-state-diagram.html
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
До меня дойти почему-то не могло, каким образом мы можем превратить одно пространство в другое. Речь о чем-то очень примитивном, но у меня не хватало фундаментальных знаний линейной алгебры.
В общем, у нас есть экран HD
1920x1080.WebGL в свою очередь рендерит все что помещается в диапазон от
-1 до 1 как по X, так и по Y. Все что за пределами - не попадает в зону видимости. Собственно это clip space (пространстве отсечения).Так вот, чтобы от рендерить экран - нам нужно его нормализовать. Это значит чтобы мы позиции точек по ширине в диапазоне от
0 до 1920 (видимый диапазон) - преобразовали в диапазон от -1 до 1 и тоже самое нужно сделать по высоте.Для этого необходимо наши позиции точек разделить на наш размер экрана
Screen (1080, 1920). Ну типа два вектора поделить между собой. Например у нас точка на нашем экран
P1, находится в таких координатах (200, 200)Поэтому для преобразования в диапазон от
-1 до 1 (нормализовать), нужно сделать P1 / Screen = (200 / 1080, 200 / 1920) = (~0,18, 0,10). Результат округлил, чтобы не писать много цифр, но и
webgl умеет тоже округлять качество значений с плавающей точкой через precision в шейдере.Так вот, мы получили нормализованные координаты, но прикол в том, что они находятся в диапазоне от
0 до 1. Получается что наши позиции точек будут нахдоится где-то сверху в правой части экрана. Почему? Ну потому что webgl рендерит область от -1 до 1 и центр у него находится ровно в (0, 0). А так как у нас нормализованные координаты находятся от 0 до 1, то это справа сверху.Здесь я не мог понять, потому что не хватало теоретических знаний, но до меня дошло, когда я начал рисовать это дело.
Так вот, помимо того, что у нас нормализованные координаты сверху справа, так они еще и в два раза меньше нашего экрана в конечном итоге.
Чтобы привести в нужный вид, нам необходимо сначала заскейлить это дело, ведь не гоже наши точки уменьшать, для этого нужно умножить на x2 и нормализованные координаты выйдут в диапазон от
0 до 2. Берем нашу нормализованную точку: P1 * 2 = (~0.18 * 2, ~0.10 * 2) = (~0,37, ~0,20).Ну вот, теперь нормально. Однако, если координаты точки были бы больше
1, то в данный момент webgl их бы не от рендерил за пределами видимой области (clip space).Теперь нам нужно сделать смещение / перемещение. Собственно так как у нас все находится по прежнему в правом верхнем углу экрана, то нам нужно это сместить ровно на
1 влево и на 1 вниз, потому что наш диапазон теперь от 0 до 2. Берем нашу нормализованную и отскейленную точку: P1 - 1 = (~0,37 - 1, ~0,20 - 1) = (~-0,63, ~-0,79).Отлично! Теперь наши точки находятся в пределах видимости от
-1 до 1 и они смещены в левый нижний угол экрана. Однако есть некое нечистое деяние, это не по стандарту, все должно быть относительно левого верхнего угла экрана. Почему? Ну погуглив и послушав больших дядек из этой области, понял что это маст хев, пока инфы по этому нет, но в вобщем и целом это сложилось исторически + согласованность, чтобы было во всех системах одинаково для упрощения.Как нам это провернуть? Ну тут все тоже просто, достаточно умножить наши координаты точек на инверсивный вектор. Например сейчас у нас в левом нижнем углу, значит нам нужно поменять только позицию по высоте, отразить их. Для этого берем вектор
(1, -1), где X будет 1, потому что нам не нужно менять позиции по X, но будет -1 по Y, чтобы отразить все координаты. P1 = (~-0,63, ~-0,79) * (1, -1) = (~-0.63 * 1, ~0.79 * -1) = (~-0,63, ~0.79)А вот собственно и весь шейдер данной операции:
#version 300 es;
precision mediump float;
attribute vec2 a_position;
uniform vec2 u_resolution;
void main() {
v_color = a_color;
vec2 n_position = a_position / u_resolution;
vec2 sn_position = n_position * 2.0;
vec2 clip_space = (sn_position - 1.0) * vec2(1, -1);
gl_Position = vec4(clip_space, 0, 1);
}
P.S. На скрине все в кучу, но это было очень эффективно :D
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Было интересно создавать объект для управления цветом. Раньше понимал как оно устроено, но сам никогда не делал этого.
Базово реализовал:
- Работа с
10-тичными значениями от 0 до 255- Перевод
10-тичных в нормализованные значения от 0 до 1- Работа с
hex (rrggbbaa)- Трансформация цвета в
Uint8Array и просто Array для удобства взаимодействия при рендеринге.Вот простенькая реализация для работы с
hex:public static fromHex(rrggbbaa: string): Color {
let hex = rrggbbaa.trim();
hex = hex.replace('#', '');
const r16 = hex.substring(0, 2);
const g16 = hex.substring(2, 4);
const b16 = hex.substring(4, 6);
const a16 = hex.substring(6, 8);
const r0255 = Number.parseInt(r16, 16);
const g0255 = Number.parseInt(g16, 16);
const b0255 = Number.parseInt(b16, 16);
let a0255 = Number.parseInt(a16, 16);
if(Number.isNaN(a0255)) {
//Set default alpha value
a0255 = 255;
}
return new Color(r0255, g0255, b0255, a0255);
}В планах сделать еще поддержку
HS[V/L] и обратно, хочу поэкспериментировать с этим делом, а будущем можно будет юзать для систем градиентов и каких-нибудь эффектов.HSV (Hue, Saturation, Value) удобен для работы с оттенками. Например, можно быстро сделать цвет ярче или темнее, просто изменяя V.HSL (Hue, Saturation, Lightness) часто используется в графическом дизайне, так как L (светлота) позволяет легко контролировать пастельные оттенки.Но это все теоретически, то что в данный момент читаю в книжке по фундаментальному
CG, так что для углубления попробую это реализовать.Еще начал читать главу про палитру "безопасных цветов" для Web, пока не знаю что именно там будет, но это что-то вроде фиксированной группы, которая одинаково отображается на всех мониторах.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
Неделя была довольно тяжелой. В основном занимался тем, что разбирался как создать удобный апи для той части движка, которая отвечает за ренедринг спрайтов. Успел переписать немного систему, стало лучше, но все равно есть моменты, которые мне не нравятся.
Что успел сделать:
- Поправил систему сборки, чтобы удобнее было тестировать.
- Реализовал систему Scene -> Node -> Component
- Написал базовую систему работы с ресурсами, что бы уже хоть что-то загружать и использовать.
- Создал компонент для рендеринга спрайтов. Реализовал возможность установки спрайта и выбора цвета (пока логику шейдеров для спрайтов обернул в материалы).
Что изучал:
- Ознакомился с Batch Rendering. Сейчас думаю, как интегрировать это в текущую систему.
- Просмотрел видео о 2D рендеринге, что оказалось очень полезным.
- Наткнулся на интересный блог Sean's GameDev, который рассказывает о геймдеве и затрагивает темы разработки движков.
Теперь планирую сосредоточиться на изучении работы с матрицами, чтобы преобразования объектов выполнялись через матрицы, а не через текущие методы. Сейчас, например, при изменении угла поворота объект может начать растягиваться. Поэтому в ближайшее время начну разрабатывать свою библиотеку для работы с матрицами и векторами, чтобы лучше понять основы и углубить знания.
P.S. Начал параллельно делать на этом движке Flappy Bird, иду так скажем целенаправленно. Начинаю что-то делать и вижу что не хватает этого или это работает не удобно и так далее. На скрине можно увидеть первый в мире спрайт, созданный через vecxy!
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
This media is not supported in your browser
VIEW IN TELEGRAM
Спрайт, над которым теперь можно делать преобразования — скейлить, вращать и менять позицию.
Для этого я немного углубился в основы работы с матрицами, изучил как их умножать, складывать, умножать на скаляр и прочее. Осознал, насколько важно правильно работать с матричными операциями, чтобы точнее контролировать трансформации объектов в пространстве.
Затем познакомился с аффинной матрицей. Она позволила мне описывать такие преобразования, как масштабирование, поворот и перемещение в 2D-пространстве. Однако, всё ещё есть некоторые шероховатости в понимании, как именно она работает в контексте 2D. Планирую на следующей неделе посвятить ещё несколько дней этому вопросу и углубиться в практику, чтобы лучше разобраться.
Еще очень интересный момент, что каждый раз, когда ты меняешь порядок операций умножения, результат может быть совершенно другим. Это потому, что умножение матриц некоммутативно, то есть порядок умножения имеет значение.
Примерно так: если ты сначала применяешь масштабирование, а потом поворот, ты получишь один результат. Но если ты поменяешь их местами и сначала применишь поворот, а потом масштабирование, результат будет абсолютно другим, потому что одно преобразование влияет на то, как будет восприниматься другое.
В этом туториале очень качественно эти преобразования объясняются и визуально демонстрируют в софте Nuke Studio. Сегодня себе его установил, чтобы по CG всякие штуки визуально закреплять.
Также поэкспериментировал с ортографической проекцией. Это позволило моему спрайту отображаться одинаково, независимо от его положения относительно камеры. Таким образом, он не искажается при удалении или приближении камеры. Камеру, правда, я еще не реализовал — пока что нужно больше практики в матрицах.
В общем, потрогал много всего, было много трудностей и косяков, но в итоге получилось то, что можно посмотреть в видео. Теперь буду более детально разбирать эти моменты и пробовать разные подходы, чтобы всё закрепить и действительно освоить.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
This media is not supported in your browser
VIEW IN TELEGRAM
Это первая птица-иконка, которая научилась двигаться в этом движке!
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥2👍1
This media is not supported in your browser
VIEW IN TELEGRAM
Ну это уже выглядит неплохо, почти готовый Fappy Bird! В целом если еще разработать систему коллизий, то этого будет более чем достаточно, чтобы полностью реализовать данную мини игру.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1🔥1
This media is not supported in your browser
VIEW IN TELEGRAM
🟢 Начал делать простенькую либу для создания дев-интерфейса. Для отображения какого-то поля, достаточно повестить декоратор и оно будет выведено в общий контейнер для редактирования. (все это на уровне DOM)
Примерно вот так:
@property(NUMBER, SLIDER, -10, 0)
private _ground_speed = -2;
В целом мне с этим работать удобно, так как очень быстро можно выводить параметры, однако сама реализация мне не нравится, пока что думаю как можно создавать кастомные компоненты и прикручивать к ним стили. На данный момент думаю делать через innerHTML + DOMParser, но тоже весьма сомнительно. В любом случае это пока только для тестов, потом начну думать над каким-то редакторским инструментарием.
🟢 Сделал анимацию для птички, чтобы у нее крылья шевелились. Пока что в тупую в компоненте самой птицы, думаю потом вынести это в отдельный компонент для анимаций. Очень простое решение, где каждые N фреймов будет происходить переключение спрайта. В будущем хочу подумать над интеграцией Spine или Dragon Bones (но тут похоже полная жопа будет, так как посмотрел как это делали Cocos или Phaser, то это сложная таска и буду над ней долго страдать)
🟢 Сделал движение земли с эффектом бесконечного смещения, тут просто перемещение двух экземпляров друг за другом и перестановкой слева на право.
🟢 Еще немного оптимизировал текущий рендеринг спрайта. Раньше каждый кадр рассчитывал данные для вершин (позиция + uv), потом понял, что достаточно это делать только при установки спрайта, так как дальше они не пересчитываются, им это и не нужно. Все остальное делает уже ортографическая матрица проекции и матрица преобразования.
Что планирую делать дальше:
1. Сделать объект камеры и в нем сделать view матрицу, чтобы уже относительно камеры строить проекцию.
2. Сделать компонент для анимаций в виде spritesheets
3. Подумать над логикой стилей и создание компонентов для дев-интерфейса + сделать группы и более удобный апи для этого дела.
4. Начать думать над системой коллизий
5. Сделать ограничения для мира птички, чтобы она не вылетала за границы и могла стукаться о землю.
6. Сделать трубы, которые двигаются справа на лево и о них можно врезаться.
В общем в планах по прежнему добить основную логику игры при помощи движка и попутно этому реализовывать все, чего не хватает. После этого буду смотреть что у меня в общей картине вышло, как с этим работается и что это позволяет делать. Буду делать выводы и анализ, после чего начну этап переработки получившегося, дабы устранить все тупые и не правильные вещи.
И еще, чуть не забыл упомянуть. Движок все таки в будущем (Когда освою базовые вещи с 2D) не будет сконцентрирован на 2D играх, так как я не фанат подобных игр, а если я играл во что-то, то это очень большая редкость, потому что меня тянет больше в 3D игры и это основное что я потребляю как игрок. А так как движок в первую очередь - это инструмент для разработки игр, то мне бы хотелось именно вести его в этом векторе.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Мне тут мемчик создали в честь др, сохраню для истории.
P.S. Если что, там я читал правила для the binding of isaac настольная игра
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥2👍1
⚙️Log #17
Всем привет! Давно не было новостей по движку. Всё потому, что у меня фокус последние месяцы прыгал куда угодно, только не в него. Работы и дел навалено выше крыши, сил не всегда хватает, да и желание часто сдувается. В итоге движок лежал без движения почти пять месяцев.
Почему так? Всё просто: есть основная работа. Мобилки и веб, казуальщина, головоломки и детский кринж. За это платят стабильно и по меркам моего региона — даже очень хорошо. Но по меркам геймдева в целом — это середнячок, а удовольствия от процесса минимум.
А хотелось бы заниматься настоящим геймдевом — делать проекты под ПК, игры мечты, в которые хочется играть самому. Но серьёзный проект требует огромного количества времени и сил, а когда параллельно тащишь основную работу, и ты, и проект страдаете. Чтобы уйти с головой в своё — нужны деньги хотя бы просто на жизнь. Пока такого ресурса нет, поэтому приходится балансировать.
Есть ещё и мой внутренний программист, который грызёт изнутри. За 7 лет я нафигачил кучу опыта: Unity, Cocos Creator, C#, JS/TS. Набежало больше 20.000 часов. Если верить правилу 10.000 часов, я «дважды мастер». Но по факту так себя не ощущаю.
Проблема в том, что я слишком верхнеуровневый. Опыт размазан: работаешь → выгораешь → снова работаешь. Да, могу собрать игру определённого уровня. Но есть потолок. Смотришь на AAA — и вообще не понимаешь, как там такие технологии реализованы. Для этого нужен другой фундамент, а у меня его тупо нет.
Вот поэтому и чешется уйти в низкоуровневое программирование. Зарыться в книги, как червяк в землю. Уехать куда-то в тишину, отрезать весь шум и начать строить нормальный фундамент знаний.
Хочу ещё копать не только в технику, но и в историю. Истории вроде Ады Лавлейс, первых компьютеров и всего этого старого безумия. Это вдохновляет, заряжает и помогает не забывать, ради чего всё начиналось.
Хочу подтянуть теорию. Потому что, когда у тебя только практика, это реально мешает в общении и в работе. Теоретическая база даёт уверенность, системность и другой уровень понимания.
🟢 Теперь к движку. За эти месяцы у меня родились новые идеи, и я начал перестраивать архитектуру.
План такой:
- будет два ядра — одно на C#, другое на JS/TS, чтобы собирать проекты и под веб, и под ПК;
- будет свой скриптовый язык ManuScript, пишу его на Си;
- на C# сделаю собственную библиотеку рендера, а на ней — фреймворк для UI, чтобы можно было собирать как игры, так и десктопные приложения;
- на этом же фреймворке будет написан редактор под Windows.
Короче, разработка движка потихоньку возвращается, но уже с новым взглядом и новой архитектурой.
Всем привет! Давно не было новостей по движку. Всё потому, что у меня фокус последние месяцы прыгал куда угодно, только не в него. Работы и дел навалено выше крыши, сил не всегда хватает, да и желание часто сдувается. В итоге движок лежал без движения почти пять месяцев.
Почему так? Всё просто: есть основная работа. Мобилки и веб, казуальщина, головоломки и детский кринж. За это платят стабильно и по меркам моего региона — даже очень хорошо. Но по меркам геймдева в целом — это середнячок, а удовольствия от процесса минимум.
А хотелось бы заниматься настоящим геймдевом — делать проекты под ПК, игры мечты, в которые хочется играть самому. Но серьёзный проект требует огромного количества времени и сил, а когда параллельно тащишь основную работу, и ты, и проект страдаете. Чтобы уйти с головой в своё — нужны деньги хотя бы просто на жизнь. Пока такого ресурса нет, поэтому приходится балансировать.
Есть ещё и мой внутренний программист, который грызёт изнутри. За 7 лет я нафигачил кучу опыта: Unity, Cocos Creator, C#, JS/TS. Набежало больше 20.000 часов. Если верить правилу 10.000 часов, я «дважды мастер». Но по факту так себя не ощущаю.
Проблема в том, что я слишком верхнеуровневый. Опыт размазан: работаешь → выгораешь → снова работаешь. Да, могу собрать игру определённого уровня. Но есть потолок. Смотришь на AAA — и вообще не понимаешь, как там такие технологии реализованы. Для этого нужен другой фундамент, а у меня его тупо нет.
Вот поэтому и чешется уйти в низкоуровневое программирование. Зарыться в книги, как червяк в землю. Уехать куда-то в тишину, отрезать весь шум и начать строить нормальный фундамент знаний.
Хочу ещё копать не только в технику, но и в историю. Истории вроде Ады Лавлейс, первых компьютеров и всего этого старого безумия. Это вдохновляет, заряжает и помогает не забывать, ради чего всё начиналось.
Хочу подтянуть теорию. Потому что, когда у тебя только практика, это реально мешает в общении и в работе. Теоретическая база даёт уверенность, системность и другой уровень понимания.
🟢 Теперь к движку. За эти месяцы у меня родились новые идеи, и я начал перестраивать архитектуру.
План такой:
- будет два ядра — одно на C#, другое на JS/TS, чтобы собирать проекты и под веб, и под ПК;
- будет свой скриптовый язык ManuScript, пишу его на Си;
- на C# сделаю собственную библиотеку рендера, а на ней — фреймворк для UI, чтобы можно было собирать как игры, так и десктопные приложения;
- на этом же фреймворке будет написан редактор под Windows.
Короче, разработка движка потихоньку возвращается, но уже с новым взглядом и новой архитектурой.
🔥1
⚙️Log #18
Прошлый пост был больше эмоциональным, а в этом — конкретика по новому архитектурному плану. Покопался в идеях, переосмыслил подходы и пришёл к более трезвым и реализуемым решениям.
🟢 Нативное ядро: C# и .NET 8.0 LTS. Это основная сила движка для десктопа. .NET 8 предоставляет отличную производительность и современные возможности. Логика будет разбита на модули в виде отдельных DLL-библиотек. Сам движок соберётся в одну динамическую библиотеку.
🟢 В нативной части рендеринг будет на OpenGL. Это проверенная технология с хорошим балансом сложности и возможностей. В далёкой перспективе — переход на Vulkan для более тонкого контроля и производительности. Для веб-версии, естественно, будет задействован WebGL|GPU.
🟢 Фокус сейчас на Windows. Как только нативное ядро станет стабильным, займусь вебом через WebAssembly (WASM) и того же WebGL.
🟢 От идеи с собственным скриптовым языком ManuScript я отказался. Не вижу смысла тратить силы на создание и поддержку того, что уже прекрасно реализовано другими. Это дикая дистилляция велосипеда.
- Основной язык логики — C#.
- Для гибкости будем подключать скриптовые языки, вроде Lua или JavaScript. Идеально для контента, модов или быстрого прототипирования.
- DSL остаются для конкретных задач, например, для описания UI, сделаю что-то похожее на XML для структуры и подобие CSS для стилей.
- Будет еще визуальный язык, что-то похожее на блюпринты в UE. (это не в приоритете и скорее всего будет являться инструментом для систем, где важна четкая визуальная связь)
Итого план стал более зрелым и сфокусированным. Вместо распыления на два ядра и свой язык — концентрация на сильном нативном ядре на C# с продуманной модульной архитектурой.
Прошлый пост был больше эмоциональным, а в этом — конкретика по новому архитектурному плану. Покопался в идеях, переосмыслил подходы и пришёл к более трезвым и реализуемым решениям.
🟢 Нативное ядро: C# и .NET 8.0 LTS. Это основная сила движка для десктопа. .NET 8 предоставляет отличную производительность и современные возможности. Логика будет разбита на модули в виде отдельных DLL-библиотек. Сам движок соберётся в одну динамическую библиотеку.
🟢 В нативной части рендеринг будет на OpenGL. Это проверенная технология с хорошим балансом сложности и возможностей. В далёкой перспективе — переход на Vulkan для более тонкого контроля и производительности. Для веб-версии, естественно, будет задействован WebGL|GPU.
🟢 Фокус сейчас на Windows. Как только нативное ядро станет стабильным, займусь вебом через WebAssembly (WASM) и того же WebGL.
🟢 От идеи с собственным скриптовым языком ManuScript я отказался. Не вижу смысла тратить силы на создание и поддержку того, что уже прекрасно реализовано другими. Это дикая дистилляция велосипеда.
- Основной язык логики — C#.
- Для гибкости будем подключать скриптовые языки, вроде Lua или JavaScript. Идеально для контента, модов или быстрого прототипирования.
- DSL остаются для конкретных задач, например, для описания UI, сделаю что-то похожее на XML для структуры и подобие CSS для стилей.
- Будет еще визуальный язык, что-то похожее на блюпринты в UE. (это не в приоритете и скорее всего будет являться инструментом для систем, где важна четкая визуальная связь)
Итого план стал более зрелым и сфокусированным. Вместо распыления на два ядра и свой язык — концентрация на сильном нативном ядре на C# с продуманной модульной архитектурой.
🔥5
⚙️Log #19
В данный момент занимаюсь модулем рендеринга. Пробую описать какой-то пайплайн...
🟢 Пока что пришел к такой системе: Render Pipeline -> Render Phase -> Render Phase Layer -> IRenderable
Для теста завел объект спрайта и попытался его отрендерить в этойм пайплайне.
🟢 Моя первичная задача:
Реализовать модуль рендеринга до какого-то рабочего состояния.
- Чтобы можно было например отрендерить N спрайтов с уникальными трансформами
- Чтобы все это было за N батчей (система батчинга)
- Чтобы N спрайтов использовали N текстур
- Чтобы были динамические спрайты, которые могут менять свои параметры в процессе. (для анимаций)
🟡 После этого рендеринг можно будет оставить на какое-то время и заняться системой UI. Начну писать каркас на основе XML + CSS.
В данный момент занимаюсь модулем рендеринга. Пробую описать какой-то пайплайн...
🟢 Пока что пришел к такой системе: Render Pipeline -> Render Phase -> Render Phase Layer -> IRenderable
Для теста завел объект спрайта и попытался его отрендерить в этойм пайплайне.
🟢 Моя первичная задача:
Реализовать модуль рендеринга до какого-то рабочего состояния.
- Чтобы можно было например отрендерить N спрайтов с уникальными трансформами
- Чтобы все это было за N батчей (система батчинга)
- Чтобы N спрайтов использовали N текстур
- Чтобы были динамические спрайты, которые могут менять свои параметры в процессе. (для анимаций)
🟡 После этого рендеринг можно будет оставить на какое-то время и заняться системой UI. Начну писать каркас на основе XML + CSS.
👍1
⚙️Log #20
Я давно знал о существовании RenderDoc - наблюдал за его использованием на стримах других разработчиков, но тогда он казался мне чем-то сложным и непонятным. До недавнего времени я откладывал глубокое погружение, но сейчас решился - и был приятно удивлен!
🟢 RenderDoc оказался настоящим мастхэвом в геймдеве и графической разработке. Однако мой путь к его освоению начался с неожиданных трудностей.
Я разрабатываю движок и систему рендеринга на .NET 8. При обычном запуске или дебаге все работало идеально, но при попытке захватить процесс через RenderDoc приложение молча падало на старте. Ни ошибок, ни логов - просто краш.
Перелопатил пол-интернета в поисках решения. Находил похожие проблемы, но их фиксы не подходили под мой случай. Начал даже думать, что .NET принципиально несовместим с RenderDoc.
Тут началось детективное расследование.
В отчаянии я решил проверить другие .NET движки - Stride и Prowl. К моему удивлению, их рантайм прекрасно работал с RenderDoc! Это окончательно сбило меня с толку - почему у них работает, а у меня нет?
Погрузившись в документацию OpenTK (обертка над OpenGL), я нашел ключ - нужно было включить дебаг-режим для OpenGL. И вот тут проявилась настоящая причина проблемы!
🟢 Оказалось, в коде освобождения памяти шейдеров после линковки я передавал неверный ID хендлера - вместо ID шейдерной программы указывал ID фрагментного шейдера. Из-за этого система пыталась отцепить шейдер от несуществующей программы.
Самое интересное: в обычном режиме эта ошибка молча игнорировалась, но RenderDoc сразу ее детектил и жестко падал - без каких-либо подсказок о причине.
🟡 Теперь планирую глубже изучить RenderDoc - буду читать документацию и экспериментировать с различными фичами инструмента.
Я давно знал о существовании RenderDoc - наблюдал за его использованием на стримах других разработчиков, но тогда он казался мне чем-то сложным и непонятным. До недавнего времени я откладывал глубокое погружение, но сейчас решился - и был приятно удивлен!
🟢 RenderDoc оказался настоящим мастхэвом в геймдеве и графической разработке. Однако мой путь к его освоению начался с неожиданных трудностей.
Я разрабатываю движок и систему рендеринга на .NET 8. При обычном запуске или дебаге все работало идеально, но при попытке захватить процесс через RenderDoc приложение молча падало на старте. Ни ошибок, ни логов - просто краш.
Перелопатил пол-интернета в поисках решения. Находил похожие проблемы, но их фиксы не подходили под мой случай. Начал даже думать, что .NET принципиально несовместим с RenderDoc.
Тут началось детективное расследование.
В отчаянии я решил проверить другие .NET движки - Stride и Prowl. К моему удивлению, их рантайм прекрасно работал с RenderDoc! Это окончательно сбило меня с толку - почему у них работает, а у меня нет?
Погрузившись в документацию OpenTK (обертка над OpenGL), я нашел ключ - нужно было включить дебаг-режим для OpenGL. И вот тут проявилась настоящая причина проблемы!
🟢 Оказалось, в коде освобождения памяти шейдеров после линковки я передавал неверный ID хендлера - вместо ID шейдерной программы указывал ID фрагментного шейдера. Из-за этого система пыталась отцепить шейдер от несуществующей программы.
Самое интересное: в обычном режиме эта ошибка молча игнорировалась, но RenderDoc сразу ее детектил и жестко падал - без каких-либо подсказок о причине.
🟡 Теперь планирую глубже изучить RenderDoc - буду читать документацию и экспериментировать с различными фичами инструмента.
🔥2
⚙️Log #21
Изучаю потихоньку OpenGL, вникаю в системы рендеринга.
Вот основной список документаций, которые штудирую:
1. OpenTK Learn
2. OpenGL Core
3. OpenGL GLSLang Spec
4. OpenGL Registry
🟢 Ещё глубже разобрался, как работают атрибуты и юниформы. Всё оказалось гораздо проще, чем я себе представлял. Если условно, то атрибут используется для каждой вершины в вершинном шейдере, а юниформа — глобально для всех шейдеров. Очень удобно их применять на разных этапах рендеринга. Например, атрибут может спокойно принимать целый буфер разных данных в одном массиве. Потом мы просто для атрибута конкретной вершины определяем смещение в байтах и настраиваем офсет. Таким образом, массив становится своеобразной инструкцией для всех вершин. Атрибут можно использовать, например, для настройки градиента для спрайта, указания позиций вершин, нормалей для освещения или тех же UV-координат для наложения текстур.
Если брать юниформу, то она очень полезна для глобальных значений. Например, можно задать цвет материала, настроить металличность, указать индекс текстуры, загруженной в GPU, или, скажем, передать матрицу преобразования.
🟢 Помимо этого, ещё почитал документацию о VBO — это вершинный буфер, который, собственно, принимает в себя данные для загрузки на GPU. Потом углубился в VAO — это некоторая настройка для атрибутов, через которую можно задавать правила чтения этого буфера. Важный момент: я понял, что привязка VAO происходит к текущему привязанному VBO. Это важно учитывать. Если VBO не был привязан или привязался другой, то VAO будет привязан к последнему или выбросит ошибку, потому что нет буфера, с которым он работает.
🟢 Потом познакомился с оптимизацией для работы с вершинами. Условно, мы можем для отрисовки спрайта использовать 6 вершин, что не очень эффективно, потому что в двух местах будет дубликат вершин из-за наложения двух треугольников. Либо же мы можем использовать только 4 вершины, что будет правильнее, и переиспользовать для создания треугольника уже известную вершину. Для этого нам поможет EBO — это буфер элементов, в который мы передаём индексы вершин, загруженные в VBO и определённые при помощи VAO. Здесь, кстати, тоже есть важный момент: EBO нужно привязывать к привязанной VAO, потому что он работает с ней, так как ему нужно понимать индексы вершин.
🟢 Ну и по мелочи: ещё поигрался с настройками OpenGL, например, с включением смешивания.
🟡 Дальше планирую по-прежнему работать над рендерингом. Нужно разобраться более подробно с фундаментом.
Изучаю потихоньку OpenGL, вникаю в системы рендеринга.
Вот основной список документаций, которые штудирую:
1. OpenTK Learn
2. OpenGL Core
3. OpenGL GLSLang Spec
4. OpenGL Registry
🟢 Ещё глубже разобрался, как работают атрибуты и юниформы. Всё оказалось гораздо проще, чем я себе представлял. Если условно, то атрибут используется для каждой вершины в вершинном шейдере, а юниформа — глобально для всех шейдеров. Очень удобно их применять на разных этапах рендеринга. Например, атрибут может спокойно принимать целый буфер разных данных в одном массиве. Потом мы просто для атрибута конкретной вершины определяем смещение в байтах и настраиваем офсет. Таким образом, массив становится своеобразной инструкцией для всех вершин. Атрибут можно использовать, например, для настройки градиента для спрайта, указания позиций вершин, нормалей для освещения или тех же UV-координат для наложения текстур.
Если брать юниформу, то она очень полезна для глобальных значений. Например, можно задать цвет материала, настроить металличность, указать индекс текстуры, загруженной в GPU, или, скажем, передать матрицу преобразования.
🟢 Помимо этого, ещё почитал документацию о VBO — это вершинный буфер, который, собственно, принимает в себя данные для загрузки на GPU. Потом углубился в VAO — это некоторая настройка для атрибутов, через которую можно задавать правила чтения этого буфера. Важный момент: я понял, что привязка VAO происходит к текущему привязанному VBO. Это важно учитывать. Если VBO не был привязан или привязался другой, то VAO будет привязан к последнему или выбросит ошибку, потому что нет буфера, с которым он работает.
🟢 Потом познакомился с оптимизацией для работы с вершинами. Условно, мы можем для отрисовки спрайта использовать 6 вершин, что не очень эффективно, потому что в двух местах будет дубликат вершин из-за наложения двух треугольников. Либо же мы можем использовать только 4 вершины, что будет правильнее, и переиспользовать для создания треугольника уже известную вершину. Для этого нам поможет EBO — это буфер элементов, в который мы передаём индексы вершин, загруженные в VBO и определённые при помощи VAO. Здесь, кстати, тоже есть важный момент: EBO нужно привязывать к привязанной VAO, потому что он работает с ней, так как ему нужно понимать индексы вершин.
🟢 Ну и по мелочи: ещё поигрался с настройками OpenGL, например, с включением смешивания.
🟡 Дальше планирую по-прежнему работать над рендерингом. Нужно разобраться более подробно с фундаментом.
⚙️Log #23
Давно не ощущал такой свободы в разработке...
За последние два года устал заниматься Unity-like мобильной дрочильней. Единственной отдушиной был веб-стек, который пришлось осваивать из-за очередного бума HTML5-игр. Но и там всё переросло в рутину, потому что задачи в той или иной степени повторяются.
А сейчас пишу свой движок, и здесь, чтобы что-то реализовать, приходится изучать тонны информации. Даже этот простенький парсер .obj-моделей заставляет изучить его структуру, почитать историю создания формата, параллельно залезть в документацию по работе с шейдингом. Потом увидел, что модель может быть не триангулирована, и нужно как-то обрабатывать этот кейс. Пошел смотреть, как такое делают на примере уже готовой библиотеки, хочу потом написать свой триангулятор просто потому, что хочу.
Вот оно, запах инди! Многое здесь создается только потому, что «Хочу!»
Давно не ощущал такой свободы в разработке...
За последние два года устал заниматься Unity-like мобильной дрочильней. Единственной отдушиной был веб-стек, который пришлось осваивать из-за очередного бума HTML5-игр. Но и там всё переросло в рутину, потому что задачи в той или иной степени повторяются.
А сейчас пишу свой движок, и здесь, чтобы что-то реализовать, приходится изучать тонны информации. Даже этот простенький парсер .obj-моделей заставляет изучить его структуру, почитать историю создания формата, параллельно залезть в документацию по работе с шейдингом. Потом увидел, что модель может быть не триангулирована, и нужно как-то обрабатывать этот кейс. Пошел смотреть, как такое делают на примере уже готовой библиотеки, хочу потом написать свой триангулятор просто потому, что хочу.
Вот оно, запах инди! Многое здесь создается только потому, что «Хочу!»
⚙️Log #24
Сезон страданий открылся...
Вчера потратил больше 6 часов на поиск решения проблемы. Изначально я написал логику втупую для работы с буферами, потом решил сделать для них обёртки, чтобы было удобно с этим работать и не писать кучу одинакового низкоуровневого кода.
Всё шло прекрасно до одного нюанса. Приложение начало крашиться, когда я делал два раза подряд привязку одного и того же буфера или вершинного массива. Раньше можно было привязывать сколько угодно раз, а теперь — нет. В рендере начиналась дичь в момент привязки указателей на данные атрибутов. Я хотел сделать лаяут, который автоматически сдвигал бы offset и определял stride для вершинных атрибутов.
По идее всё должно было работать чётко, но приложение падало с access violation. Решения на форумах я не нашёл, а официальная документация утверждала, что привязку можно делать сколько угодно раз. Так в чём же была проблема?
Оказалось, что я создавал буферы в автоматическом свойстве через лямбду:
Из-за того, что GL.GenBuffer() — unsafe-метод, при каждом обращении к свойству генерировался новый буфер, а старый терялся, создавая утечку памяти. Решение было простым — вынести создание в конструктор:
Теперь буфер создаётся только один раз и больше не вызывает падений при повторных привязках.
P.S. Дипсик и GPT-чат вообще не справляются с низкоуровневой разработкой. Да, они знают многое, и полезно с ними сверяться по каким-то нюансам, но они очень жестко теряются и постоянно выдают невалидную информацию. Она иногда даже не сходится с официальной документацией. А такие нюансы, как порядок привязки буферов — вообще жопа для них. Да, они в курсе, что должен быть какой-то порядок, но какой именно — им пофиг. В каждом ответе всегда рандомный порядок и бессмысленные биндинги и т.д.
Сезон страданий открылся...
Вчера потратил больше 6 часов на поиск решения проблемы. Изначально я написал логику втупую для работы с буферами, потом решил сделать для них обёртки, чтобы было удобно с этим работать и не писать кучу одинакового низкоуровневого кода.
Всё шло прекрасно до одного нюанса. Приложение начало крашиться, когда я делал два раза подряд привязку одного и того же буфера или вершинного массива. Раньше можно было привязывать сколько угодно раз, а теперь — нет. В рендере начиналась дичь в момент привязки указателей на данные атрибутов. Я хотел сделать лаяут, который автоматически сдвигал бы offset и определял stride для вершинных атрибутов.
По идее всё должно было работать чётко, но приложение падало с access violation. Решения на форумах я не нашёл, а официальная документация утверждала, что привязку можно делать сколько угодно раз. Так в чём же была проблема?
Оказалось, что я создавал буферы в автоматическом свойстве через лямбду:
public int Id => GL.GenBuffer();
Из-за того, что GL.GenBuffer() — unsafe-метод, при каждом обращении к свойству генерировался новый буфер, а старый терялся, создавая утечку памяти. Решение было простым — вынести создание в конструктор:
public int Id { get; private set; }
public ArrayBuffer()
{
Id = GL.GenBuffer();
}Теперь буфер создаётся только один раз и больше не вызывает падений при повторных привязках.
P.S. Дипсик и GPT-чат вообще не справляются с низкоуровневой разработкой. Да, они знают многое, и полезно с ними сверяться по каким-то нюансам, но они очень жестко теряются и постоянно выдают невалидную информацию. Она иногда даже не сходится с официальной документацией. А такие нюансы, как порядок привязки буферов — вообще жопа для них. Да, они в курсе, что должен быть какой-то порядок, но какой именно — им пофиг. В каждом ответе всегда рандомный порядок и бессмысленные биндинги и т.д.
⚙️Log #25
🟢 Начал разработку системы проектов для движка. Её основой станет лаунчер, в котором будет отображаться список всех проектов пользователя, их настройки и другие параметры.
При выборе проекта в лаунчере он будет открываться в изолированной рабочей директории. Эта папка будет содержать только файлы, относящиеся к данному проекту, а также служебные метафайлы для работы редактора и движка.
🟢 Решил реализовать эту систему на раннем этапе, чтобы заложить основу для её дальнейшего развития.
На текущий момент наметил принцип работы:
- Для каждого проекта автоматически генерируется решение (.sln) под .NET 8.0, а также его первый проект (.csproj).
- Параметры сборки для всех проектов определяются на уровне решения. В нём прописаны ссылки на собранные DLL-библиотеки движка.
- Пока что путь к этим DLL читается из переменных окружения компьютера. В будущем этот механизм может быть изменён — возможно, путь будет генерироваться динамически при запуске редактора. Решение через переменные окружения было выбрано как наиболее стандартное на первом этапе.
🟢 Движок можно будет установить прямо из лаунчера. Он загрузится в виде архива, распакуется в указанную пользователем папку и начнется процесс установки, после чего путь к нему будет автоматически добавлен в переменные окружения системы. Это позволит использовать движок как в процессе сборки проектов, так и напрямую из терминала.
🟢 Каждый проект будет содержать главный файл .project с информацией о нём: название, описание и т.д.
Также в этом файле указывается тип проекта:
- Игра — конечный продукт, разрабатываемый на движке. Собирается в исполняемый .exe-файл вместе с контентом. Может иметь зависимости в виде библиотек и пакетов.
- Библиотека — проект, состоящий в основном из кода, без контента. Представляет собой обычные .dll-файлы, которые могут использоваться в играх или других пакетах. Библиотеки могут быть одиночными или представлять собой группу взаимозависимых проектов.
- Пакет — reusable-компонент, который можно легко переиспользовать в разных проектах. Это аналог Packages в Unity, но адаптированный под нужды данного движка. Пакет может содержать как код, так и контент. Он может зависеть от других проектов, но только от библиотек или других пакетов.
🟡 В качестве теста я начал делать в этой системе игру Flappy Bird. Пока что всё работает отлично: процесс удобен и не требует дополнительных манипуляций с файлами проекта.
🟢 Начал разработку системы проектов для движка. Её основой станет лаунчер, в котором будет отображаться список всех проектов пользователя, их настройки и другие параметры.
При выборе проекта в лаунчере он будет открываться в изолированной рабочей директории. Эта папка будет содержать только файлы, относящиеся к данному проекту, а также служебные метафайлы для работы редактора и движка.
🟢 Решил реализовать эту систему на раннем этапе, чтобы заложить основу для её дальнейшего развития.
На текущий момент наметил принцип работы:
- Для каждого проекта автоматически генерируется решение (.sln) под .NET 8.0, а также его первый проект (.csproj).
- Параметры сборки для всех проектов определяются на уровне решения. В нём прописаны ссылки на собранные DLL-библиотеки движка.
- Пока что путь к этим DLL читается из переменных окружения компьютера. В будущем этот механизм может быть изменён — возможно, путь будет генерироваться динамически при запуске редактора. Решение через переменные окружения было выбрано как наиболее стандартное на первом этапе.
🟢 Движок можно будет установить прямо из лаунчера. Он загрузится в виде архива, распакуется в указанную пользователем папку и начнется процесс установки, после чего путь к нему будет автоматически добавлен в переменные окружения системы. Это позволит использовать движок как в процессе сборки проектов, так и напрямую из терминала.
🟢 Каждый проект будет содержать главный файл .project с информацией о нём: название, описание и т.д.
Также в этом файле указывается тип проекта:
- Игра — конечный продукт, разрабатываемый на движке. Собирается в исполняемый .exe-файл вместе с контентом. Может иметь зависимости в виде библиотек и пакетов.
- Библиотека — проект, состоящий в основном из кода, без контента. Представляет собой обычные .dll-файлы, которые могут использоваться в играх или других пакетах. Библиотеки могут быть одиночными или представлять собой группу взаимозависимых проектов.
- Пакет — reusable-компонент, который можно легко переиспользовать в разных проектах. Это аналог Packages в Unity, но адаптированный под нужды данного движка. Пакет может содержать как код, так и контент. Он может зависеть от других проектов, но только от библиотек или других пакетов.
🟡 В качестве теста я начал делать в этой системе игру Flappy Bird. Пока что всё работает отлично: процесс удобен и не требует дополнительных манипуляций с файлами проекта.
👍1
⚙️Log #26
Давно не было новостей по разработке, но потихоньку, помаленьку, по строчке — работал!
🟢 Продумал больше архитектурных штук, определился с техническим стеком, нащупал направления, которые моему внутреннему феншую в радость. Например, закончил с экспериментами структуры пользовательского проекта, который будет создаваться по шаблону через CLI движка. В планах сделать виртуальную среду для V8 в связке с TypeScript. Скажете зачем? А я захотел интегрировать JavaScript движок для скриптинга. Потому что интерпретируемость даст условия для быстрых итераций без пересборки, а еще Hot Reload в такой среде куда лучше. Не могу сказать, что у шарпа он плохой, но интерпретатор может творить магию, особенно в таком нетипизированном языке (благо правится тайпскриптом). Скриптинг теперь только в JS? Нет, будет основной нативный слой на C# и дополнительный JS. Можно работать в связке: часть, требующая быстрых правок — на JS, а производительности — на C#. Это пока определилось на этапе экспериментов, может это все бредни сумасшедшего, а может прекрасное решение.
🟢 Мне очень не нравится концепция движка, где основа — редактор. Нашел компромисс: будет встроенный редактор в рантайм игры. Движок будет делиться на слои выполнения, где в дебаг режиме основной слой идет со встроенными инструментами. Вкратце: движок — это библиотека, игра — плагин. Ни у движка, ни у игры не будет исполняемой стороны, это просто либы, однако, когда CLI соберет исполняемый файл, он просто подгрузит их по слоям. Управление будет через CLI, а редакторские инструменты станут частью рантайма.
🟢 Еще не захотелось юзать OpenTK, некомфортно делегировать нативные вещи. Хочу писать сам, понимать как это работает под капотом и иметь контроль. Потому в проекте появились нативные модули на C/C++, которые экспортируют API для C# через LibraryImport. Сейчас делаю Window, чтобы иметь полный контроль над процедурой окна. Как закончу, буду писать обработку инпута. Ну хочется мне нативного кода, хочется плюсов, хочется возни с unsafe шарпом.
🟢 Перекатил проект на .NET 10, будем двигать в ногу с платформой. Новые фичи — это имба, особенно когда работал 7 лет на Unity с убогим шарпом. А еще наконец-то выкатили полную поддержку .slnx и теперь перешел на него, красиво оформлено.
🟢 Настроил сборку в одну папку, теперь чище выглядят артефакты. Потом изучу вопрос паблишинга, может есть инструменты для пайплайна, если нет — напишу скрипты на JS или питоне (баш или батники не хочется юзать). Для CLI сделал парсер аргументов и абстрагировал это командами, получилось удобнее, чем у всратых решений из гитхаба. Сделал тестовую сборку проекта в двух конфигурациях — это следующий большой этап разработки. Пока руки ленятся туда лезть, потому и начал писать нативные модули, чтобы заниматься чем-то веселым.
В общем как-то так. Заряжен как никогда, куча идей, осталось настроиться на завершение этапа окружения. Считаю, что важно сейчас не рендеринг, а именно то, как будет строиться рабочий процесс и масштабируемость.
Давно не было новостей по разработке, но потихоньку, помаленьку, по строчке — работал!
🟢 Продумал больше архитектурных штук, определился с техническим стеком, нащупал направления, которые моему внутреннему феншую в радость. Например, закончил с экспериментами структуры пользовательского проекта, который будет создаваться по шаблону через CLI движка. В планах сделать виртуальную среду для V8 в связке с TypeScript. Скажете зачем? А я захотел интегрировать JavaScript движок для скриптинга. Потому что интерпретируемость даст условия для быстрых итераций без пересборки, а еще Hot Reload в такой среде куда лучше. Не могу сказать, что у шарпа он плохой, но интерпретатор может творить магию, особенно в таком нетипизированном языке (благо правится тайпскриптом). Скриптинг теперь только в JS? Нет, будет основной нативный слой на C# и дополнительный JS. Можно работать в связке: часть, требующая быстрых правок — на JS, а производительности — на C#. Это пока определилось на этапе экспериментов, может это все бредни сумасшедшего, а может прекрасное решение.
🟢 Мне очень не нравится концепция движка, где основа — редактор. Нашел компромисс: будет встроенный редактор в рантайм игры. Движок будет делиться на слои выполнения, где в дебаг режиме основной слой идет со встроенными инструментами. Вкратце: движок — это библиотека, игра — плагин. Ни у движка, ни у игры не будет исполняемой стороны, это просто либы, однако, когда CLI соберет исполняемый файл, он просто подгрузит их по слоям. Управление будет через CLI, а редакторские инструменты станут частью рантайма.
🟢 Еще не захотелось юзать OpenTK, некомфортно делегировать нативные вещи. Хочу писать сам, понимать как это работает под капотом и иметь контроль. Потому в проекте появились нативные модули на C/C++, которые экспортируют API для C# через LibraryImport. Сейчас делаю Window, чтобы иметь полный контроль над процедурой окна. Как закончу, буду писать обработку инпута. Ну хочется мне нативного кода, хочется плюсов, хочется возни с unsafe шарпом.
🟢 Перекатил проект на .NET 10, будем двигать в ногу с платформой. Новые фичи — это имба, особенно когда работал 7 лет на Unity с убогим шарпом. А еще наконец-то выкатили полную поддержку .slnx и теперь перешел на него, красиво оформлено.
🟢 Настроил сборку в одну папку, теперь чище выглядят артефакты. Потом изучу вопрос паблишинга, может есть инструменты для пайплайна, если нет — напишу скрипты на JS или питоне (баш или батники не хочется юзать). Для CLI сделал парсер аргументов и абстрагировал это командами, получилось удобнее, чем у всратых решений из гитхаба. Сделал тестовую сборку проекта в двух конфигурациях — это следующий большой этап разработки. Пока руки ленятся туда лезть, потому и начал писать нативные модули, чтобы заниматься чем-то веселым.
В общем как-то так. Заряжен как никогда, куча идей, осталось настроиться на завершение этапа окружения. Считаю, что важно сейчас не рендеринг, а именно то, как будет строиться рабочий процесс и масштабируемость.
👍3
⚙️Log #27
Столько разных идей и вдохновений на архитектуру движка!
Расскажу о них чуть позже, нужно все мысли привести в порядок, что-то попробовать реализовать в черновом состоянии, а что-то допилить до MVP.
Держите пока что первую версию внешнего вида сайта с документацией к движку. Решил отвлечься и заняться монкей работой, поэкспериментировал с .md файликами для описания доков. Может потом сделаю еще и отдельную страницу для блога и дев отчетов и переедем туда, так как телега жестко ограничивает в этом деле.
В этом году если все пойдет по плану, то дам в общее пользование, пощупать двигло, поклепать флепи бердов и дудл джампов :D
Столько разных идей и вдохновений на архитектуру движка!
Расскажу о них чуть позже, нужно все мысли привести в порядок, что-то попробовать реализовать в черновом состоянии, а что-то допилить до MVP.
Держите пока что первую версию внешнего вида сайта с документацией к движку. Решил отвлечься и заняться монкей работой, поэкспериментировал с .md файликами для описания доков. Может потом сделаю еще и отдельную страницу для блога и дев отчетов и переедем туда, так как телега жестко ограничивает в этом деле.
В этом году если все пойдет по плану, то дам в общее пользование, пощупать двигло, поклепать флепи бердов и дудл джампов :D
🔥2