forum
Добро пожаловать, Гость
Логин: Пароль: Запомнить меня

ТЕМА: Новости с фронта

Новости с фронта 22.03.2016 16:58 #7244

  • StaticZ
  • StaticZ аватар
  • Вне сайта
  • Разработчик
  • Демиург
  • Сообщений: 292
  • Спасибо получено: 142
  • Репутация: 68
sam0delk1n пишет:
Основная причина -- большое количество деталей сцены размером меньше пиксела, распространение спекуляра и расширенного диапазона освещения (который hdr).
Спасибо за исчерпывающий ответ, теперь я ощущаю себя еще большим мамонтом, прогресс двинулся дальше и теперь моих скромных общих представлений уже с трудом хватает чтобы понять принципы работы современных движков.... В моем мире одно лишь предположении о том что что-то может быть меньше пикселя является ересью. Тем не менее все равно не очень понятно, ну я еще могу себе понять что это касается мелких деталей в дали вроде проводов или прутьев сетки, но зачастую это режет глаз и с крупными объектами находящимися вблизи к примеру край стола.
sam0delk1n пишет:
А вот используя системную память сам факт заполнения больших участков буфера цветом уже проблематичен. Поэтому раз заполняется всёравно медленно, можно за это время посчитать полигоны и воспользоваться этой возможностью чтобы рисовать плавные рельефы. Одним словом увеличение количества полигонов, если они рисуются на ту же площадь изображения, не сильно замедлят рендеринг, как это было бы в случае с видеокартой.
Насколько я понял вы говорите грубо говоря об рендинге в два потока - один считает нормали, а другой рисует. Если да, то спорный вопрос, конечно в случае реального 3д оно имеет смысл обновлять нормали параллельно, чтобы не тормозить отрисовку, в конце концов при плавном вращении камеры отставание и расинхрон в расчетах нормалей не так критичен. Тем не менее на практике те же таблицы в памяти работают шустрее, теоретически конечно подгрузка страниц памяти в кэш процессора может быть медленее расчета в лупе загруженных в конвеер инструкций, однако на практике там будет куча переменных что раскиданы по всему адресному пространству (свойства\поля классов к примеру) и их перегонка на регистры те же яйца в профиль. К тому же за паралелизм отвечает ОС и она сама решает что куда и как пойдет. Разве что запускать несколько процессов на разных ядрах с общим адресным пространством и учитывая специфику конкретной архитектуры (зачастую кэш общий на 2 ядра). Но это уже какой-то изврат безумно сложный и трудозатратный. Другое дело что в случае реального 3д тут гигабайты потребуются чтобы хранить все возможные варианты. В случае 2д считать эти нормали постоянно вообще не нужно, я даже текстурные координаты при рендинге рельфа не высчитываю и собственно тут какраз куда критичнее оптимизация алгоритмов один лишний IF в отрисовке пикселя очень серьезно сказывается на производительности. Собственно и рельеф-то рисоваться будет всего 1 раз и частично обновляться при скроле или изменении высоты части на экране и даже в этом случае 20 обновлений в секунд хватит за глаза для анимации. А ну да еще при изменении положения света да, если карта большая то тут да имеет смысл пускать параллельный перерасчет нормалей
Game isn't a dream, it is the reality, reality which is coming while we dream...
Последнее редактирование: 22.03.2016 17:03 от StaticZ.
Тема заблокирована.

Новости с фронта 22.03.2016 20:02 #7245

  • sam0delk1n
  • sam0delk1n аватар
  • Вне сайта
  • Интересующийся
  • Сообщений: 76
  • Спасибо получено: 47
  • Репутация: 24
StaticZ пишет:
к примеру край стола.
Вот например много тонких бликов на машинах (на краях и на других закругленных поверхностях, особенно вокруг бокового окна слева). Обычно металл очень тонкие блики даёт, меньше пикселя шириной если камера достаточно далеко. Тогда без сглаживания будет хаотичное мерцание отдельных пикселей.

Ещё бывает яркий свет сзади объекта, если бы это была не кожа, а металл, то блик получился бы очень тонкий.

А вот пример с мелкой листвой (куст по центру и деревья справа-сверху). Это уже на скрине так, а в динамике когда листья колышутся, вообще ужас.

А после сглаживания листья уже получше:

Так же игры с deferred rendering'ом и составными пост-эффектами могут что-то сглаживать, а что-то нет. Например отражения на воде могут не сглаживаться (ещё могут и более низкого разрешения быть).

StaticZ пишет:
Насколько я понял вы говорите грубо говоря об рендинге в два потока
Не я про один поток. У процессора есть префетчинг. Он заранее подгружает в кеш следующие инструкции и данные следующие за уже прочитанными. Грузит кеш-линиями по 64 байта (даже если нужна только переменная в 4 байта). Обычно при использовании бинарных языков можно контролировать как разместить данные в памяти и плотно их упаковать, чтобы кеш-линии были целиком из полезных данных, и друг за другом по мере нужности данных. Префетчер всегда работает параллельно с вычислителем, главное чтобы данные лежали в предсказуемых местах, а не хаотично разбросаны по всей памяти, тогда и скорость резко возрастет и не будет простоев в ожидании данных. У ядер общий кеш только L3, а верхние два у каждого свои, так что чем автономнее код -- тем лучше. А работа с вершинами не требует обращения к большим экранным буферам, поэтому теоретически обработка вершин не должна очень сильно нагружать пропускную способность памяти, а cpu в целом достаточно быстро будет их считать. В любом случае надо пробовать, только на тестах станет ясно =).
Тема заблокирована.
Спасибо сказали: Samael, StaticZ

В поисках Света и Тени (взгляд изнутри) 25.03.2016 04:10 #7249

  • StaticZ
  • StaticZ аватар
  • Вне сайта
  • Разработчик
  • Демиург
  • Сообщений: 292
  • Спасибо получено: 142
  • Репутация: 68
Синопсис
На этот раз я хочу рассказать, как ковалась сталь и как закалялся металл… Хотя пожалуй об этом как нибудь в другой раз, а сейчас я лучше расскажу о том как реализован 3D рельеф. Конечно сей псевдо-научный трактат является лишь наглядной демонстрацией эволюции примата и разработки проекта под кодовым названием «IceTeria», тем не менее возможно сей материал окажется полезным и для других разработчиков желающих приобщиться или разобраться в 3D графике. Но раз уж эта тема посвящена конкретному проекту, стоит немного уделить внимание и другим изменениям, одним из которых стала реализация прокрутки карты. Ну а самым важным изменением о котором и пойдет в основном речь, конечно стало освещение, причем изменяемое в зависимости от положения солнца (т.е. времени суток):





Клиппер как синдром аутизма двухмерной графики
Истинный 2D движок без клипера, все равно что машина без мотора — поедет лишь если в нее засадить Флинстоуна. Для тех, для кого слово клиппинг ни о чем не говорит, поясним что произошло оно от древне забугорского clip — «обрезать». И как не сложно догадаться это техника обрезки изображения, что является основой оптимизации любого более менее серьезного истинного 2D движка (в отличии от ложного, что использует силу Вуду (то бишь аппаратного ускорения)). Ведь очевидно, что скорость отрисовки зависит от числа обрабатываемых пикселей, даже если тупо идет копирование картинки — чем меньше пикселей, тем меньше надо копировать из одной области памяти в другую, а значит тем меньше это времени займет. Ну а в случае к примеру 3х мерного рельефа, где идет приходиться попиксельно обрабатывать данные это дает куда больший выигрыш. Для чего оно нужно на практике? Допустим у нас персонаж делает шаг и нам надо перерисовать спрайт, а значит и все что под ним, но зачем ради этого перерисовывать весь экран, если достаточно обновить небольшую область 60х28 пикселей? Конечно можно возразить, что в реальной жизни у нас на экране куча спрайтов и все двигается, но так ли это? Одни спрайты обновляются раз в 0.4 секунды, другие раз в 0.3, третьи раз в 0.7 и в итоге мы получаем что для обновления экрана нам все равно не нужно перерисовывать абсолютно все, а это позволяет сократить нагрузку в десятки раз. В случае с картой задачка усложняется тем что надо не только отбрасывать пиксели рисуемых тайлов, что не попадают в область, но и сделать предварительно выборку самой этой области, ибо проверка каждого тайла тоже не малая работа и когда их тысячи это сильно бьет по производительности. В нашем случае все вышло немного сложнее, т.к. мало того что у нас ромбики так еще и карта не квадратная, в зависимости от строки и столбца число тайлов в ней\нем скачет, вместе с положением (а ведь так как рисовка идет по диагоналям, нужно определить начало и конец каждой). Кроме того так как карта у нас 3х мерная пришлось увеличить область поиска по оси Y на предмет возможных высоких «гор» снизу и «впадин» сверху, что могут попасть в нашу область. В итоге вышло вот что (слева показан первый клипер, что делает предварительную выборку по тайлам, что могут попасть в область отрисовки, справа результат после применения второго клипера что отсекает отрисовку вне заданной области, желтая рамка просто для наглядности показывает границы области что должна быть отрисованно):



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

3D Рельеф своими руками

Чтобы отобразить, что-то 3х мерное без магии OpenGL и\или DirectX потребуется:



Спроецировать изображение на плоскость, определив координаты вершин на экране.
Нарисовать примитивы по известным координатам (с точками и линиями все и так понятно, так что можно сказать тут речь идет лишь о полигонах (трехмерные объекты принято представлять в виде набора треугольников или реже четырехугольников))
Обработать освещение сцены (конечно на деле это следует делать одновременно с предыдущем пунктом, но для простоты изложения разделим их). Красивое освещение конечно всегда радует глаз, но надо понимать, что оно еще попросту необходимо для ощущения объема — на примере чайника справа отчетливо видно отличие результата с расчетом освещения и без. Так же освещение позволяет сгладить острые углы мало полигональных объектов.
В трехмерной графике, как и в жизни расстояния зависят от положения камеры и возня с координатами удобно сводиться к операциям над матрицами и векторами, оперируя с которыми можно легко вращать, масштабировать или проецировать объекты. В нашем случае двухмерной «изометрической» проекции с эти все намного проще — координаты XY сами по себе являются проекциями на экран, а координата Z проецируется на ось Y c выбранным коэффициентом. Таким образом нам остается лишь растянуть изображение полигонов. Я не хочу вдаваться в подробности механизмов 3D рендера, так как тема сия объёмна и мало относиться к нашему случаю.



Если вкратце, то полигон помимо вершинных координат, определяющих его положение в пространстве имеет также и текстурные координаты связывающие часть текстуры отображаемой на нем. Таким образом задачка наложения текстуры сводиться к трансформированию текстурных координат, самый простой вариант — заключается в том, что для полигона определяется наиболее длинная сторона по Y, затем из уравнения прямой интерполируются координаты начала и конца строк, после чего изображение рисуется построчно с линейной интерполяцией текстурных координат.

Но, так как я не пишу полноценный 3D движок задача сильно упрощается, тем фактом что размеры всех тайлов по оси X постоянны, а высота по Z влияет лишь на координаты вершин по Y. Так что нужно это лишь заранее сделать поворот текстуры на 45 градусов и сжать ее по оси X (ну или как вариант подготовить таблицы трансформирования координат). После чего все сводиться к простой линейной интерполяции столбцов изображения. Таким образом как видно в нашем случае отрисовка 3D рельефа осуществляется очень легко и быстро, конечно интерполяция и попиксельная отрисовка не так эффективная как построчный блиттинг, но тем не менее не является слишком уж тяжелой.

Да будет свет!

Как известно из оптики в общем случае отражение падающего света некоторой поверхностью делиться на диффузное и зеркальное. Причем в зависимости от падающего излучения одна и та же поверхность может иметь различные составляющие диффузного и зеркального отражения для разных спектров длин волн. Диффузное отражение происходит за счет неровностей поверхностей порядка равного или превышающего порядок длины волны.



Но не будем сильно углубляться в физику и больше сконцентрируемся на прикладном применении теории. Во первых в случае 2D графики зеркальной составляющей можно и даже нужно пренебречь. Дело в том, что она зависит от угла падения света, что в свою очередь зависит от расстояния от поверхности до источника света.

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



Отличие можно легко оценить на примере кубика слева в изометрической проекции и перспективе. Потребность в подобном правиле диктуется здравым смыслом — иначе при движении камеры пришлось бы перерисовывать все что есть и не просто перерисовывать, но еще и трансформировать как-то, а это слишком уж накладно.

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



Idiff = Kd • Ip • cos(θ), где Ip — интенсивность света, Kd — коэффициент диффузной отражательной способности поверхности (от 0 до 1). N — вектор нормали к поверхности. L — вектор задающий направление источника света. Если вспомнить, что скалярное произведение векторов N•L = |N|•|L|•cos(θ), то в случае если вектора N и L единичные получим, что Idiff = Kd • Ip • (N•L)

Для полного счастья введем еще рассеянное освещение, интенсивность которого определяется по формуле Iamb = Ka • Ia, где Ia — интенсивность рассеянного света, Ka — коэффициент рассеянного отражения. Учитывая оба источника света получим результирующую интенсивность: I = Idiff + Iamb = Kd • Ip • (N•L) + Ka • Ia. Однако в нашем случае двухмерной графики рассеянное освещение уже определяется спрайтами и тайлами, а в случае если мы захотим например сделать ночь эта составляющая будет определяться яркостью. Теперь нам надо разобраться с векторами L и N. Опять же исходя из двухмерности использование точечных источников света помимо солнца не является целесообразным, во первых по причине малого влияния, во вторых из-за черезмерного возрастания нагрузки на процессор.

Но настало время перейти от теории к практике, для чего сначала объявим тип вектора:
struct Vector3f { float x, y, z; }
где x, y, z длинны проекций вектора на соответствующие оси. Тогда получение вектор L для солнца в зависимости от угла (angle) можно представить как:
Vector3f GetLightVector(float angle)
{
	Vector3f L;
	L.y = 1f;
	L.x = (float)(y*tan(angle)*pi/180f);
	L.z = 0.5f;
	float vlen = sqrt(x*x+y*y) / sqrt(0.75);
	L.x /= vlen;
	L.y /= vlen;
	return L;
}
Мы задаем вектор направленный по оси Y составляющей с ней угол angle. Затем осуществляем его нормализацию в плоскости XY, положив составляющую Z равную 0.5. Так как в случае изометрии освещенность любой поверхности в плоскости XY не должна зависеть от вектора освещенности это означает что dot ( Vector3f(0f,0f,1f), L ) должен быть равен константе, а это условие будет выполняться лишь в том случае если L.z = const. В данном случае для удобства было выбрано значение 0.5.

Теперь нам остается получить нормаль к поверхности, для задания который как известно достаточно трех точек. И так:
Vector3f GetNormal(Vector3f a, Vector3f b, Vector3f c)
{
	Vector3f n, v1, v2;
	v1.x = a.x - b.x;
	v1.y = a.y - b.y;
	v1.z = a.z - b.z;

	v2.x = b.x - c.x;
	v2.y = b.y - c.y;
	v2.z = b.z - c.z;

	n.x = v1.y * v2.z - v1.z * v2.y;
	n.y = v1.z * v2.x - v1.x * v2.z;
	n.z = v1.x * v2.y - v1.y * v2.x;
	return n;
}
Нормаль направлена от лицевой стороны, что определяться порядком перечисления вершин (по часовой стрелки или против), в нашем случае используется перечисление по часовой стрелке.

Ну и финальный аккорд это нормализация, сложение векторов и скалярное произведение:
Normilize(ref Vector3f v)
{
	float vlen = (float)sqrt(x * x + y * y + z * z);
	v.x /= vlen;
	v.y /= vlen;
	v.z /= vlen;
}
Vector3f Sum (Vector3f l, Vector3f r)
{
	Vector3f v;
	v.x = l.x + r.x;
	v.y = l.y + r.y;
	v.z = l.z + r.z;
	return v;
}
float CalcIntensity(Vector3f v1, Vector3f v2)
{
	float dot = v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
	return 32f * min(max(0.1875f, dot), 0.6875f);
}
Последнее наверное стоит пояснить для облегчения расчетов мы будем считать скалярное произведение только для единичный векторов, и только для определения интенсивности света. По этой причине мы и умножаем результат на 32, таким образом для поверхностей в плоскости XY интенсивность света будет равна 16.0, в случае если поверхность повернута к источнику света ее интенсивность будет от 16 до 22, а если повернута от источника света то от 6 до 16. Дополнительные граничные условия позволяют избежать через мерной засветки и затемнения. Почему были выбранны такие странные на первый взгляд значения, расскажу когда дойдем до расчета результирующего цвета пикселя.

В принципе тут много где можно было сэкономить на расчетах, т.к. длинны проекций сторон тайла в плоскости XY величина постоянная, а значит их расчет является избыточным, таким образом расчет нормалей можно было переписать используя априорные знания о координатах вершин лишь из составляющих координат Z.

Плоская модель затенения



И так теория позади, с математикой разобрались настало дело практики. Для простоты положим Kd и Ip равными 1, до тех пор пока у нас всего один источник света его яркость не играет роли, а коэффициент диффузного отражения имеет смысл лишь в случае если хотите подчеркнуть различие в интенсивности освещения различных поверхностей. Это конечно хороши и интересно, но вспомним из-за чего весь сыр бор — свет нам необходим для улучшения восприятия объема рельефа, подчеркивания склонов а не выпендрежа. Таким образом интенсивность света будет равна произведению единичных векторов N и L. А результирующий цвет будет равен ca = min( max(0f, ca • I • Ia), 1f), где ca составляющая компонент R,G,B цвета пикселя от 0 до 1, а Ia — составляющая компонента цвета источника света. В случае бесцветного цвета Ia = 1.

Однако если помните, то я нормализовал вектор освещения так, что для плоского тайла cos(θ) = 0.5, а результат умножал на 32. Про магическое значение числа 32 будет пояснено ниже, а пока отмечу лишь, что это позволило слегка смухлевать и в нашем случае освещенность превратилась в коэффициентом засветки и затемнения поверхности.

В идеале нам остается найти нормаль и интенсивность в каждой точке, но расчет нормалей к каждому пикселю дело чрезвычайно муторное и накладное, к тому же в условиях низкой полигональнности надо как-то апроксиммирвать значения, для сглаживания угловатостей. Но обо все по порядку, начнем с самого простого варианта — плоской модели затенения (Flat shading), согласно которой мы просто считаем нормаль к каждой поверхности и применяем ее к любой точке на ней. Стоит добавить, что данная модель дает приемлемый результат лишь в случаях когда источник света достаточно удален от граней и произведение векторов N•L=const. Тут правда ждет небольшой сюрприз, так как тайл у нас определяется 4мя вершинами, тут можно или вручную поделить его пополам по горизонтали или вертикали или же ввести 5ю вершину по центру в плоскости XY и усредненными значениями координаты Z вершин.

Результат можно лицезреть справа, тут солнце двигается от -45 до + 45 градусов относительно оси Y, яркая монотонная текстура была выбрана специально для более наглядной демонстрации процесса затемнения и осветления

Магия Гуро



Как видно результат радует своей угловатостью, с этим можно бороться или увеличением числа полигонов или воспользовавшись моделями затенения с сглаживанием. Наиболее распространенна модель освещение по Гуро и пришедшая ей на замену модель по Фонгу. Последняя дает более лучшее качества и хоть существуют быстрые варианты данной модели она остается слишком тяжелой для процессора, поэтому оставим ее за кадром и сконцентрируемся на модели Гуро, что традиционно применялась в старых играх, по причине легко оптимизируемой линейной интерполяции используемой в ней.


В рамках этой модели мы должны оперировать не с нормалями к плоскостям а с нормалями к вершинам. Нормаль вершины определяется как Nv = ∑k Nk / | ∑k Nk |, где Nk — нормаль смежной грани. Аналогично можно ввести определение нормали ребра Ne. Теперь аналогично определяем интенсивность света в каждой вершине I1, I2, I3. После чего задача сводиться к уже хорошо нам знакомой линейной интерполяции при обработке рисуемой строки (scan line), для чего предварительно надо найти граничные значения интенсивностей определяемые по формулам (кстати по большому счету процесс наложения текстур ничем не отличается, кроме того что там текстурные координаты вместо интенсивности освещения):



I4 = I1 •(y4 — y2) / (y1 — y2) + I2 •(y1 — y4) / (y1 — y2)
I5 = I3 •(y5 — y2) / (y3 — y2) + I2 •(y3 — y5) / (y3 — y2)
Ip = I4 •(x5 — xp) / (x5 — x4) + I5 •(xp — x4) / (x5 — x4)


Конечно в нашем случае удобнее будет вести обработку вертикальных линий совместив ее с наложением текстуры. Использование априорных знаний о размерах тайлов так же позволит сильно сэкономить на вычислениях и даже избавиться от делений, даже более того поскольку вектора N и L у нас постоянны мы можем заранее рассчитать значения интенсивностей света для вершин. Результат этого безобразия представлен слева (сильно выделяющиеся тайлы по центру являются следствием, того что для оптимизации отрисовка плоских тайлов идет другим образом и на данном этапе там не учитывалось освещение).



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



Очевидное решение, что само напрашивается — ввести дополнительные условия для интерполяции, добавив посередине каждого ребра дополнительную точку с интенсивностью равной интенсивности света нормали данного ребра. Это и правда помогло решить проблему (результат представлен справа), однако качества сглаживания все еще оставляет желать лучшего. Это также легко объясняется, дело в том что для идеального результата при линейной интерполяции должно выполняться условия перпендикулярности осей. А в нашем случае при изменении координаты Z меняется и угол между диагоналями, а значит для получения более точного результата мы должны или как-то предварительно экстраполировать значения или разбить тайл на еще более мелкие примитивы или отказаться от линейной интерполяции и\или модели Гуро. Но так как данная проблема бросается в глаза лишь при большом контрасте, т.е. сильном затемнении на очень ярких текстурах, я решил пока остановиться на достигнутом. Возможно позже я еще вернусь к этому вопросу когда станет более ясно каким запасом быстродействия обладает движок.

Преобразование цвета

Давно пора бы было поставить точку и начать раздавать призы тем, кто осилил эту писанину и добрался почти до самого конца, но за кадром осталось еще один не мало важный аспект а именно применение освещенности. Что в этом такого? Все же просто:
var i = (byte)(intensity >> 9);
var b = (int)(*(byte*)line & 0x1F);
var g = (int)(*(ushort*)line & 0x3E0);
var r = (int)(*(ushort*)line & 0x7C00);
b = Math.Min(b * i, 0x3E0) & 0x3E0;
g = Math.Min(g * i, 0x7C00) & 0x7C00;
r = Math.Min(r * i, 0xF8000) & 0xF8000;
*dest = (ushort)((r | g | b) >> 5);
intensity += intensity_inc;
Но в результате мы наблюдаем проседание FPS с 130 до 70-75, несмотря на микро-оптимизации (я сэкономил на битовых сдвигах при извлечении и упаковки компонент цветов). Тут же становиться понятен мистический смысл умножения интенсивности (intensity) на 32, таким образом я избавился от преобразования не целочисленного значения и получил возможность оптимизации путем махинаций с битовыми сдвигами (о чем именно я говорю станет понятно в последнем варианте). Конечно тут сразу же смущает Math.Min попытка замены данной функции на так мною любимую битовую магию
var i = (byte)(intensity >> 9);
var b = (*(byte*)line & 0x1F) * i; 
b += ((0x3E0 - b) & ((0x3E0 - b) >> 31));
var g = (*(ushort*)line & 0x3E0) * i; 
g += ((0x7C00 - g) & ((0x7C00 - g) >> 31));
var r = (*(ushort*)line & 0x7C00) * i; 
r += ((0xF8000 - r) & ((0xF8000 - r) >> 31));
*dest = (ushort)(((r & 0xF8000) | (g & 0x7C00) | (b & 0x3E0)) >> 5);
intensity += intensity_inc;
На деле дало проигрыш в 10 FPS, что странно так как бенчмарк показал прирост битовой магии (val1 + ((val2 — val1) & ((val2 — val1) >> 31)) ) по сравнению с Math.Min в полтора-два раза, но как известно бенчмаркам никогда не стоит слепо верить. А вот его замена на ветвления, как ни странно дало прирост в 5 FPS:
var i = (byte)(intensity >> 9);
var b = (int)(*(byte*)line & 0x1F);
var g = (int)(*(ushort*)line & 0x3E0);
var r = (int)(*(ushort*)line & 0x7C00);
b *= i; b = (b <= 0x3E0) ? (b & 0x3E0) : 0x3E0;  
g *= i; g = (g <= 0x7C00) ? (g & 0x7C00) : 0x7C00;
r *= i; r = (r <= 0xF8000) ? (r & 0xF8000) : 0xF8000;
*dest = (ushort)((r | g | b) >> 5);
intensity += intensity_inc;
Подумав, как еще над оптимизацией данной задачи я в результате пришел к следующему варианту, что дал прирост в 15 FPS:
uint cl = (uint)(*line);
cl = ((cl | (cl << 16))) & 0x03E07C1Fu;
cl = (((cl * (byte)(intensity >> 9)) >> 4));
cl |= 0x1Fu * ((cl & 0x4008020u) >> 5);
cl &= 0x83E07C1Fu;
*dest = (ushort)(cl | (cl >> 16));
intensity += intensity_inc;
А что бы не заменяться каждый раз преобразованием цвета исходного тайла я решил перенести это преобразование в загрузку спрайта, да конечно это увеличит объем памяти, но сегодня она давно перестала быть дефицитом, а вот выигрыш еще в 5 FPS стоит пары лишних мегабайт. Таким образом оптимизация пары строк кода позволило увеличить производительность отрисовки фактически на 50%.

Ну а на этом пока все, до новых встреч!
Game isn't a dream, it is the reality, reality which is coming while we dream...
Последнее редактирование: 26.03.2016 00:08 от StaticZ.
Тема заблокирована.
Спасибо сказали: AnnTenna, Samael, sam0delk1n

Новости с фронта 26.03.2016 20:12 #7251

  • sam0delk1n
  • sam0delk1n аватар
  • Вне сайта
  • Интересующийся
  • Сообщений: 76
  • Спасибо получено: 47
  • Репутация: 24
Первые два скрина с ландшафтом выглядят достаточно хорошо. Если они также хорошо выглядят и при других углах освещения, то я думаю можно дальше не улучшать.

Мне кажется управляемый код не надо оптимизировать в стиле машинного. Компилятор лучше знает. Математических/алгоритмических оптимизаций достаточно. Можно экспериментировать, но результаты непредсказуемы, это времени займёт много.

Что касается машинного, там ковыряние в битах и байтах также может только ухудшить скорость. Операции нужно выполнять с регистром целиком. Преобразование в байты и обратно будет добавлять лишние операции. Вот например 32-битный регистр общего назначения в x86 разделён на вложенные части EAX (биты 0-31), AX (биты 0-15), AL (биты 0-7). Операции с байтом проводятся в регистре AL. Если данные находятся в битах 24-31, их нужно циклическим сдвигом (или ещё как-то) перенести в AL, затем только делать операцию, затем обратно поставить на место. Это долго. Поэтому нужно использовать операции которые обрабатывают сразу весь регистр. Например сложение: можно сложить два 4-байтных значения, при этом каждый байт в них также корректно сложится. В SSE и видеокартах 128 битные регистры, а операции с float не сильно уступают в производительности целочисленным, поэтому считается float4 самая оптимальная величина для хранения 3d координат, RGB цвета и т.п. Четвёртая компонента используется для корректного умножения на матрицы или для хранения некоторых коэффициентов.

Значения меньше float4 эффективны в плане экономии памяти (и её пропускной способности), когда являются элементами больших экранных буферов или текстур. Поэтому обычно текстуры в памяти хранят 4 байта integer на тексел. Выборка цвета берёт и конвертирует integer во float4 (4 штуки float). Все вычисления происходят с ним. Затем результат конвертируется обратно в integer и записывается в пиксел экранного буфера. Вот например подход deferred rendering требует создавать экранные G-Buffer'ы, размером в сотни мегабайт, так что даже скорости GDDR5 весьма ограничивают реализацию.

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

Ещё, источник света тоже может иметь цвет, а не только быть белым. На практике интенсивность и цвет света лучше кодировать в виде одного вектора RGB. Отношение интенсивности каждого компонента к друг другу определяет цвет, а сумма интенсивности всех компонентов -- яркость.

Вот Ваша исходная формула: I = Kd * Ip * (N * L) + Ka * Ia. Если перейти к RGB то Ip и Ia будут векторами в которых и интенсивность и цвет света. Далее: свойство диффузного отражения поверхности обычно одинаково и для фонового (ambient), так как свойство поверхности не меняется, а фоновый свет он тоже "диффузно" отражается. Более того текстура цвета как раз и представляет собой это "свойство диффузного отражения" для каждого компонента RGB. Так что Kd и Ka это Tex (цвет текселя текстуры в этом месте поверхности) и это тоже вектор. В Вашем случае на цвет тексела придётся умножать позже. Если бы текстура была здесь, формула примет следующий вид: I = Tex * ( Ip * (N * L) + Ia ). Результат I это итоговый цвет пиксела, а значит тоже вектор. Где * это покомпонентное умножение, то-есть также как скалярное, но без сложения, на выходе вектор, а не число. А скалярное я обозначу dot, получаем: I = Tex * ( Ip * dot( N * L ) + Ia ).

Код HLSL мог бы выглядеть так:
// tex2D() это выборка цвета текселя в данном месте поверхности текстуры.
float4 result = tex2D() * ( diffLigCol * dot( normal, diffLigDir ) + ambLigCol );
Заметьте что все операции происходят сразу над 128 битными регистрами, без выделения отдельных байтов, а значит быстро.

Цвет солнца должен быть чуть более жёлтым, а при восходе/закате более красным. Ambient освещение обычно играет роль света от неба, со всех сторон. Ночью цвет солнца можно заменить на цвет луны, а фоновое освещение убрать. Одной такой игрой света можно сильно разнообразить цветовую палитру изображения. Что касается пересвета пикселей: не стоит его так уж бояться, текстуры цветные, а не белые и цвет света тоже не всегда белый, поэтому каждый RGB-компонент результата не всегда будет превышать максимальное значение. С другой стороны, если страховаться и оставлять запасы, может получиться так что гамма окажется совсем блёклой. 256 значений яркости компонента доступных монитору не так уж много и их надо использовать максимально, чтобы цвета выглядели насыщенно. В играх с HDR и tone-mapping'ом вычисления идут с большим диапазоном, чем 256 градаций. Затем для результирующего изображения подбирается экспозиция и корректируются цвета, чтобы вывести наилучшую гамму на "ограниченный" монитор. Игры до появления HDR обычно выглядят заметно более блёклыми (ну или у художников и дизайнеров было очень много работы по качественному подбору освещения и цветов вручную).
Тема заблокирована.
Спасибо сказали: StaticZ

Новости с фронта 27.03.2016 03:08 #7252

  • StaticZ
  • StaticZ аватар
  • Вне сайта
  • Разработчик
  • Демиург
  • Сообщений: 292
  • Спасибо получено: 142
  • Репутация: 68
sam0delk1n пишет:
Первые два скрина с ландшафтом выглядят достаточно хорошо. Если они также хорошо выглядят и при других углах освещения, то я думаю можно дальше не улучшать.
Это от текстур зависит, если взять монотонную ярко белую текстуру то будут видны все косяки затенения, а на этих да с большим трудом можно различить. Но так как на практике смысла использовать подобные текстуры не много то да согласен...

sam0delk1n пишет:
Мне кажется управляемый код не надо оптимизировать в стиле машинного. Компилятор лучше знает. Математических/алгоритмических оптимизаций достаточно. Можно экспериментировать, но результаты непредсказуемы, это времени займёт много.
У вас какое-то мистическое представление об управляемом коде. Во первых указатели и размещение данных в неуправляемой памяти позволяет исключить их из сборщика мусора, как следствие облегчив труд оного и что главное предотвратив задержки связанные с их перемещением туда сюда. Во вторых применение не безопасного кода позволяет избавиться от рейндж чеков (в управляемом коде при доступе к элементу массива к примеру всегда идет проверка диапазона и тд и тп), в третьих у виртуальной машины нет много времени для проведения длительных оптимизаций и анализа кода по этой причине к примеру встраиваются (inline) лишь методы чей размер не превышает несколько байт, остальные просто обходятся (если не указать аттрибутом компилятору принудительно встраивать данный метод), я уже молчу о том что компилятор даже не задействует многие возможности IL (к примеру как оказалось там есть тот же fsqrt). Ну и наконец ассемблер ассемблером, но решает все число итерацией и их вес, деление что в ASM что в C++ что в C# медленее умножения и побитовых сдвигов.

sam0delk1n пишет:
Что касается машинного, там ковыряние в битах и байтах также может только ухудшить скорость. Операции нужно выполнять с регистром целиком. Преобразование в байты и обратно будет добавлять лишние операции.
А вот тут как раз идет помощь управляемому коду, если множить два 32 битных числа он их преобразует в 64 битные и результат дает соответствующий, что затем опять придется преобразовывать в 32 битный. А если умножать 32 битное на 8 битное то результат идет как 32 битный. По этой причине преобразование в байт тут позволяет избежать куда большего числа преобразований и что важнее с более медленной на х86 работай с 64 битными числами.


sam0delk1n пишет:
Кстати правильно заметили что освещение и наложение текстур -- схожие операции. На самом деле лучше сводить это в одну операцию и экономить вычисления. В Вашем случае ограничением может служить техника наложения текстур, когда (если я правильно понял) сразу строка или столбец масштабируется под размер тайла и рисуется. Было бы удобнее получать цвет текселя (делать выборку) для соответствующего пикселя на этапе интерполяции.
У меня так и происходит, это я в описании разделил для того чтобы не валить все в одну кучу а объяснить обо всем по отдельности. А рисовка да идет по столбцам, больше скачек по памяти конечно, но меньше возни с интерполяцией.

sam0delk1n пишет:
Ещё, источник света тоже может иметь цвет, а не только быть белым.
Да я с этим тоже игрался по сути просто тогда разные компоненты, собственно я об этом писал -
ca = min( max(0f, ca • I • Ia), 1f), где ca составляющая компонент R,G,B цвета пикселя от 0 до 1, а Ia — составляющая компонента цвета источника света. В случае бесцветного цвета Ia = 1.
Т.е. в случае цветного освещения просто 3 значения интенсивности будут. Но так как я рендить буду в буффер, а спрайты вообще будут идти без расчета осветления, амбиет будет обратываться отдельно при финальном рендере (сборки 3х экранных буферов и буфферов GUI). Более интересный эффект что я пытался получить это не равномерный цвет, т.е. к примеру чтобы красным отдавали только засвечиваемые участки поверхности. Это бы создавало эффект солнечного цвета при этом не перекрашивая весь экран. В общем тоже самое только там уже 4 компонента цвета (1 общая для горизонтальных и затемненных поверхностей и 3 для засветленных). Но это громадуха с ветвлениями так что пока задвинул в ящик, когда оно будет ближе к релизу вернусь.




sam0delk1n пишет:
Вот Ваша исходная формула: I = Kd * Ip * (N * L) + Ka * Ia.
Ну это я писал в общем виде, что бы было понятно откуда возникла моя ересь, а сам я все сильно упростил, по факту Ia у меня тоже что и Ip вышло. Насчет векторов тоже не все так просто это на видеокарте цвета от 0 до 1, а тут от 0 до 31 или 255 взависимости от битности, т.е. от проверок границ не уйти, т.к. как после умножение может получить 256, в случае если цвета от 0 до 1 то тут физически выше 1 не выйдет.
Game isn't a dream, it is the reality, reality which is coming while we dream...
Тема заблокирована.
Спасибо сказали: sam0delk1n

Новости с фронта 28.03.2016 00:37 #7253

  • sam0delk1n
  • sam0delk1n аватар
  • Вне сайта
  • Интересующийся
  • Сообщений: 76
  • Спасибо получено: 47
  • Репутация: 24
StaticZ пишет:
У вас какое-то мистическое представление об управляемом коде.
Я рассуждаю так: в компиляторах нативного кода указывается конкретная платформа. Это значит что будут использоваться реально существующие команды процессора. В C# и Java насколько я знаю платформо-независимый байт-код. А значит есть вероятность что машина часть своих команд начнёт эмулировать другими, которыми располагает целевая платформа. В принципе разработчики компилятора должны знать какие наборы команд виртуальной машины будут более эффективно выполняться на той или иной целевой платформе, поэтому я бы положился на их решение. С другой стороны если бы я совсем не понимал принципы работы реального железа, то я бы точно также положился на решение компилятора нативного кода. Но всё таки раз факт того что целевая платформа известна, принимать то или иное решение проще, чем когда платформа не известна.

Пока буду следить за проектом дальше, вроде интересно.
Тема заблокирована.
Спасибо сказали: StaticZ

Новости с фронта 28.03.2016 16:20 #7254

  • StaticZ
  • StaticZ аватар
  • Вне сайта
  • Разработчик
  • Демиург
  • Сообщений: 292
  • Спасибо получено: 142
  • Репутация: 68
sam0delk1n пишет:
Я рассуждаю так: в компиляторах нативного кода указывается конкретная платформа. Это значит что будут использоваться реально существующие команды процессора.
Ога, однако эффективность конкретного кода зависит от конкретной модели процессора. Таким образом идеальный код можно написать лишь на ассемблере под конкретный процессор и конкретную начинку.

sam0delk1n пишет:
В C# и Java насколько я знаю платформо-независимый байт-код. А значит есть вероятность что машина часть своих команд начнёт эмулировать другими, которыми располагает целевая платформа. В принципе разработчики компилятора должны знать какие наборы команд виртуальной машины будут более эффективно выполняться на той или иной целевой платформе, поэтому я бы положился на их решение.
И? Не путайте преобразования управляемого кода в неуправляемый и оптимизацию. Виртуальная машина сама подбирает какие команды вызывать, что на какие регистры слать и тд и тп, но она ничего сама не сочиняет и не выдумывает. Ей дают псевдо код умножения двух чисел она смотрит на их размеры пихает их на подходящие регистры и вызывает нужную инструкцию. Я же пишу на C#, который компилятор сам конвертирует в IL, который уже конвертируется в машинный байт код во время выполнения. Я не пишу машинный код и я даже не пишу IL код, я лишь пишу код на C#. Что происходит далее??

допустим напишем код для нахождения суммы квадратов произвольного набора чисел, вариант 1 использующий все прелести управляемого языка:
public virtual int[] GetSqrSum(int[] data)
{
   if (data == null)
       throw ArgumentNullException("Параметр data не может быть равен null");
   foreach(vat d in data) {
     if (d < 0)
       throw Exception("Числа должны быть больше или равны нулю");
     yield (int)Math.Pow(d, 2f);
   } 
}

и его применяем для вычисления кучи сумм еще большей кучи каких-то данных, опять же в традиционном духе управляемого кода:
  ....
  for (int i = 0; i < 100000; ++i) {
    try {
      sums[i] = GetSqrSum(data[i].ToArray());
    } catch(Exception e) {
      sums[i] = -1;
    }
  }
  ...


Что мы получим на выходе?? В случае если это код разовый и его скорость не критична то тут конечно универсальность, надежность и простота стоят потерь производительности, но если нам нужна скорость то это УЖАС. Я не буду писать IL код или тем более возможный ассемблерный код, я лишь расскажу, что будет делать сгенерированный компилятором IL код. Ну про очевидные мелочи вроде .ToArray() упоминать даже не буду. Итак, во первых в случае если данные не верны идет генерация исключения, что в нашем случае обрабатывается, если это ситуация маловероятно это не так страшно (теряем в основном на if'aх), но если подобных исключений много, то быстродействие просядет в разы, т.к. при ее генерации идет считывание и обработка всего стека (а еще в добавок и рефлексия для получения имен, прототипов методов и прочего), чем выше вложенность нашего кода тем больше времени это займет. Затем обращение к элементу чего-то (data) по сути самим компилятором подменяется на if (i < 0 || i >= data.Length) throw new OutOfRangeException("блаблабла") else return data (управляемый код, он же безопасный, какраз за счет того что эти ренджчеки и другое пихаются где только можно), далее любимый foreach по факту создает объект IEnumerator<int>, при этом предварительно идет каст массива в IEnumerable<int>. Дальше больше как и для всех интерфейсов так и для выше лежащего виртуального метода (GetSqrSum) для получения адреса данных методов\свойств необходимо ломиться в таблицу виртуальных адресов, осуществлять там поиск указателя. Вместо просто инкремента мы получаем неявный вызов IEnumerator<int>.MoveNext() + для доступа к элементу коллекции конструкцию в виде IEnumerator<int>.Current. Далее смотрим, что у нас в цикле - вызов родного метода возведения в степень при этом идет каст int в double и потом обратный каст результата... В довершение всего это все упаковывается новомодным оператором yield, который по факту означает опять же создание коллекции IList, неявное добавление туда элементов с последующим кастом в массив.

Теперь перепишем это так:

public static unsafe int* GetSqrSum(int* data, int length)
{
   if (data == null || length == 0)
       return -1;
   int result = stacalloc int[length];
   do {
     if (*data < 0)
       return -1;
     *result++ = (int)(*data * *data);
     ++data;
   } while (--length != 0)
   return result;
}
  ....
  for (int i = 0; i < 100000; ++i) {
    *sums++ = GetSqrSum(data->IntPtr, data->Length);
    ++data;
  }
  ...

В результате мы на выходе получим IL код без лишних кастов, возни с виртуальными таблицами, рейндж чеков, насилие над стеком, кучи вызовов лишних методов и прочего. А теперь вопрос вы правда верите что чудо виртуальная машина сможет оптимизировать ассемблерный код первого варианта лучше чем второго? В втором варианте меньше действий, меньше операций, на выходе более легкий IL код. Да конечно тут можно гадать догадается ли виртуальная машина заменить "(int)Math.Pow(d, 2f);" на "(int)(d*d);" или нет, но лучше не верьте в магию если вы возьмете d = 0x7FFFFFFF то получите исключение с сообщением переполнения при возведение в кдвадрт где-то в глубине Math.Pow(), что намекает на то что виртуальная машина выполняет написанный код а не творит какую-то магию анализируя его и оптимизируя криворукий код. Впрочем оно и логично если посмотреть на время компиляции того же C++, что для серьезных проектов не мало времени занимает.


Кроме того код написанный под .Net2 требует только на .Net2 его не устроит ни .Net3 ни .Net4, ровно как и игра написанная под DX9, не запуститься на DX10 (вернее запуститься, но просто по факту инсталятор DX10 помимо самого DX10 включает в себя и предыдущие версии). Конечно есть патчи и SP но они не такие уж и частые и не ставят все вверх ногами, так что тут особо нет смысла напрягаться насчет различий виртуальной машины. Разве что кроме случая к примеру Mono на линуксе там да отличия могут быть существеннее, но всеравно пускай второй вариант и будет работать на 20% медленее, он попрежнему останется быстрее первого, за счет своей краткости.


PS А вообще кстати сейчас активно развивается компиляция управляемого C# непосредственно в машинный байт-код, так что если уж совсем смущает можно позамарачиваться с компиляцией прямо в машинный код.
Game isn't a dream, it is the reality, reality which is coming while we dream...
Последнее редактирование: 28.03.2016 16:26 от StaticZ.
Тема заблокирована.
Спасибо сказали: Samael, sam0delk1n

Новости с фронта 28.03.2016 23:16 #7255

  • sam0delk1n
  • sam0delk1n аватар
  • Вне сайта
  • Интересующийся
  • Сообщений: 76
  • Спасибо получено: 47
  • Репутация: 24
Ога, однако эффективность конкретного кода зависит от конкретной модели процессора. Таким образом идеальный код можно написать лишь на ассемблере под конкретный процессор и конкретную начинку.
Да. Но это не практично. Это слишком крайний случай. Я говорю о наборе команд x86 (и SSE), которые есть на всех x86-совместимых процессорах. Реализация этих команд в каждой отдельной модели процессора не сильно различается по скорости (особенно их RISC часть).
А теперь вопрос вы правда верите что чудо виртуальная машина сможет оптимизировать ассемблерный код первого варианта лучше чем второго?
Не, в первом варианте явно больше действий. Лишние действия (и сложные структуры/классы содержащие их) конечно надо убирать, но вот на отдельные байты и биты переходить не надо, количество действий увеличится, а регистры будут наполовину пустые. Я думаю код в стиле Си и оперирование переменными сопоставимыми с размером регистров будет лучшим вариантом.

Я имел ввиду что вот например есть цикл где складываются вектора. Виртуальная машина C# не имеет SSE регистры (допустим). Она сгенерит код где по очереди складывается каждый компонент. А если компилятору платформа известна то или компилятор или прогер сам сможет воспользоваться SSE и сделать код в 4 раза быстрее.

Вообще я не очень-то в машинах C# разбираюсь =), может там не так всё плохо. Просто очень много не очевидных вещей в них. Например какая-то часть системных или библиотечных функций могут быть нативно реализованы и прогер может пытаться писать свои велосипеды, но быстрее не получится. Или же например может оказаться что машина умеет распараллеливать циклы на SSE регистры, но прогер вместо общей формы записи начнёт явно указывать работу с байтами и машина сделает указанные операции на РОНах и опять же окажется медленней.

Возможно если получше разберусь с работой машины то будет более очевидно. Я видел много движков на яве, шарпе, питоне и прочих управляемых языках и они были крайне медлительны. Лучше подожду и посмотрю что у вас получится =)
Тема заблокирована.
Спасибо сказали: StaticZ

Новости с фронта 29.03.2016 01:41 #7256

  • StaticZ
  • StaticZ аватар
  • Вне сайта
  • Разработчик
  • Демиург
  • Сообщений: 292
  • Спасибо получено: 142
  • Репутация: 68
sam0delk1n пишет:
Да. Но это не практично. Это слишком крайний случай. Я говорю о наборе команд x86 (и SSE), которые есть на всех x86-совместимых процессорах. Реализация этих команд в каждой отдельной модели процессора не сильно различается по скорости (особенно их RISC часть).
Я к тому что идеальная оптимизация это абсурд и возможно лишь только под конкретную железку. По большому счету возможны два типа оптимизаций - низкоуровневая оптимизация, по факту заточка кода под специфику железки (ассемблер) и неявная оптимизация связанная с косвенным влиянием на генерируемый код компилятором и облегчения его веса. Вы не знаете что именно даст ассемблерный код, но вы знаете специфику платформы, языка, компилятора и эти знания применяете для оптимизации. К примеру вы знаете что вызов метода это код, что деление медленнее битовых сдвигов и этим пользуйтесь. В С++ вам легче предсказать машинный код, в C# нет, но это не мешает вам оптимизировать код, другое дело что возможно не так эффективно как в С++, но от этого данный процесс не является менее важным.

sam0delk1n пишет:
Не, в первом варианте явно больше действий. Лишние действия (и сложные структуры/классы содержащие их) конечно надо убирать, но вот на отдельные байты и биты переходить не надо, количество действий увеличится, а регистры будут наполовину пустые. Я думаю код в стиле Си и оперирование переменными сопоставимыми с размером регистров будет лучшим вариантом.
Я вам хотел показать сколько мусора несет за собой родной код на C#. И тут есть и свои замуты, так к примеру в С/С++ изза обратного порядка отображения в памяти байтов у чисел каст DWORD\WORD\BYTE по сути скорее формален и сообщает компилятору сколько байт брать из памяти. В управляемом языке каст это создание нового объекта на стеке, так что тут куда более желательно избегать кастов в критичных местах.


sam0delk1n пишет:
Я имел ввиду что вот например есть цикл где складываются вектора. Виртуальная машина C# не имеет SSE регистры (допустим). Она сгенерит код где по очереди складывается каждый компонент. А если компилятору платформа известна то или компилятор или прогер сам сможет воспользоваться SSE и сделать код в 4 раза быстрее.
Не могу сказать точно но насколько я знаю в данный момент CLR не использует SSE для тяжелой оптимизации аля распаралеливание. Есть правда реализация Vector в проекте SIMD входящем в состав BCL, что добавляет в частности тип Vector и работу с ними реализуемую с использованием SSE2. Но мне не шибко важная скорость векторов, к тому же для потенциальной кросплатформенной совместимости не очень горю желанием пока тащить сторонии библиотеки. Мне и так предстоит эпичный квест с сборкой под андройды )) В любом случае подобные места я тестирую и оставляю наиболее оптимальный вариант.


sam0delk1n пишет:
Возможно если получше разберусь с работой машины то будет более очевидно. Я видел много движков на яве, шарпе, питоне и прочих управляемых языках и они были крайне медлительны. Лучше подожду и посмотрю что у вас получится =)
На самом деле .Net достаточно сильно отличается от явы, питона и прочего. Они являются интерпретируемыми языками, т.е. запуская их мы по сути запускаем VM что грузит данный код и начинает его интерпретировать по ходу его работы, грубо говоря считывает код, парсит, транслирует его в какой-то набор комманд. .Net - скорее ближе к транслятору чем интерпритаторам, собственно везде подчеркивается тот факт что CLR не является интерпритатором. Запуская .Net идет загрузка mscoree.dll, которая уже отвечает за JIT-компиляцию байт кода из PE и запуск скомпилированного кода (таким образом в принципе можно даже вызывать непосредственно .Net код из неуправляемого). Причем CLR непосредственно на лету компилирует код в машинный, и только 1 раз, т.е. при повторном вызове скомпилированного кода уже будет идти непосредственный вызов машинного кода, а не разборка манаджмент кода. Именно по этому IL так близок к ассемблеру. Помимо прочего CLR параллельно занимается оптимизацией доступа и хранения кода, например размещая куски одновременно вызываемого кода, она может переместить их в памяти так, чтобы они располагались внутри одной группы страниц памяти, тем самым сводя к минимуму количество случаев непопадания на страницу и повышая эффективность кэша кода при выполнении приложения. Помимо прочего CLR для тяжелых кусков со временем проводит перекомпиляцию с оптимизацией. Таким образом .Net становиться посредине между C++ и интерпретируемыми языками (на которых кстати почему-то помешанны больше всего сишные прогеры, тот же питон я как правило встречал лишь в проектах на С++, про LUA я уж молчу, честно говоря я вообще не понимаю любви к нему как по мне даже на С\С++ "скриптить" удобнее, разве что только возможность запихнуть "компиляцию" скриптов в редактор, правда с другой стороны хватает проектов что на лету компилирует тот же С). И не смотря на свою среднюю позицию простые бенчмарки в духе С++ vs С# дают очень не внятные результаты из-за чего местами разгораются понастоящему эпические срачи на вечную тему холивара. Я вовсе не берусь утверждать, что C# также шустр как и С++, в случае низкоуровневого кода C++ конечно уделает C#, но в случае всяких бустов и классового засилия CLR работает также как и С++. Так что честно говоря не знаю где вы видели такие ужасные движки, тот же презираемый всеми Unity хоть и не идеал, но не так уж и плох. Есть много примеров и приличных игр на C# в том числе и профессиональных. Конечно очевидно что если цель создать новый крайзес C# не вариант, но в остальном годно. А вот java испокон веков славилась своей тупизной, даже обычные GUI приложения зачастую тормазнутые по не балуй. Хотя есть и на нем не плохие игры, куда проще правда. Вообще как по мне единственный плюс явы это кросплатформеность - JVM как вирус успела заразить все ос и платформы начиная от древних сотовых телефонов.
Game isn't a dream, it is the reality, reality which is coming while we dream...
Тема заблокирована.
Спасибо сказали: Samael, sam0delk1n

Новости с фронта 29.03.2016 15:12 #7257

  • sam0delk1n
  • sam0delk1n аватар
  • Вне сайта
  • Интересующийся
  • Сообщений: 76
  • Спасибо получено: 47
  • Репутация: 24
StaticZ пишет:
И тут есть и свои замуты, так к примеру в С/С++ изза обратного порядка отображения в памяти байтов у чисел каст DWORD\WORD\BYTE по сути скорее формален и сообщает компилятору сколько байт брать из памяти.
Это скорее к С относится. В С++ более конкретные касты, например static_cast делает каст на этапе компиляции, чтобы в ассемблере его избежать. dynamic_cast делает RTTI что считается медленной операцией в критических участках кода. const_cast это установка/снятие квалификатора const для переменных и указателей. reiterpret_cast это как раз аналог скобок С.

А обратный порядок байтов для всей х86/64 работает, а в С# как, на лету переворачивает или тоже обратный?

StaticZ пишет:
В управляемом языке каст это создание нового объекта на стеке, так что тут куда более желательно избегать кастов в критичных местах.
В Вашем коде их довольно много =).

StaticZ пишет:
Так что честно говоря не знаю где вы видели такие ужасные движки, тот же презираемый всеми Unity хоть и не идеал, но не так уж и плох.
Скорее даже так: ужасными их делают пользователи. Я считаю что управляемые языки в таких движках это как скрипты, нужно писать только управляющий код (геймплей, сценарии, порядок работы и т.п.), делегируя основную нагрузку на вызовы функций движка, которые имеют нативную реализацию. Но пользователи начинают писать на них всё подряд, хотя могли бы вынести критические участки в DLL на C/С++ и подключить её. Опять же к разговору о простоте использования чужих движков: чтобы их верно применять надо понимать как они работают, а это не всегда проще написания своего. Поэтому выбор чужого может быть по причине стандартизации, поддержки со стороны его разработчиков, широкого применения, отлаженности, но никак не попытка упростить разработку ссылаясь на то что зачем делать то что можно бесплатно скачать из инета.

Я сторонник того что для каждого уровня и задачи свои языки и иногда лучше скомбинировать несколько чем пытаться применять один в неудобных для него местах. Например как у Unity3D можно поместить внутрь движка управляемую среду (упрощенную, только для игровых нужд) и писать игровой код, логику, интерфейсы и т.д. на управляемом языке, с простым синтаксисом и определённым уровнем автоматизации. Сам движок писать на гибком (типа С++) нативном языке, а отдельные критические участки, где нужна скорость, а не гибкость, на С (с ассемблерными вставками например). Причём не обязательно кроссплатформенный код, если целевых платформ 2-3, а не 10, можно создать специальные версии для каждой, а вот игровой код на управляемом языке будет работать кроссплатформенно. Вот я примерно так вижу устройство удобного движка. Ещё кстати что касается функционала и обработки ресурсов, я считаю чем больше это вынесено в тулзы и редакторы, тем лучше. Сам движок должен быть как плеер, только проигрывать подготовленные данные, и никаких встроенных туда редакторов и прочего мусора, например как это в CryEngine или UnrealEngine я считаю лишним. Редактор по идее действительно может быть собран на основе движка, но это должна быть отдельная программа, не предназначенная для игры.
Тема заблокирована.
Спасибо сказали: Samael, StaticZ
Время создания страницы: 0.230 секунд
полузаброшенный сайтСветлая зона и Академия РПГ Мейкераkn4kn5Плагины для RPG MakerДневник одной нэкоkn Топ Разработка игр