Pull to refresh
1
0
Эдуард @EduardGuschin

Junior C#

Send message

У меня получилось сделать такой же эффект волн на SFML. Получается он, если масштабировать спрайт на нецелое число.

Резюмируя: проблема не в SDL и не в коде. Просто так устроен механизм отрисовки. Если цель сделать красивый pixelart с плавным движением на современных разрешениях, лучше использовать спрайты больших размеров в стилистике pixelart и включать сглаживание, а не каноничные pixelart спрайты, и не в коем случае не использовать масштабирование спрайтов на нецелое число, если есть хоть-какая ни будь анимация.

Большое вам спасибо за уделенное время, вы очень помогли.

Артефакты есть, как не крути. Вопрос, как быть, если спрайты все таки нужно масштабировать? Скиллировать исходные спрайты? Выглядит как костыль.

На самом деле стало плавнее, но для пиксельарта сглаживание не вариант.

На всякий, скину код в 200 строк. Ничего C#-по зависимого, просто SDL2

Код
using System;
using static SDL2.SDL;
using static SDL2.SDL_image;

namespace SDLTest
{
    internal static class Program
    {
        private static IntPtr _windowContext, _renderContext;
        private static bool _mainLoop;

        private static Texture _treeTexture;
        private static Sprite _treeSprite;

        private static void Main(string[] args)
        {

            _mainLoop = true;

            CreateRenderer();

            RenderLoop();
        }
        
        private static void CreateRenderer()
        {
            SDL_Init(SDL_INIT_EVERYTHING);
            IMG_Init(IMG_InitFlags.IMG_INIT_PNG);
            
            
            
            _windowContext = SDL_CreateWindow("test", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 
                1024, 768, SDL_WindowFlags.SDL_WINDOW_SHOWN);
            
            const SDL_RendererFlags renderFlags = SDL_RendererFlags.SDL_RENDERER_ACCELERATED | 
                                                  SDL_RendererFlags.SDL_RENDERER_TARGETTEXTURE | 
                                                  SDL_RendererFlags.SDL_RENDERER_PRESENTVSYNC;
            
            _renderContext = SDL_CreateRenderer(_windowContext, -1, renderFlags);
            
            SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "2");
        }
        
        private static void RenderLoop()
        {
            LoadTexture();
            
            while (_mainLoop)
            {
                SDL_RenderClear(_renderContext);

                PoolEvents();

                RenderSprite();

                SDL_RenderPresent(_renderContext);
            }
        }

        private static void LoadTexture()
        {
            _treeTexture = new Texture("tree.png", _renderContext);
            _treeSprite = new Sprite(_treeTexture, _renderContext)
            {
                Width = _treeTexture.Width,
                Height = _treeTexture.Height,

                //X = 0,
                //Y = 0
            };
            _treeSprite.X = 1024 / 2 - (_treeSprite.Width / 2);
            _treeSprite.Y = 768 / 2 - (_treeSprite.Width / 2);
        }

        private static void RenderSprite()
        {
            _treeSprite.Draw();
            _treeSprite.X += 0.001f;
        }

        private static void PoolEvents()
        {
            while (SDL_PollEvent(out SDL_Event e) == 1)
            {
                PoolWindowEvent(e);
                PoolKeyEvent(e);
            }
        }

        private static void PoolKeyEvent(SDL_Event e)
        {
            if (e.key.type == SDL_EventType.SDL_KEYUP)
            {
                if (e.key.keysym.sym == SDL_Keycode.SDLK_ESCAPE)
                    _mainLoop = false;
            }
        }

        private static void PoolWindowEvent(SDL_Event e)
        {
            if (e.window.windowEvent == SDL_WindowEventID.SDL_WINDOWEVENT_CLOSE)
                _mainLoop = false;
        }
    }

    public struct Texture
    {
        public Texture(string path, IntPtr renderContext)
        {
            
            Instance = IMG_LoadTexture(renderContext, path);
            SDL_QueryTexture(Instance, out var format, out var access, out var width, out var height);
            Width = width;
            Height = height;
        }
        
        public int Width { get; }
        
        public int Height { get; }

        public IntPtr Instance { get; }
    }

    public struct Sprite
    {
        private readonly Texture _instance;
        private readonly IntPtr _renderInstance;
        private SDL_Rect _bounds;

        public Sprite(Texture texture, IntPtr renderContext)
        {
            _instance = texture;
            _renderInstance = renderContext;
            X = 0;
            Y = 0;
            Width = texture.Width;
            Height = texture.Height;

            _bounds = new SDL_Rect()
            {
                x = 0,
                y = 0,
                w = _instance.Width,
                h = _instance.Height
            };
        }

        public float X { get; set; }
        
        public float Y { get; set; }
        
        public float Width { get; set; }
        
        public float Height { get; set; }

        public void Draw()
        {
            var w = Width;
            var h = Height;
            var drawRect = new SDL_FRect()
            {
                x = X,
                y = Y,
                w = w,
                h = h
            };
            
            Console.WriteLine($"{w} {X}");

            SDL_RenderCopyExF(_renderInstance, _instance.Instance, ref _bounds, ref drawRect, 0, IntPtr.Zero,
                SDL_RendererFlip.SDL_FLIP_NONE);
        }
    }
}

Спрайт передвигается так же рывками по одному экранному пикселю.

Да. Эксперименты показали, что нет вообще никакой разницы, передавать целые координаты или с плавающей точкой, SDL у себя внутри перед отрисовкой их округляет.

  1. Ширина не целая, координата не целая: движется волнами

  2. Ширина целая, координата не целая: движется прыжком

  3. Ширина не целая, координата целая: движется прыжком

  4. Ширина целая, координата целая: движется прыжком

Растительность на заднем плане из двух спрайтов. Табличка - один спрайт. Вертикальные волны - это тот самый эффект, который можно заметить и на других видео. Он же дает и пробелы в земле. Я экспериментировал с демкой написанной с нуля, и понял, что эти волны можно получить, если прибавлять к _draw_rect.w любое число или умножать на не целое. Если умножить на целое, волн не будет, но тогда получится эффект, как на видео с параллаксом, где спрайт перемещается не плавно, а заметными рывками, хоть и согласно пиксельной сетке монитора. Плюс ко всему, при перемещении камеры спрайты переходят на следующий пиксель не одновременно, а в разнобой, из за чего складывается ощущение, что спрайты между собой дергаются.

Все таки, мне кажется, дело не в неправильном округлении, не в масштабировании.

Я привязал камеру к кусту, а сам куст начал двигать каждый кадр на 0.0001 по оси X без умножения на DeltaTime.

Сначала видно, как плывет куст, но когда камера начинает двигаться за кустом - плыть начинает все остальное окружение. (https://www.youtube.com/watch?v=JJCuxeHgyrQ).

Мне кажется, природа плавающих пикселей и пробелов между тайлами одна и та же. Пробелы появляются, когда тайл с одной стороны уже перескочил на пиксель, а тайл с другой стороны - нет.

Проблема в функции SDL_RenderCopyExF, или в враппере SDL для C#.

Попробую доказать это, сделаю демку с минимум кода чтобы исключить вообще любые косяки в коде.

К сожалению, разрыв есть, даже когда размер блока целый.

Попробовал в сторону ближайшего целого через упаковку в int +0.5, эффект тот же, что и на первом видео (сделал public)

При этом расстояние между тайлами так же скачет

Лог
2: (77; 77) (558; 653)
1: (77; 77) (482; 653)
Result: 76, DeltaTime: 0,01708

2: (77; 77) (558; 653)
1: (77; 77) (482; 653)
Result: 76, DeltaTime: 0,0164386

2: (77; 77) (558; 653)
1: (77; 77) (482; 653)
Result: 76, DeltaTime: 0,016708

2: (77; 77) (558; 653)
1: (77; 77) (481; 653)
Result: 77, DeltaTime: 0,016612

2: (77; 77) (558; 653)
1: (77; 77) (481; 653)
Result: 77, DeltaTime: 0,0167341

Можно округлить в сторону отрицательной бесконечности через Floor: Можно через Round или (int), эффект будет один и тот же.

_draw_rect.w = unit * (_size.Width / PixelPerUnit);
_draw_rect.h = unit * (_size.Height / PixelPerUnit);

_draw_rect.x = Math.Floor(point.X - _draw_rect.w * transformTo.Achor.X);
_draw_rect.y = Math.Floor(point.Y - _draw_rect.h * transformTo.Achor.Y);

В таком случае спрайты начинают дергаться целиком: https://youtu.be/56L5giUZL9A

Почему так происходит, я показал вот здесь: https://youtu.be/kjoDMnizsdM (здесь для примера не используется ничего, кроме попиксельного перемещения слоев с разной скоростью, чтобы исключить ошибки в DeltaTime и математических функциях отвечающих за плавное перемещение. Нет даже масштабирования текстур, они представлены как есть)

Казалось, субпиксельная точность поможет избавиться от такого эффекта

Можно даже вместо SDL_RenderCopyExF заюзать SDL_RenderCopyEx, чтобы вообще исключить возможность косяков в коде, но эффект тот-же.

Вы навели меня на одну мысль. По идее, расстояние между двумя тайлами должно быть статично (при моих параметрах это 76,8). Чтобы это проверить, я убрал все тайлы земли кроме двух, поставил их рядом, и начал вычислять их фактическое расстояние между собой. Логи показали интересный результат

Лог
№ спрайта: (ширина; высота) (x; y)


2: (76,8; 76,8) (806,1994; 652,8)
1: (76,8; 76,8) (729,3994; 652,8)
Result: 76,79999

2: (76,8; 76,8) (806,1991; 652,8)
1: (76,8; 76,8) (729,39905; 652,8)
Result: 76,80005

2: (76,8; 76,8) (806,1987; 652,8)
1: (76,8; 76,8) (729,3987; 652,8)
Result: 76,80005

2: (76,8; 76,8) (806,19836; 652,8)
1: (76,8; 76,8) (729,3984; 652,8)
Result: 76,79999

2: (76,8; 76,8) (806,198; 652,8)
1: (76,8; 76,8) (729,398; 652,8)
Result: 76,79999

2: (76,8; 76,8) (806,1977; 652,8)
1: (76,8; 76,8) (729,3977; 652,8)
Result: 76,79999

2: (76,8; 76,8) (806,1974; 652,8)
1: (76,8; 76,8) (729,39734; 652,8)
Result: 76,80005

UPD: Я округлил экранные координаты до двух знаков после запятой перед вычислением расстояния, и результат стал еще интереснее

Лог
2: (76,8; 76,8) (857,1; 652,8)
1: (76,8; 76,8) (780,3; 652,8)
Result: 76,79999

2: (76,8; 76,8) (857,09; 652,8)
1: (76,8; 76,8) (780,29; 652,8)
Result: 76,80005

Если считать данные вручную, получается всегда 76,8, но во время выполнения это не так. Логика float в данном случае мне не понятна.

Да, это тоже проблема. Их природа, скорее всего, такая же как и у остальных плавающих спрайтов. Земля - тайловый спрайт размером 15x15, и чтобы все эти маленькие пиксельартные спрайты отобразить на FullHD и выше, пришлось поработать над игровым масштабированием. Происходит оно следующим образом:

При загрузке спрайта земли ему назначается pixelPerUnit = 15; (это на верхнем уровне дает возможность упростить работу с координатной плоскостью, благодаря этому Size спрайта земли равен 1x1, а значит по оси X его можно расставлять на целочисленных координатах без стыков) Область видимости камеры по умолчанию устанавливается на 5.

При отрисовке спрайта над его размером и положением проводятся следующие манипуляции:

  1. Вычисляем Unit (единицу измерения) мира

    1.1. Берем две точки, (1;0) и (0;0), конвертируем их из мировых в экранные координаты, и вычитаем первую из второй. В итоге у нас получится размер мировой единицы измерения в экранных пикселях.

  2. Получаем Высоту и Ширину объекта:

    _size.Width = _bounds.Width * transformTo.LocalScale.X;
    _size.Height = _bounds.Height * transformTo.LocalScale.Y;
  3. Вычисляем draw_rect для передачи в SDL:

    var unit = Camera.MainCamera.WorldUnit;
    
    _draw_rect.w = (float)(unit * (_size.Width / PixelPerUnit));
    _draw_rect.h = (float)(unit * (_size.Height / PixelPerUnit));
    
    _draw_rect.x = (float)(point.X - (_draw_rect.w * transformTo.Achor.X));
    _draw_rect.y = (float)(point.Y - (_draw_rect.h * transformTo.Achor.Y));
    
    center.x = (float)(_draw_rect.w * transformTo.Achor.X);
    center.y = (float)(_draw_rect.h * transformTo.Achor.Y);
  4. И на основе этих данных рисуем:

    SDL.SDL_RenderCopyExF(Game.RenderContext, _texture.Instance, ref _bounds.SDLRect, draw_rect, transformTo.Degrees, ref center, flip);

В итоге размер тайла в экранных координатах при окне рендера в 1024х768 становится равен 76.8x76.8, и не меняется на протяжении всего рендера.

Можно было бы объяснить разрывы в тайлах, если бы в какой-то момент времени SDL на отрисовку передавалась разная ширина тайла, но это не так.

Только сейчас дошло, о чем речь с 120мс. Вообще при рендере таких заметных просадок по FPS нет, скорее всего так получилось, потому что участок с рябью я ловлю выводом в консоль и закрытием окна (чтобы не терять время на запись в файл). В момент, когда ЛКМ зажимает кнопку закрытия окна отрисовка останавливается. В общем, скорее всего 120мс - это скорость моего клика.

По поводу значений - да. Я сделал функцию, которая заставляет камеру плавно следовать за персонажем. Там для указания скорости слежения используется DeltaTime, чтобы она не зависела от FPS.

var camera_pos =  Math.Lerp(Camera.Transform.Position.X, _playerAnimator.Transform.Position.X, 1 * Time.DeltaTime);
Camera.Transform.Position = new Point(Math.Clamp(camera_pos, -3.5, 3.9), 0);

Но так как в SDL нет понятия камеры, приходится, по сути, двигать все остальные спрайты в игровом мире при движении персонажа. Поэтому DeltaTime влияет на вообще все спрайты при вычислении координат.

Я могу выслать демку, чтобы вы смогли своими глазами взглянуть на то, как это выглядит в реальном времени (видео не все передает, к сожалению), если вам интересно, конечно. Так же у движка открытый исходный код. В любом случае вы уже очень сильно помогли, за что вам большое спасибо. Баг ищу уже очень долго, обидно, что разработка встала из-за такой мелочи.

UPD: При локе в 15 FPS отчетливо видно, что этот эффект происходит из-за того, что в определенный момент времени все пиксели по оси Y меняют свою фактическую ширину. https://youtu.be/dLct7Rsk0BU

UPD2: На уровне SDL это можно решить включением линейного или анизатропного сглаживания, но тогда можно забыть про тайловый pixelart, потому что картинка сильно мылится и и становятся видны края тайлов

Скриншот

Вертикальная синхронизация отключена, верхний лок FPS - 144, просто чтобы впустую не гонять GPU, поэтому время между кадрами может быть очень маленьким. Попробовал сделать лок FPS на 15, наблюдается та же проблема с рябью.

Для вычисления времени между двумя кадрами я, по аналогии с Unity, сделал свойство DeltaTime, которое можно получить в любой момент времени. Его удобно юзать, чтобы открепить скорость воспроизведения анимации от FPS.

Для этого графика пришлось взять другие данные, так как в старых метка времени в мс была не больше двух знаков.

Получилось как-то так:

Просто отрисовка происходит чаще

"59:21:6927","1780,8"

"59:21:6987","1780,8"

"59:21:7047","1780,8"

"59:21:7108","1780,8"

"59:21:7173","1780,8"

Взял спрайт дома. Взял участок данных за промежуток времени, в который происходит самая заметная "рябь" (209 строк с 29с:67мс по 31с:12мс). График получился не ступенчатый и не совсем ровный. Именно эти координаты передаются в функцию SDL_RenderCopyExF.

График и данные

Не совсем понял, что вы подразумеваете под плавающим фреймрейтом.

Да, это первое, что приходит в голову. Но я, все перепроверив с десяток раз, в упор не вижу, в каком месте косяк.

1

Information

Rating
Does not participate
Location
Магадан, Магаданская обл., Россия
Date of birth
Registered
Activity