Создание стерео картинки
Создание стерео картинки
Почти все видели стерео картинки, я, например, помню в школе у нас были тетрадки, на обложке которых были напечатаны непонятные каракули. Но если посмотреть на них «правильно» - эти каракули обретали объем. Не у всех получалось увидеть их сразу, кому это было просто, а кто так и не смог. Есть несколько рекомендаций как достигнуть иллюзию объема: смотреть «сквозь» картинку или приложить ее вплотную к носу и медленно отводить.
Немного теории.
Как человек воспринимает объем? Чем ближе расположен рассматриваемый объект, тем больше наблюдатель «сводит» глаза. Т.о. если рассматриваемый объект расположен на бесконечно удаленном расстоянии, то глаза будут смотреть на него «параллельно».
Рис.1
Теперь рассмотрим как достигнуть иллюзию объема на плоской картинке. Итак, у нас имеется человек с двумя глазами (рис.1): левый глаз O1 и правый O2,а так же плоскость проекции стерео изображения D1D2. Пусть рассматриваемый объект B расположен на расстоянии h от наблюдателя. Тогда для того чтоб «увидеть» его на картинке необходимо совпадение рисунка в точках B1 и B2. Получается мы держим перед собой плоский рисунок и смотрим как бы «сквозь» него. Левый глаз видит точку B1, правый B2, изображения для них на листе совпадают и создается иллюзия того, что на самом деле мы видим объект B, расположенный за плоскостью листа. Т.о. чем больше расстояние между точками B1 и B2,в которых совпадает изображение, тем «дальше» воспринимается объект B.
Теперь выведем пару расчетных формул, которые пригодятся для просчета стерео изображения. Пусть a – расстояние между глазами, H – расстояние до максимально удаленного объекта, h – расстояние до объекта h_max – расстояние от максимально удаленного объекта до плоскости проекции стерео изображения. Из подобия треугольников BB1B2 и BO1O2 следует B1B2/BE= O1O2/BF. B1B2=d , O1O2=a, BF=h, BE=BF-EF, EF=AF-AE=H-h_max, BE=h-(H-h_max). Итого d/(h-H+h_max))=a/h.
Пожалуй теории пока достаточно, можно перейти к практике. Для этого понадобится Photoshop и 3dsMAX. В Photoshop’е создадим текстуру, из которой в последствии будет построено стерео изображение. Несколько рекомендаций: чем больше на текстуре деталей, тем лучше получиться окончательный вариант (с однотонными вообще ничего не получиться); высота текстуры должна совпадать с высотой изображения (поэтому сразу надо определить размер финального стерео изображения); а ширина зависит от того, где вы собираетесь в дальнейшем использовать полученную картинку. На рис.1 величина d_max как раз соответствует ширине текстуры, поэтому в реальности (на экране монитора или после распечатки) она не должна превышать расстояния между глазами a. В тоже время чем больше пикселей по ширине будет в исходной текстуре, тем более качественным получиться окончательный результат. У меня получилась вот такая текстура:
Рис.2
Ширина ее 100 пикселей, а высота 600 (в итоге рассчитываю получить стерео изображение размером 800 на 600 пикселей). Хотя лучше сделать ее так, чтоб она тайлилась по горизонтали. Далее создаем или открывает любой 3D-объект в MAX’е. Я для этого использовал модель осы:
Для результата не пригодятся ни материалы, ни сторонние рендеры, ни источники света. Поэтому если все это имеется в сцене, то назначаем на все стандартный материал, удаляем все источники света и используем Default Scanline Renderer. Теперь можно приступить к написанию скрипта. Для этого в меню Max’а выбираем MAXScript->New Script и откроется окно редактирование скриптов. Наш скрипт будет иметь свой собственный интерфейс. Это можно реализовать двумя способами: на панели Utilites или в отдельном окне. Выберем первый вариант и создадим новую скрипт-утилиту:
utility stereoImg "Stereo Image"
-- утилита создания стерео изображения
(
)
Сохраним ее с названием stereoImg.ms и отправим ее на просчет. Для этого надо в меню редактора скриптов выбрать Tools->Evaluate All или нажать комбинацию клавиш Ctrl+E. После этого наша утилита приобрела свой интерфейс (рис.4) и единственное что можно с ней сделать – это открыть и закрыть.
Рис.4
Разберем что же мы сделали: utility – зарезервированное слово с помощью которого создаются новые скрипт-утилиты, stereoImg – имя переменной, с помощью которой можно осуществлять контроль над утилитой, “Stereo Image” – символьная строка, именно она отображается в списке утилит (рис.4-3), далее в круглых скобках идет тело утилиты. Более подробную информацию по созданию скрипт-утилит и MAXScript’у можно найти в файле помощи MAX’а (в основном меню Help->MAXScript Reference). Для добавления комментариев используется «--», после него компилятор игнорирует все символы до конца строки . Пора наполнить нашу утилиту кнопками и другими элементами пользовательского интерфейса. Будем ориентироваться на следующий вариант:
Рис.5
Итак, список необходимых элементов: две группы Texture и Render. В группе Texture имеется метка, в которой будет отображаться информация о текстуре, и кнопка, запускающая диалог открытия файла с текстурой. В группе Render – спиннер (определяет ширину рендера), метка (информация о высоте рендера), спиннер (качество стерео изображения), индикатор процесса, кнопка выбора объекта и «самая главная» кнопка запускающая скрипт на просчет. Для этого в тело утилиты надо добавить следующие строки:
fn fltr_cam obj = superClassOf obj == camera -- функция возвращает истину если obj камера (для фильтра выбора)
group "Texture:"
-- параметры текстуры
(
label l_T_img "no texture" -- информация о размере
button b_T_img "Load texture bitmap" -- загрузка текстуры
)
group "Render:"
-- параметры рендеринга
(
spinner s_R_w "width:" range:[0,1500,0] type:#integer width:82 across:2 -- ширина
label l_R_h "height:0" -- высота
spinner s_R_prec "quality of stereo image" range:[1,10,4] type:#integer width:80 align:#right -- качество стерео изображения
progressBar prb_R_status value:0 -- прогресс рендера
pickbutton pb_R_cam "Pick camera" filter:fltr_cam across:2 -- выбор камеры
button b_R_stereo "<GO>" -- запуск рендера
)
Здесь: group – зарезервированное слово для создание группы, после него следует название группы, и в круглых скобках элементы группы; label – для создания метки, потом название переменной для управления меткой и срока (выводимый на форму текст); button – для создания кнопки, потом название переменной для управления кнопкой и строка (отображается на кнопке). Это основная структура для создания элементов пользовательского интерфейса. Но некоторые элементы имеют так же дополнительные параметры. Для spinner’а – range:<точка в 3d-пространстве> (первая координата определяет нижний предел изменения значения, вторая – верхний, третья – начальное значение), type:#integer (тип значения – целое число, так же может принимать значение #float – вещественное число и #worldunits – число в размерности текущих координат MAX’а). Для progressBar – value:<первоначальное значение>. Для pickbutton – filter:<ссылка на функцию фильтра>, функция фильтра имеет один параметр – объект и возвращает true, если объект может быть выбран, и false в противном случае. Для определение такой функции используется зарезервированное слово fn потом идет название функции, параметр и после «=» логическое выражение, которое возвращает функция. Параметры width:, across: и align: могут использоваться при создании любых элементов и отвечают за компоновку элементов на форме: первый – ширина (если требуется отличная от создаваемой по умолчанию), второй – количество элементов в одной строке (по умолчанию каждый новый элемент создается в новой строке), третий – выравнивание.
В самом начале тела утилиты (перед описанием пользовательских элементов) определим локальные переменные, необходимые для создания стерео изображения:
-- определение локальных переменных
local T_img = undefined -- текстура
local width, height, d_max -- ширина и высота рендера, ширина текстуры
Осталось прописать обработчики событий от элементов пользовательского интерфейса. Описание всех событий должно быть следующим образом: on<имя элемента> <название события> <список аргументов>do(<обработка события>). Для начала опишем поведение MAX’а на нажатие кнопки загрузки текстуры. Что должно происходить: открыться окно диалога выбора файла, после выбора файла текстуры она загружается в MAX, выводиться на форму информация о размере текстуры и ее название, далее инициируются локальные переменные и настраиваются глобальные параметры рендера.
on b_T_img pressed do
-- загрузка текстуры
(
T_file_name = getOpenFileName types:"bitmap(*.bmp)|*.bmp" -- диалог открытия файла
if T_file_name != undefined then -- если все файл бы выбран
(
T_img = openBitMap (T_file_name) -- загрузка картинки
b_T_img.caption = filenameFromPath T_file_name -- на кнопку имя файла картинки
l_T_img.caption = "width: " + (T_img.width as String) + " height:" + (T_img.height as String) -- вывести информацию о картинке
d_max = T_img.width -- обновление всех локальных переменных
height = T_img.height
width = 4*height/3 as Integer -- определение ширины рендера так чтоб в итоге получились пропорции 3:4
s_R_w.value = width -- отображение информации о разрешении рендера
l_R_h.caption = "height:" + height as String
renderWidth = width -- настройка глобальных параметров рендера (ширина, высота)
renderHeight = height
)
)
Определим вид, который мы желаем получит на стерео изображении. Для этого нам понадобиться камера. Если в сцене уже имеются камеры с «нужным» видом – отлично, можно использовать их. Если нет, то необходимо создать камеру. Проще всего перейти к виду Perspective, где выбрать необходимый ракурс. После чего создать камеру из вида: в главном меню Create->Cameras->CreateCameraFromView или Crtl+C. Вспомним теорию, там были такие параметры как H – расстояние до максимально удаленного объекта и h_max – расстояние от плоскости проецирования до максимально удаленного объекта. Чтоб не вводить их вручную, будем брать их из свойства камеры, а именно из Clipping Planes. Выберем необходимую камеру, на панели Modify раскроем свиток Parameters, ставим галочку возле Clip Manually. Осталось только настроить Near Clip и Far Clip. Плоскость Near Clip соответствует плоскости проекции стерео изображения, а Far Clip – расстояние до максимально удаленного объекта. Рекомендации: объекты, по которым будет строиться стерео изображение, должна располагаться между Far Clip и Near Clip, Near Clip равно приблизительно половине Far Clip, объекты должны быть расположены подальше от Near Clip и вплотную к Far Clip. Хотя можно этим пренебречь и в дальнейшем экспериментировать с Clipping Planes, для получения более желаемого результата. Т.о. вид настроен, осталось «объяснить» скрипту, что мы будем работать с этой камерой, для этого напишем обработчик нажатия кнопки выбора камеры:
on pb_R_cam picked cam do
-- выбор камеры
(
pb_R_cam.caption = pb_R_cam.object.name -- на кнопку имя камеры
)
После этого ссылка на камеру содержится в pb_R_cam.object.Так же необходимо скрипту отреагировать на изменение пользователем ширины рендера:
on s_R_w changed val do
-- обработка изменения ширины рендера
(
width = s_R_w.value
renderWidth = width
)
Осталось обработать последнее событие – нажатие «самой главной» кнопки. Опять придется заняться теорией на основе полученной ранее формулы: d/(h-H+h_max))=a/h. Здесь a, H и h_max – констаны. Последние две получаються из Clipping Planes камеры. Определим a, для этого подставим вместо h величину H – крайний случай, когда объект находиться на максимальном расстоянии. Отсюда a=d_max*H/h_max. И последняя формула: d=a*(h-H+h_max)/h – по ней вычисляется расстояние между точками d, изображения в которых совпадают, для того чтоб создавалась иллюзия, что рассматриваемый объект расположен на расстоянии h от наблюдателя.
on b_R_stereo pressed do
-- просчет стерео изображения
(
-- рендер глубины
DOF_img = render camera:pb_R_cam.object outputwidth:width outputheight:height channels:#(#zDepth)
unDisplay DOF_img
-- создание битмапы для будущего стерео изображения
OUT_img = bitmap (width+d_max) height color:black
-- заполнение первой полосы текстурой
for x = 0 to (d_max-1)do
(
for y = 0 to (height-1) do
(
c =(getPixels T_img [x,y] 1)[1] -- берем пиксель из текстуры
setPixels OUT_img [x,y] #(c) -- и вставляем в будущее стерео изображение
)
prb_R_status.value =(100 * x / d_max)as integer -- обновление прогресса текущей операции
)
prb_R_status.value = 0
-- отрисовка стерео изображения
H = pb_R_cam.object.farclip -- расстояние от глаз до максимальной глубины
h_max = pb_R_cam.object.farclip - pb_R_cam.object.nearclip -- расстояние от плоскости проецирования до макс. глубины
a = d_max*H/h_max -- расстояние между глазами
-- построчное построение стерео изображения
for y = 0 to (height - 1)do
(
for x = 0 to (width - 1) do
(
-- берем из рендера глубины расстояние от камеры до ближайшей точки
dist = - (getChannel DOF_img [x,y] #zDepth)[1]
if dist > H then dist = H -- не должно быть больше макс. глубины
d =(a*(dist+h_max-H)/(dist)+.5)as integer -- вычисляем расстояние на котором должны совпадать пиксели стерео изображения
setPixels OUT_img [x+d_max,y](getPixels OUT_img [x+d_max-d,y] 1) -- и дубируем пиксели согласно этому расстоянию
)
prb_R_status.value =(100 * y / height) as integer
)
prb_R_status.value = 0
display OUT_img -- открываем окно с финальным стерео изображением
)
Рассмотрим подробнее принцып работы обработчика нажатия «самой главной» кнопки. В переменную DOF_img сохраняется ссылка на рендер из выбранной камеры. Параметр channels:<массив каналов> метода render указывает какие каналы необходимо отрендерить дополнительно, в нашем случае обязателен только один канал: #zDepth - расстояние от камеры до места пересечения объекта с лучом, выходящим из камеры и проходящим через заданную точку рендера h (в скрипте – это переменная dist). Метод unDisplay <ссылка на изображение> закрывает окно с изображением, если оно открыто, этот метод противоположен display <ссылка на изображение>. Затем создается новое изображение шириной большей, чем ширина рендера на ширину текстуры и слева заполняется полосой текстуры (рис.6).
Потом идет построчное сканирование канала глубины (с проверкой на то, чтоб расстояние не превысило Far Clip камеры). На основе этой величины h (в скрипте dist) и полученных ранее формул вычисляется расстояние между повторяющимися пикселами. Таким образом идет построение стерео изображения, попутно обновляется прогресс вычислений. И в конце концов на экран выводиться окончательный результат (рис.7).
Ниже приведен полный листинг скрипт-утилиты с некоторыми улучшениями:
utility stereoImg "Stereo Image"
-- утилита создания стерео изображения
(
-- определение локальных переменных
local T_img = undefined -- текстура
local width, height, d_max -- ширина и высота рендера, ширина текстуры
fn fltr_cam obj = superClassOf obj == camera -- функция возвращает истину если obj камера (для фильтра выбора)
group "Texture:"
-- параметры текстуры
(
label l_T_img "no texture" -- информация о размере
button b_T_img "Load texture bitmap" -- загрузка текстуры
)
group "Render:"
-- параметры рендеринга
(
spinner s_R_w "width:" range:[0,1500,0] type:#integer width:82 across:2 -- ширина
label l_R_h "height:0" -- высота
spinner s_R_prec "quality of stereo image" range:[1,10,4] type:#integer width:80 align:#right -- качество стерео изображения
progressBar prb_R_status value:0 -- прогресс рендера
pickbutton pb_R_cam "Pick camera" filter:fltr_cam across:2 -- выбор камеры
button b_R_stereo "<GO>" -- запуск рендера
)
on b_T_img pressed do
-- загрузка текстуры
(
T_file_name = getOpenFileName types:"bitmap(*.bmp)|*.bmp" -- диалог открытия файла
if T_file_name != undefined then -- если все файл бы выбран
(
T_img = openBitMap (T_file_name) -- загрузка картинки
b_T_img.caption = filenameFromPath T_file_name -- на кнопку имя файла картинки
l_T_img.caption = "width: " + (T_img.width as String) + " height:" + (T_img.height as String) -- вывести информацию о картинке
d_max = T_img.width -- обновление всех локальных переменных
height = T_img.height
width = 4*height/3 as Integer -- определение ширины рендера так чтоб в итоге получились пропорции 3:4
s_R_w.value = width -- отображение информации о разрешении рендера
l_R_h.caption = "height:" + height as String
renderWidth = width -- настройка глобальных параметров рендера (ширина, высота)
renderHeight = height
)
)
on pb_R_cam picked cam do
-- выбор камеры
(
pb_R_cam.caption = pb_R_cam.object.name -- на кнопку имя камеры
)
on s_R_w changed val do
-- обработка изменения ширины рендера
(
width = s_R_w.value
renderWidth = width
)
on b_R_stereo pressed do
-- просчет стерео изображения
(
if (height == undefined) or (width == 0) then
(
messageBox "No texture or invalid size"
return 0
)
if pb_R_cam.object == undefined then
(
messageBox "Pick camera first"
return 0
)
-- рендер глубины
DOF_img = render camera:pb_R_cam.object outputwidth:width outputheight:height channels:#(#zDepth)
unDisplay DOF_img
-- создание битмапы для будущего "расширенного" стерео изображения
-- причем ширина умножается на точность
prec = s_R_prec.value -- точность (коэф. качества)
OUT_img = bitmap ((width+d_max)*prec) height color:black
-- заполнение первой полосы текстурой
for x = 0 to (d_max-1)do
(
for y = 0 to (height-1) do
for i = 0 to (prec-1) do
(
c1 =(getPixels T_img [x,y] 1)[1] -- берем пиксель из текстуры
c2 = (getPixels T_img [x+1,y] 1)[1]
if c2 == undefined then c2 = (getPixels T_img [0,y] 1)[1]
c = c1*(1.*(prec-i)/prec)+ c2*(1.*i/prec) -- линейно сглаживаем растяжение
setPixels OUT_img [x*prec+i,y] #(c) -- и вставляем в будущее стерео изображение
)
prb_R_status.value =(100 * x / d_max)as integer -- обновление прогресса текущей операции
)
prb_R_status.value = 0
-- отрисовка "расширенного" стерео изображения
H = pb_R_cam.object.farclip -- расстояние от глаз до максимальной глубины
h_max = pb_R_cam.object.farclip - pb_R_cam.object.nearclip -- расстояние от плоскости проецирования до макс. глубины
a = d_max*H/h_max -- расстояние между глазами
-- построчное построение стерео изображения ("расширенное")
for y = 0 to (height - 1) do
(
for x = 0 to (width - 1) do
for dx = 0 to (prec - 1) do
(
-- берем из рендера глубины расстояние от камеры до ближайшей точки (тут тоже идет линейное сглаживание)
dist = (- (getChannel DOF_img [x,y] #zDepth)[1]*(prec-dx) - (getChannel DOF_img [x,y] #zDepth)[1]*dx)/prec
if dist > H then dist = H -- не должно быть больше макс. глубины
d =(prec*a*(dist+h_max-H)/(dist)+.5)as integer -- вычисляем расстояние на котором должны совпадать пиксели стерео изображения
setPixels OUT_img [x*prec+dx+d_max*prec,y](getPixels OUT_img [x*prec+dx+d,y] 1) -- и дубируем пиксели согласно этому расстоянию
)
prb_R_status.value =(100 * y / height) as integer
)
prb_R_status.value = 0
-- получение из "расширенного" "нормальное" стерео изображение
FIN_img = bitmap (width+d_max) height color:black -- создание битмапы для финального стерео изображения
for y = 0 to (height - 1) do
(
for x = 0 to (width + d_max - 1) do
(
c = black
for i = 0 to (prec-1) do
c +=(getPixels OUT_img [x*prec+i,y] 1)[1]/prec -- получение сренего арифметического цвета
setPixels FIN_img [x,y] #(c) -- и сохранение его в финальное стрео изображение
)
prb_R_status.value =(100 * y / height) as integer
)
prb_R_status.value = 0
display FIN_img -- открываем окно с финальным стерео изображением
)
)
Изменение здесь только в алгоритме просчета стерео изображения. Добавилась проверка «от дурака»: просчет не начнется, если не была выбрана текстура и камера. Так же тут был реализован алгоритм «улучшенного» расчета стерео изображения с использованием спиннера «качество». В двух словах в чем это заключается: изображение сначала линейно растягивается и просчет ведется тоже «растянуто», а потом полученный результат сжимается до первоначального размера. Разницу можно увидеть на рис.7, рис.8, рис.9.
На рис.7 качество выставлено в единицу, а на рис.8 и рис.9 – 4 и 8 соответственно. Конечно чем выше качество, тем больше время просчета.
Вот и все. Теперь у нас имеется скрипт-утилита, с помощью которой можно из любого 3D-объекта получить стерео изображение. Очень надеюсь что урок был понятен (для этого надо дружить с геометрией :) и полезен (по крайней мере тем, что можно действительно посмотреть свои модели «в объеме»).