Pull to refresh

Comments 58

Вот именно такая графика была в Героях Меча и Магии III
За исключением того, что там в игре рендеринг был не трёхмерный :)

И вообще там был православный пиксель арт)

Короче, там все было совершенно по другому, но, вцелом, REPISOT прав))
UFO just landed and posted this here
Спрашиваю, потому что не в курсе, как вообще получить такое состояние экрана в оригинальной игре. Это отдельный режим, или какая-то модификация, или отдельная игра на тех же ресурсах?
Это абсолютно точно не оригинальная игра, и не hota. Больше всего тут даже не вид смущает, а количество стеков, их не более 7 должно быть. Могу предположить что это неофициальное дополнение WOG с очень кастомными настройками.
Больше всего смущает количество героев, как верно заметил комментатор ниже. В WoG я тоже подобного не видел, да и к тому же, как это реализовать в обычной игре, учитывая, что герои путешествуют в одиночку?
Но тут же 2+2 армии на одном экране. Что за модификация?
Я подозреваю, что человек просто нашел скрин в гугле и сам не знает. Если сделать поиск по картинке, то можно узнать, что это "HeroesLand, или Герои меча и магии 3 онлайн"

С интересом читаю ваши статьи. Особенно про трассировку лучей.
Спасибо Вам за Вашу работу. Жду следующих Ваших публикаций.
С интересом читаю ваши статьи. Особенно про трассировку лучей.
Спасибо Вам за Вашу работу. Жду следующих Ваших публикаций.
Спасибо!
Вот бы такое про «звуковую оптику». Посмотрел на всякий случай, да зачитался:)
Жаль у вас про звук только совсем немного было про робота.
Толи я плохо ищу, толи в русскоязычном сегменте не хватает подобных материалов.
Проще изучить английский, чем обойтись материалами доступными на русском.
И это наверное не плохо — если все будут пользоваться одним языком, информация для всех будет доступна максимально целостной. Хотя это, конечно, не значит, что не нужно производить контент на иных языках в принципе.
Проблема скорее в удобстве понимания, так как при написании материалов на русском будут пытаться адаптировать под русскоговорящего человека, потому что не всегда ему понятно то, то написал американец. Сам иногда сталкиваюсь с таким, вроде и понимаешь о чём автор, но всё же не до конца, а если бы было такое же или пусть даже немного хуже, но на русском, то, с большой вероятностью, автор русской версии объяснил бы понятнее.
Особенно такие вещи заметны тогда, когда только начинаешь изучать что-то, так как нужно часто разбираться в мелких аспектах, которые, к тому же, чертовски важны.
Если у вас есть под рукой, то я хочу пример.
К сожалению, с ходу не могу дать пример, так как уже давненько не сталкивался, но помню, что были такие проблемы и неудобств они доставили нормально.
Жаль, потому что мой опыт ровно обратный, и мне интересно сравнить.
Даже не знаю что и добавить)) Учитывая что я «долбанный» верстальщик, для меня это нечто грандиозное!)) Всегда мечтал работать с графикой.
Не бывает долбаных верстальщиков. Бывают люди, которым интересно ковырять что-то помимо их основной работы/учёбы, и люди, которым неинтересно.
Я не коим образом не хотел обделить верстальщиков, но всё же по факту вёрстка сайтов дело куда более проще чем графика, 3d или разработка тех же игр.
А свободное время можно уделять на другие занятия отличные от коддинга — спорт, творчество и тд.

Оно все кажется сложным, если наблюдать, а не щупать.

Ну начнём с того, что на этом сайте вряд ли много людей, которые рисуют или бегают, и при этом совсем не занимаются техническими хобби. А если продолжать, то технические хобби повышают уровень подготовки, что напрямую сказывается на карьерном росте.
Вот меня интересует вопрос, а можно ли материал одной текстурой задавать? Создать, допустим, 2-3 сотни различных материалов и каждому свой цвет определить. На каждый цвет/материал свои шейдеры, ну или местами один и тот же с разными переменными. А то сейчас по 6 текстур на объект в играх делают, никакой памяти не хватит.
Так а ведь шесть текстур на объект — это и есть материал, заданный в текстуре (ну, если не считать карт нормалей всяких). Мы же задаём материал вообще каждой точке нашего объекта. Тут у меня капелька пота, здесь сажей испачкано, и т.п.

1) Я сильно сомневаюсь, что будет какой-то выигрыш в создании отдельной библиотеки материалов, которая покрывает весь игровой мир.

2) Ну аж как художники взвоют (и совершенно справедливо)! Сделать нормальный интерфейс работы с этой билиотекой просто нереально.
На самом деле, мегатекстуры по факту ничем не отличаются от использования обычных мелких текстур. Вопрос просто упаковки этого дела в памяти.
Если правильно понял про «цвет=материал», то сейчас в deferred подходах и так используется условно говоря «цветовое» кодирование материалов в буфере кадра для последующей обработки. Только в конце концов все равно же понядобятся эти «шесть текстур», ведь PBR шейдинг должен откуда-то брать данные о фактуре, цвете поверхности, физических свойствах. Да и более простые модели освещения все одно захотят несколько текстур.

Совсем уж альтернатива — это только процедурная генерация текстур.
Где то я видел похожую статью, но в упрощенном виде. Попробовал — посмотрел на взыв в 16-17 FPS и забыл, только академический интерес.
«Графоний», конечно, у Вас всегда отменный. Настоящая магия программирования. Всё никак не найду в себе сил допройти (на практике) хоть одну из статей.
Но вот в этой генерации взрыва с точки зрения реализма смущает формирование по периферии клубов дыма оторванных от источника. Нечто подобное, конечно, можно наблюдать при мощных взрывах, например на испытаниях ядерных бомб — облака конденсируются в области перед ударной волной, но там совсем иной масштаб и механизм. Есть ли методы исправления этого недостатка? Может быть, достаточно использовать растущую сферу заполненную трёхмерным шумом Перлина?
Да, это проблема, и её нетрудно решить, сгенерировав нужный шум. Я решать не стал, так как обычно взрывы быстрые (в отличие от моей анимации), и этих артефактов просто не будет видно.
Но взрывы приятно скриншотить… А то раз быстрые, то можно и двумерными анимациями обойтись тогда.
А вообще мне кажется должна быть какая-то техника для моделирования физически корректных взрывов, подобно тому как ray tracing неплохо моделирует распространение света до учёта квантовых и гравитационных эффектов (хотя наверняка есть и такие модификации).
Вообще статья отличная, но в некоторых местах (особенно в либе) качество кода оставляет желать лучшего, что к сожалению затрудняет понимание
Порт на c#: tinykaboom in c#. Считает немного долговато (порядка 30 сек на моей машине).
Параметр времени в анимации — это радиус увеличивающейся ограничивающей сферы?

Отлично! А теперь надо добавить фоновую (сферическую) картинку и сделать центр взрыва прозрачным.

Вы про какой параметр времени, я не очень понял?
Ну, чтобы кадры получать. Каждый кадр анимации — это ведь расчёт с каким-то значением параметра. Что мы тут изменяем, чтобы происходило движение во времени?
Я изменял радиус сферы и размер шума, т.к. он абсолютный.
Не совсем ясно со сферической картинкой. Это картинка размером 2 π × π. Я должен «вырезать» из неё сегмент, который пробегает луч, формируя изображение?
Да, у нас есть картинка размером 2 π × π. Если у вас луч промахнулся мимо сцены, то сейчас вы просто рисуете пиксель постоянного цвета. А ведь ничто не мешает продолжить луч и пересечь его со сферой, например, радиуса 1000, которая описывает всю сцену. Получить точку (x,y,z) пересечения, превратить её в долготу и широту (φ, θ), которые сами живут в [0, 2π] ×[0, π], и вместо постоянного взять цвет из картинки.

Сразу скажу про маленькую тонкость, я регулярно забываю, что у нас верх — это игрек, а не зед. Надо не ошибиться в формуле сферических координат :)
Я добавил фон при помощи магических констант, взяв исходные известные углы для «окна» и наложив их на сферическую картинку. Я думал, что не важно какой имеет радиус внешняя сфера, т.к. с его изменением область проекции нашего изображения будет постоянной относительно всей площади сферы. Так?

А прозрачность пока не знаю как делать.

Да, радиус абсолютно неважен, главное, чтобы он был больше сцены. А прозрачность очень просто: на данный момент вы считаете фоновый пиксель тогда, когда не пересеклись со взрывом, так? А теперь и тогда, когда пересеклись со взрывом, тоже считайте цвет фонового пикселя. И складывайте их цвета: для начала просто к цвету взрыва добавляйте цвет фона, помноженный на коэффициент прозрачности, который просто равен уровню шума в данном пикселе. Так вы получите серые части взрыва непрозрачными, и красно-жёлтые прозрачными.
Что-то вроде этого получается:

framebuffer[ i + j * width ] = ( framebuffer[ i + j * width ] * noise + palette_fire( noise ) ) * lightIntensity;


Ну вообще красота! Теперь добавить ещё объектов (для начала просто пару взрывов), потом просто шахматную доску снизу, и чтобы не-взрывы освещались бы в зависмости от расстояния до центра взрыва. Потом можно будет добавить тени от непрозрачных частей. В общем, простор для фантазии широчайший, и всё стоит десяток строк кода…

Кстати, а что изменилось, что фон стал другим по сравнению с предыдущей картинкой?
Это центр сферической картинки, в отличие от предыдущего варианта. У меня в коде есть коэффициенты масштабирования (магические константы). Я их слегка меняю, чтобы по вертикали и горизонтали масштаб совпадал более менее.
Я не очень понял про магические константы. Скажите, ваш код сильно отличается от вот этого (см. последние семь строчек моего комментария)?
Ну, вообще, отличается. Я не использую значения высоты и ширины сферической картинки в расчётах, т.к. считаю в радианах. Ширина — 2 π, а — высота — π. Далее нужно посчитать углы для нашего окна и соотнести их с размерами в радианах, так получим коэффициенты масштабирования по направлениям. Заполняя буфер фоном мы берём цвета из сферического изображения с учётом этих коэффициентов. Я всё это делал интуитивно, поэтому получилось с точностью до каких-то множителей (нужно дополнительно посидеть, поразбираться). Начало отсчёта взял от центра сферической картинки.
На последней картинке итак видно, что она не совсем соответствует проекции, которая должна быть, но мне вид с Солнцем понравился. А должен быть угол π / 3 по высоте (fov).
Методом гугления и издевательств над кодом реймаршинга в Lua, получил вот такую картинку:image
Теперь хотелось бы допилить этот код до рендеринга сцены в равноугольную панораму. Осталось только код для камеры нагуглить.
Картинка отличная, выкладывайте код!
И давайте ссылку на определение нужной вам проекции.
Код сцены
--$Name:Рейтрейсер$
--$Info:порт рейтрейсера с pico-8$
--$Author:Kerbal$
--$Version:0.1$

require "sprite"
require "timer"
require "click"

local sqrt = math.sqrt
local flr = math.floor
local min = math.min
local max = math.max
local rnd = math.random
local abs = math.abs

local cam_shift_x = 0.1
local cam_shift_y = 0.1

local r_g_b = {
[0] = {0,0,0},
	{29, 43, 83},
	{126, 37, 83},
	{0, 135, 81},
	{171, 82, 54},
	{95, 87, 79},
	{194, 195, 199},
	{255, 241, 232},
	{255, 0, 77},
	{255, 163, 0},
	{255, 236, 39},
	{0, 228, 54},
	{41, 173, 255},
	{131, 118, 156},
	{255, 119, 168},
	{255, 204, 170},
	{199, 240, 216}, --NOKIA SCREEN
	{67, 82, 61}, --NOKIA INK 
	{0,0,0},
	{0,0,0},
	{0,0,0},
}
--[[


local colorGradients = {
  sky = {16, 16, 16, 16},
  green = {17, 16, 16, 17},
  red = {17, 16, 17},
}
]]


local colorGradients = {
  red = {1, 2, 8, 9, 10, 7},
  green = {0, 1, 5, 3, 11},
  sky = {15, 12, 12, 1},
  nokia1 = {16, 16, 16, 16},
  nokia2 = {17, 16, 16, 17},
  nokia3 = {17, 16, 17},
}
--math
--local function length(x, y, z) return sqrt(x*x + y*y + z*z) end
local function length(x, y, z)
  local y = y or 0
  local z = z or 0
  return (x*x + y*y + z*z)^0.5
end

local function norm(x, y, z)
  local y = y or 0
  local z = z or 0
  local l = length(x,y,z)
  return x/l, y/l, z/l
end

local function dot(xa, ya, za, xb, yb, zb)
  local za = za or 0
  local zb = zb or 0
  return xa*xb + ya*yb + za*zb
end

local function dot2(xa, ya, xb, yb)
  return xa*xb + ya*yb
end

--globals
local ex, ey, ez = 0, 1, -1.5 --camera position
local fov = 45 --camera FOV
local tmin, tmax = .1, 200 --minimum and maximum distance from camera
local maxSteps = 100 --maximum number of steps to take
local lx, ly, lz = norm(.2, .5, -.6) --light direction

--distance field functions

----------Shapes

local function sdCircle(x, y, r) 
  return length(x, y) - r
end

local function sdBox(x, y, w, h)
    local n, m = abs(x)-w, abs(y)-h
    return length(max(max(n,0),max(m,0))) + min(max(n,m),0.0);
end

----------Shapes

----------Primitives

local function plane(x, y, z) return y end

local function cube(x, y, z, side)
  return max(abs(x), abs(y), abs(z)) - side / 2
end

local function box(x, y, z, a, b, c)
  local l, n, k = abs(x) - a,  abs(y) - b, abs(z) - c
  return length(max(l, n, k, 0.0)) + min(max(l,max(n, k)),0.0)
end

--[[
local function rbox(x, y, z, a, b, c, r)
  local l, n, k = abs(x) - a - r,  abs(y) - b - r, abs(z) - c - r
  return length(max(l, n, k, 0.0)) + min(max(l,max(n, k)),0.0)
end
]]

local function sphere(x, y, z, radius)
  local manhattanDistance = x + y + z
  --this if is to avoid errors due to pico's limited number range
  if manhattanDistance > radius * 2 then return manhattanDistance end
  return length(x, y, z) - radius
end

local function cone(x, y, z, h, r)
  local h, r = norm(h, r)
  local q = length(x, z)
  return dot2(h, r, q, y)
end

local function torus(x, y, z, rring, r)
  return (((x^2 + y^2)^0.5 - rring)^2 + z^2)^0.5 - r
end

local function torus2(x, y, z, rring, r)
  return (((y^2 + z^2)^0.5 - rring)^2 + x^2)^0.5 - r
end

local function capped_cylinder(x,y,z, h, r)
  local a, b = abs(length(y,z)) - h, abs(x) - r
  return min(max(a, b), 0.0) + length(max(a, b, 0.0))
end

local function sdTriPrism(x, y, z, w, h)
  local a,b,c = abs(x), abs(y), abs(z)
  return max(c - h, max(a * 0.866025 + y * 0.5, -y) - w * 0.5)
end

----------Union, Subtraction, Intersection

local function op_union (a, b)
  return min(a, b)
end

local function op_substraction (a, b)
  return max(-a, b)
end

local function op_intersection (a, b)
  return max(a, b)
end

local function cutbox(x, y, z, h)
  return max(-sphere(x, y, z, h*.7), box(x, y, z, h))
end

local function cutsphere(x, y, z, h)
  return max(sphere(x, y, z, h*.75), box(x, y, z, h))
end

--union combines two distance functions
local function union(a, am, b, bm) if a < b then return a, am else return b, bm end end


--scene defines the total distance function
local function scene(x, y, z)
  local d, m = tmax, 0 --max distance is skyk
  d, m = union(d, m, plane(x, y, z), "green")
--  d, m = union(d, m, cube(x-2, y-2, z-1, 1), "green")
  d, m = union(d, m, box(x-2, y-2, z-1, .1, .5, .5), "green")
  d, m = union(d, m, sdBox(x, z - 3, .2, .2), "green")
  d, m = union(d, m, sdBox(x, z, .2, .2), "green")
  d, m = union(d, m, sdBox(x - 3, z - 3, .2, .2), "green")
  d, m = union(d, m, sdBox(x - 3, z, .2, .2), "green")
  d, m = union(d, m, sdCircle(x - 3, y, .1), "red")
  d, m = union(d, m, sdCircle(y, z, .1), "red")
--  d, m = union(d, m, cone(x-4, y-2, z, 0.01, 0.01), "green")
  d, m = union(d, m, torus( z-1, x-2, y-1, 1.5, .1), "green")
--  d, m = union(d, m, torus2(x-3, y-1, z-2, 2, .1), "green")
  d, m = union(d, m, sphere(x, y-1, z, .5), "red")
  d, m = union(d, m, sdTriPrism(y-2, x-2, z, .3, .2), "red")
  d, m = union(d, m, sphere(x, y-.5, z, .3), "red")
  d, m = union(d, m, capped_cylinder(x-2, y-1, z, .2, .2), "red" )
--  d, m = union(d, m, cutbox(x-2, y-1, z-1, 1), "red" )
  d, m = union(d, m, op_substraction(sphere(x-2, y-1, z-1, 0.7), box(x-2, y-1, z-1, .5, .5, .5)), "red" )
--  d, m = union(d, m, cutsphere(x-2, y-1, z-.75, 1), "cnob" )
  d, m = union(d, m, sphere(x, y-1.5, z, .3), "red")
  d, m = union(d, m, sphere(x-1, y-1, z-1, .3), "green")
return d,m
end
--calculates the normal at xyz
local function sceneNormal(x, y, z)
  local eps = 0.1
  local xa, xb = scene(x+eps, y, z), scene(x-eps ,y ,z)
  local ya, yb = scene(x, y+eps, z), scene(x, y-eps ,z)
  local za ,zb = scene(x, y, z+eps), scene(x, y, z-eps)
  return norm(xa-xb, ya-yb, za-zb)
end

--rendering

--returns smooth color based on position and gradient
local function dither(x, y, gradient, value)
  local whole = flr(value * #gradient)
  local fraction = value * #gradient - whole
  local low = gradient[min(whole + 1, #gradient)]
  local high = gradient[min(min(whole + 1, #gradient) + 1, #gradient)]
  if fraction < 1/7 then return low end
  if fraction < 2/7 then if (x+1)%3==0 and y%3==0 then return high else return low end end
  if fraction < 3/7 then if x%2==0 and y%2==0 then return high else return low end end
  if fraction < 4/7 then if (x%2==0 and y%2==0) or (x%2~=0 and y%2~=0) then return high else return low end end
  if fraction < 5/7 then if x%2==0 and (y+1)%2==0 then return low else return high end end
  if fraction < 6/7 then if (x+1)%3==0 and y%3==0 then return low else return high end end
  return high
end
local function getShadowPoint(t, x, y, z) return x + lx * t, y + ly * t, z + lz * t end
--computes the shoft shadow value
local function shadow(x,y,z)
  --t starts at 0.2 so the shadow ray doesn't intersect
  --the object it's trying to shadow
  local res, t, distance, sx, sy, sz = 1, 0.2, 0, 0, 0, 0
  for i = 1, 6 do
    sx, sy, sz = getShadowPoint(t, x, y, z)
    distance, _ = scene(sx, sy, sz) --we don't care about the color
    res = min(res, 2 * distance / t) --increase 2 to get sharper shadows
    t = t + min(max(distance, .02), .2)
    if distance < .05 or t > 10.0 then break end
  end
  return min(max(res, 0), 1)
end
--calculates the final lighting and color
local function render(x, y, t, tx, ty, tz, rx, ry, rz, color)
  local nx, ny, nz = sceneNormal(tx, ty, tz)
  local light = 0
  light = light + min(max(dot(nx, ny, nz, lx, ly, lz), 0), 1) --sun light
  light = light * shadow(tx,ty,tz) --shadow color
  light = min(max(light, 0), 1) --clamp final light value
  return dither(x, y, colorGradients[color], light)
end
--calculates the sky color
local function sky(x, y, rx, ry, rz)
  local altitude = (min(max(ry, 0), 1) ^ 1.5)
  return dither(x, y, colorGradients.sky, altitude)
end

local function nokia1(x, y, rx, ry, rz)
  local altitude = (min(max(ry, 0), 1) ^ 1.5)
  return dither(x, y, colorGradients.nokia1, altitude)
end


--tracing
--this is the heart of a ray tracer
--the for loop pushes the test point forward until
--it finds a surface that is close enough to render
--the forward direction is based off the xy of the screen and fov
local function getRayDirection(x, y) return norm(x / 64 - 1, (128 - y) / 64 - 1, 90 / fov) end
local function getTestPoint(t, rx, ry, rz) return ex + rx * t, ey + ry * t, ez + rz * t end
function trace(x,y)
  local rx, ry, rz = getRayDirection(x, y)
  local tx, ty, tz = 0, 0, 0
  local t, distance, color = 0, 0, 0
  for i = 1, maxSteps do
    tx, ty, tz = getTestPoint(t, rx, ry, rz)
    distance, color = scene(tx, ty, tz)
    --the test point is close enough, render
    if distance < .05 then return render(x, y, t, tx, ty, tz, rx, ry, rz, color) end
    --the test point is too far, give up, draw the sky
    if distance >= tmax then break end
    --move forward by some fraction
    t = t + distance * .7
  end
  return sky(x, y, rx, ry, rz)
--  return nokia1(x, y, rx, ry, rz)
end

--just here to get pico to work the way it should be defaut tbh

--function _init() cls() end
--function _update() end

--pick random points to trace, but only if they
--havent been traced before
--cache the expensive trace function, and set pixel
--local traced = {}
function _draw()
  for y = 1, 512 do
	for x = 1, 512 do
  
--    local x, y = flr(rnd(128)), flr(rnd(128))
    local i = x + y * 255
 --   if not traced[i] then
    
--      pxl:val(x, y, i, j, t)
--      print ("x ".."y "..trace(x,y))
		local k = trace(x,y)
--[[		print (r_g_b[k])
		print (trace(x,y))
		print (r_g_b[k][2])
		print (r_g_b[k][3] or "nope")
]]
		pxl:val(x,y, r_g_b[k][1], r_g_b[k][2], r_g_b[k][3])
 --     pset(x, y, trace(x,y))

      --traced[i] = true
--	  end
    end
  end
end

sprite.direct(true)

declare 'pxl' (false)

function game:timer()
--[[
   local x, y, i, j
   t = t + f
   v = v + r
   if t < 1 then f = 1 end
   if t > 254 then f = -1 end
   if v < 1 then r = 1 end
   if v > 254 then r = -1 end
   --   i = (x*y^0.5+t)%512
   for x = 0, 255 do
       for y = 0, 255 do
          i = (x^2-y^2+t^1.3)%255
          j = (x^2-y^2+v^1.3)%255
--          pxl:val(x, y, i, j, t)
          pxl:val(x, y, j, t, v)
       end
   end
]]  
  -- print (colorGradients["red"][3])
  -- table.insert(colorGradients["red"],colorGradients["red"][1])
  -- table.remove(colorGradients["red"],1)
   _draw() 
   pxl:draw_spr(sprite.scr())
   if ex > 1 or ex < -1 then cam_shift_x = cam_shift_x * -1 end
   if ey > 2 or ey < 0.3 then cam_shift_y = cam_shift_y * -1 end
   ex = ex + cam_shift_x
   ey = ey + cam_shift_y
--   fps = (1000/(instead.ticks() - old_ticks))
--   print (fps)
--   old_ticks = instead.ticks()
end

function start(load)
   pxl = pixels.new(512, 512)
   timer:set(10)
end


Для запуска кода используется движок Instead.
Код необходимо сохранить в файл с именем main3.lua
instead-hub.github.io

wiki.panotools.org/Equirectangular_Projection
en.wikipedia.org/wiki/Equirectangular_projection
Sign up to leave a comment.

Articles