Предлагаю на время отвлечься от задач автоматизации, и погрузиться в загадочный и увлекательный мир 3D моделирования. Дочитавшим до конца — сюрприз)
Предыстория
К сожалению, реализация этого алгоритма на OneScript имеет один критический недостаток — картинка формируется слишком медленно. Программа интенсивно использует структуры данных, что приводит к большим накладным расходам.
Забегая вперёд, скажу, что труды Mr-Rm не пропали даром, и его программа засияла новыми красками. Мной была проделана работа по адаптации этой программы под новый язык "Перфолента" Сергея Рогаткина.
Итак, мы получили работающий код на языке "Перфолента".
//Код, оптимизированный под возможности Перфоленты
#ИспользоватьСтандартнуюБиблиотеку
//В процессе экспериментов выяснилось,
//что Вектор лучше делать Структурой
//а Материал, Свет и Сферу классами, тогда производительность возрастает
//примерно на 25 процентов по сравнению с вариантом когда только структуры
//и примерно на 8 процентов по сравнению с вариантом, когда только классы
// ------------------
Структура Вектор
&ВидноВсем Поле x тип ДВещ
&ВидноВсем Поле y тип ДВещ
&ВидноВсем Поле z тип ДВещ
//---------------------------
&ВидноВсем
Конструктор(px тип ДВещ, py тип ДВещ, pz тип ДВещ)
x = px
y = py
z = pz
КонецКонструктора
#Область ПереопределениеОператоровВектора
//Переопределим для теста операторы сложения, вычитания, умножения и скалярного произведения векторов
//проверим в функции Отражение()
//---------------------------
&ВидноВсем, ОбщийДляКласса
Оператор +(В1 тип Вектор, В2 тип Вектор) тип Вектор
возврат Новый Вектор( В1.x+В2.x, В1.y+В2.y, в1.z+В2.z );
КонецОператора
//---------------------------
&ВидноВсем, ОбщийДляКласса
Оператор -(В1 тип Вектор, В2 тип Вектор) тип Вектор
возврат Новый Вектор( В1.x-В2.x, В1.y-В2.y, В1.z-В2.z );
КонецОператора
//---------------------------
&ВидноВсем, ОбщийДляКласса
Оператор *(В1 тип Вектор, ч тип ДВещ) тип Вектор
возврат Новый Вектор( В1.x*ч, В1.y*ч, В1.z*ч );
КонецОператора
//---------------------------
&ВидноВсем, ОбщийДляКласса
Оператор *(В1 тип Вектор, В2 тип Вектор) тип ДВещ
возврат В1.x*В2.x + В1.y*В2.y + В1.z*В2.z;
КонецОператора
#КонецОбласти
//---------------------------
&ВидноВсем,ОбщийДляКласса
Процедура Печать( В1 тип Вектор )
ВыводСтроки "("+В1.x+","+В1.y+","+В1.z+")";
КонецПроцедуры
//---------------------------
&ВидноВсем,ОбщийДляКласса
Функция Минус( В1 тип Вектор ) тип Вектор
возврат Новый Вектор( -В1.x, -В1.y, -В1.z );
КонецФункции
//---------------------------
&ВидноВсем,ОбщийДляКласса
Функция Сумма( В1 тип Вектор, В2 тип Вектор ) тип Вектор
возврат Новый Вектор( В1.x+В2.x, В1.y+В2.y, в1.z+В2.z );
КонецФункции
//---------------------------
&ВидноВсем,ОбщийДляКласса
Функция Разность( В1 тип Вектор, В2 тип Вектор ) тип Вектор
возврат Новый Вектор( В1.x-В2.x, В1.y-В2.y, В1.z-В2.z );
КонецФункции
//---------------------------
&ВидноВсем,ОбщийДляКласса
Функция Умножить( В1 тип Вектор, ч тип ДВещ) тип Вектор
возврат Новый Вектор( В1.x*ч, В1.y*ч, В1.z*ч );
КонецФункции
//---------------------------
&ВидноВсем,ОбщийДляКласса
Функция СкалярноеПр( В1 тип Вектор, В2 тип Вектор ) тип ДВещ
возврат В1.x*В2.x + В1.y*В2.y + В1.z*В2.z;
КонецФункции
//---------------------------
&ВидноВсем,ОбщийДляКласса
Функция Норма( В1 тип Вектор ) тип ДВещ
возврат Sqrt( В1.x*В1.x+В1.y*В1.y+В1.z*В1.z );
КонецФункции
//---------------------------
&ВидноВсем,ОбщийДляКласса
Функция КвНормы( В1 тип Вектор ) тип ДВещ
возврат В1.x*В1.x+В1.y*В1.y+В1.z*В1.z;
КонецФункции
//---------------------------
&ВидноВсем,ОбщийДляКласса
Функция Нормализация( В1 тип Вектор ) тип Вектор
норма=Вектор.Норма( В1 );
В1.x = В1.x/норма;
В1.y = В1.y/норма;
В1.z = В1.z/норма;
возврат В1;
КонецФункции
КонецСтруктуры
//***************************
Программа tiny
//---------------------------
Поле МАКС_Число тип ДВещ;
Поле ГЛУБИНА_РЕКУРСИИ тип Целое;
Поле НулевойВектор тип Вектор;
Поле ЕдиничныйВектор тип Вектор;
Поле ЕдиничныйВекторX тип Вектор;
Поле ЕдиничныйВекторY тип Вектор;
Поле ЕдиничныйВекторZ тип Вектор;
Поле ЦветФона тип Вектор;
Поле ЦветБелый тип Вектор;
Поле ЦветЖелтый тип Вектор;
Поле ПлиткаБелая тип Материал;
Поле ПлиткаЖелтая тип Материал;
Поле Сферы тип Сфера[];
Поле Освещение тип Свет[];
Поле Ширина тип Целое;
Поле Высота тип Целое;
Поле УголЗрения тип ДВещ;
Поле Кадр тип Вектор[];
Функция Норм_Цвет(знач цв тип ДВещ) тип ДВещ
возврат Цел(255*Макс(0, Мин(1,цв)));
КонецФункции
// ------------------
Функция Сфера_Пересечение(Сф тип Сфера,Исх тип Вектор, Напр тип Вектор, ссыл Расст тип ДВещ) тип Булево
КЦентру = Вектор.Разность( Сф.Центр, Исх );
перем пр тип ДВещ = Вектор.СкалярноеПр( КЦентру, Напр );
перем д2 тип ДВещ = Вектор.КвНормы( КЦентру ) - пр*пр;
перем р2 тип ДВещ = Сф.Радиус*Сф.Радиус;
если д2 > р2 тогда
возврат Ложь;
КонецЕсли;
р = Sqrt( р2 - д2 );
Расст = пр - р;
если Расст < 0 тогда
Расст = пр + р;
если Расст < 0 тогда
возврат Ложь;
КонецЕсли;
КонецЕсли;
возврат Истина;
КонецФункции
Функция НайтиПересечения( Исх тип Вектор, Напр тип Вектор, ссыл Точка тип Вектор, ссыл Нормаль тип Вектор, ссыл Материал тип Материал ) тип Булево
Перем Расст тип ДВещ = 0;
МинРасст = МАКС_Число;
Для Каждого Сф из Сферы Цикл
если (Сфера_Пересечение( Сф,Исх,Напр, Расст )) и (Расст < МинРасст) тогда
МинРасст = Расст;
Точка = Вектор.Сумма( Исх, Вектор.Умножить( Напр, Расст ) );
Нормаль = Вектор.Нормализация( Вектор.Разность( Точка, Сф.Центр ) );
Материал = Сф.Материал;
конецесли;
КонецЦикла;
// квадрат (-10,-10,-4) - (10,-30,-4)
Если Напр.y < -0.001 Тогда
р = -(Исх.y+4)/Напр.y;
Если 0<р и р<МинРасст Тогда
Т = Вектор.Умножить(Напр,р);
Т = Вектор.Сумма(Т,Исх);
Если Abs(Т.x)<10 и -30<Т.z и Т.z<(-10) Тогда
МинРасст = р;
Точка = Т;
Нормаль = ЕдиничныйВекторY;
Материал = ?( (Цел(0.5*Точка.x+1000)+Цел(0.5*Точка.z))%2=1, ПлиткаБелая, ПлиткаЖелтая );
КонецЕсли;
КонецЕсли;
КонецЕсли;
возврат МинРасст < 1000;
КонецФункции
// return I - N*2.f*(I*N);
Функция Отражение( Луч тип Вектор, Норм тип Вектор ) тип Вектор
//возврат Вектор.Разность( Луч, Вектор.Умножить( Норм, 2*Вектор.СкалярноеПр(Луч, Норм) ) );
//воспользуемся переопределенными операторами для Вектора
возврат Луч - Норм * 2д * (Луч * Норм)
КонецФункции
Функция Преломление( Луч тип Вектор, Норм тип Вектор , КоэфПр тип ДВещ ) тип Вектор
Если КоэфПр=1 Тогда
Возврат Луч;
КонецЕсли;
CosI = -Макс( -1, Мин( 1, Вектор.СкалярноеПр(Луч, Норм)));
Если CosI>=0 Тогда
_1; = 1/КоэфПр;
Н = Норм;
Иначе
CosI = -CosI;
_1; = КоэфПр;
Н = Вектор.Минус(Норм);
КонецЕсли;
К = 1 - _1;*_1;*(1-CosI*CosI);
Если К<0 Тогда
возврат ЕдиничныйВекторX;
КонецЕсли;
возврат Вектор.Сумма( Вектор.Умножить(Луч,_1;), Вектор.Умножить(Н,_1;*CosI-Sqrt(К)) );
КонецФункции
Функция ТрассироватьЛуч( Исх тип Вектор, Напр тип Вектор, Глубина тип Целое = 0 ) тип Вектор
Перем Точка тип Вектор = Неопределено;
Перем Нормаль тип Вектор = Неопределено;
Перем Материал тип Материал = Неопределено;
Перем ТеньТочка тип Вектор = Неопределено;
Перем ТеньНормаль тип Вектор = Неопределено;
Перем ВремМатериал тип Материал = Неопределено;
если Глубина>ГЛУБИНА_РЕКУРСИИ или НЕ НайтиПересечения( Исх,Напр, Точка,Нормаль,Материал ) тогда
возврат ЦветФона;
конецесли;
СдвигПоНормали = Вектор.Умножить( Нормаль, 0.001 );
ОтражНапр = Отражение(Напр,Нормаль); // нормализовано
ОтражИсх = ?( Вектор.СкалярноеПр(ОтражНапр, Нормаль)<0, Вектор.Разность(Точка,СдвигПоНормали), Вектор.Сумма(Точка,СдвигПоНормали) );
ОтражЦвет = ТрассироватьЛуч( ОтражИсх, ОтражНапр, Глубина+1 );
ПреломНапр = Вектор.Нормализация(Преломление(Напр,Нормаль,Материал.КПрелом));
ПреломИсх = ?( Вектор.СкалярноеПр(ПреломНапр, Нормаль)<0, Вектор.Разность(Точка,СдвигПоНормали), Вектор.Сумма(Точка,СдвигПоНормали) );
ПреломЦвет = ТрассироватьЛуч( ПреломИсх, ПреломНапр, Глубина+1 );
перем Яркость тип ДВещ = 0;
перем Блик тип ДВещ = 0;
Для Каждого Свет из Освещение цикл
НапрКСвету = Вектор.Нормализация(Вектор.Разность(Свет.Поз, Точка));
РасстДоСвета = Вектор.КвНормы(Вектор.Разность(Свет.Поз, Точка));
ТеньИсх = ?( Вектор.СкалярноеПр(НапрКСвету, Нормаль)<0, Вектор.Разность(Точка,СдвигПоНормали), Вектор.Сумма(Точка,СдвигПоНормали) );
если НайтиПересечения( ТеньИсх,НапрКСвету, ТеньТочка,ТеньНормаль,ВремМатериал )
и Вектор.КвНормы(Вектор.Разность(ТеньТочка, ТеньИсх))<РасстДоСвета тогда
Продолжить;
конецесли;
Яркость = Яркость + Свет.Ярк * Макс( 0, Вектор.СкалярноеПр( НапрКСвету, Нормаль ));
//powf(std::max(0.f, -reflect(-light_dir, N)*dir), material.specular_exponent)*lights[i].intensity;
Блик = Блик + Свет.Ярк * Pow(Макс(0, Вектор.СкалярноеПр(Отражение(НапрКСвету, Нормаль),Напр) ), Материал.ЭкспОтраж);
КонецЦикла;
возврат Вектор.Сумма(Вектор.Сумма(Вектор.Сумма( Вектор.Умножить( Материал.Цвет, Яркость*Материал.Альб0 ), Вектор.Умножить( ЕдиничныйВектор, Блик*Материал.Альб1 )),Вектор.Умножить( ОтражЦвет, Материал.Альб2 )),Вектор.Умножить( ПреломЦвет, Материал.Альб3 ));
КонецФункции
Процедура Рендер()
Камера = Новый Вектор(0,0,0);
Напр_Z =-Высота/(2*Tan(УголЗрения/2));
Для верт=0 по Высота-1 Цикл
линия = верт*Ширина;
Напр_Y = -(верт+0.5д)+Высота/2д;
Для гор=0 по Ширина-1 Цикл
Напр_X = (гор+0.5д)-Ширина/2д;
Направление = Вектор.Нормализация( Новый Вектор(Напр_X,Напр_Y,Напр_Z) );
Кадр[линия+гор] = ТрассироватьЛуч( Камера, Направление );
КонецЦикла;
КонецЦикла;
КонецПроцедуры
Процедура СохранитьКадр( ИмяФайла тип Строка)
файл=Новый ТекстовыйДокумент;
файл.ДобавитьСтроку("P3");
файл.ДобавитьСтроку(""+Ширина+" "+Высота);
файл.ДобавитьСтроку("255");
//Для п=0 по Высота*Ширина-1 Цикл
Для Инд тип Целое = 0 по Высота*Ширина-1 Цикл //делаем переменную цикла целой!
Пиксел = Кадр[Инд];
перем м1 тип ДВещ = Пиксел.x;
перем м2 тип ДВещ = Пиксел.y;
перем м3 тип ДВещ = Пиксел.z;
МаксИнт = Макс( м1, Макс(м2,м3) );
Если МаксИнт>1 Тогда
МаксИнт = 1/МаксИнт;
Пиксел.x = Пиксел.x * МаксИнт;
Пиксел.y = Пиксел.y * МаксИнт;
Пиксел.z = Пиксел.z * МаксИнт;
КонецЕсли;
файл.ДобавитьСтроку( ""+Норм_Цвет(Пиксел.x)+" "+Норм_Цвет(Пиксел.y)+" "+Норм_Цвет(Пиксел.z) );
КонецЦикла;
ВыводСтроки ИмяФайла
файл.Записать(ИмяФайла,КодировкаТекста.ANSI);
КонецПроцедуры
Процедура Старт
Пи = 3.14159265359;
МАКС_Число = 999999999999999;
ГЛУБИНА_РЕКУРСИИ = 4;
НулевойВектор=Новый Вектор(0, 0, 0);
ЕдиничныйВектор=Новый Вектор(1, 1, 1);
ЕдиничныйВекторX=Новый Вектор(1, 0, 0);
ЕдиничныйВекторY=Новый Вектор(0, 1, 0);
ЕдиничныйВекторZ=Новый Вектор(0, 0, 1);
ЦветБелый=ЕдиничныйВектор;
ЦветЖелтый=Новый Вектор(1, 0.7, 0.3);
Ширина=1024;
Высота=768;
УголЗрения = Пи/3;
Кадр = Новый Вектор[Ширина*Высота]
ЦветФона=Новый Вектор(0.2, 0.7, 0.8);
Серый = Новый Материал( Новый Вектор( 0.4, 0.4, 0.3 ), 0.6, 0.3,0.1,0, 50,1 );
Стекло = Новый Материал( Новый Вектор( 0.6, 0.7, 0.8 ), 0, 0.5,0.1,0.8, 125,1.5 );
Красный = Новый Материал( Новый Вектор( 0.3, 0.1, 0.1 ), 0.9, 0.1,0.0,0, 10,1 );
Зеркало = Новый Материал( Новый Вектор( 1.0, 1.0, 1.0 ), 0, 10.0,0.8,0, 1425,1 );
ПлиткаБелая = Новый Материал( Вектор.Умножить(ЦветБелый,0.3), 1,0,0,0, 0,1 );
ПлиткаЖелтая = Новый Материал( Вектор.Умножить(ЦветЖелтый,0.3), 1,0,0,0, 0,1 );
Сферы = Новый Сфера[3]
Сферы[0]=Новый Сфера(Новый Вектор(-3, 0, -16), 2, Серый )
Сферы[1]=Новый Сфера(Новый Вектор(-1.0,-1.5,-12), 2, Стекло )
Сферы[2]=Новый Сфера(Новый Вектор( 1.5,-0.5,-18), 3, Красный )
Сферы[3]=Новый Сфера(Новый Вектор( 7, 5, -18), 4, Зеркало )
Освещение = Новый Свет[2]
Освещение[0]=Новый Свет(Новый Вектор(-20, 20, 20), 1.5)
Освещение[1]=Новый Свет(Новый Вектор( 30, 50, -25), 1.8)
Освещение[2]=Новый Свет(Новый Вектор( 30, 20, 30), 1.7)
т0=ТекущаяУниверсальнаяДатаВМиллисекундах();
Рендер();
т1=ТекущаяУниверсальнаяДатаВМиллисекундах();
ВыводСтроки("~"+ Цел(т1-т0) );
СохранитьКадр(ЭтаПрограмма.Каталог+"out.ppm");
Консоль.Пауза
КонецПроцедуры
КонецПрограммы
// ------------------
Класс Материал
&ВидноВсем Поле Цвет тип Вектор
&ВидноВсем Поле Альб0 тип ДВещ
&ВидноВсем Поле Альб1 тип ДВещ
&ВидноВсем Поле Альб2 тип ДВещ
&ВидноВсем Поле Альб3 тип ДВещ
&ВидноВсем Поле ЭкспОтраж тип ДВещ
&ВидноВсем Поле КПрелом тип ДВещ
//---------------------------
&ВидноВсем
Конструктор(пЦвет тип Вектор, пАльб0 тип ДВещ, пАльб1 тип ДВещ, пАльб2 тип ДВещ, пАльб3 тип ДВещ, пЭкспОтраж тип ДВещ, пКПрелом тип ДВещ)
Цвет = пЦвет
Альб0 = пАльб0
Альб1 = пАльб1
Альб2 = пАльб2
Альб3 = пАльб3
ЭкспОтраж = пЭкспОтраж
КПрелом = пКПрелом
КонецКонструктора
КонецКласса
// ------------------
Класс Свет
&ВидноВсем Поле Поз тип Вектор
&ВидноВсем Поле Ярк тип ДВещ
//---------------------------
&ВидноВсем
Конструктор(пПоз тип Вектор, пЯрк тип ДВещ)
Поз=пПоз
Ярк=пЯрк
КонецКонструктора
КонецКласса
// ------------------
Класс Сфера
&ВидноВсем Поле Центр тип Вектор
&ВидноВсем Поле Радиус тип ДВещ
&ВидноВсем Поле Материал тип Материал
//---------------------------
&ВидноВсем
Конструктор( пЦентр тип Вектор, пРадиус тип ДВещ, пМатериал тип Материал )
Центр=пЦентр
Радиус=пРадиус
Материал=пМатериал
КонецКонструктора
КонецКласса
Как видите, классы очень органично легли в программу, и это не удивительно, потому что они были в исходной версии на С++.
Что же теперь со скоростью? Это невероятно! Программа стала выполняться быстрее в 500 раз! Это конечно не так быстро как в C++, но учитывая гораздо более простой процесс разработки, результат более чем достойный.
//1) добавляем поля
//переменные для параллельного цикла
Поле Камера тип Вектор;
Поле линия тип ДВещ;
Поле Напр_Y тип ДВещ;
Поле Напр_Z тип ДВещ;
//2) изменяем процедуру Рендер
Процедура Рендер()
Камера = Новый Вектор(0,0,0);
Напр_Z =-Высота/(2*Tan(УголЗрения/2));
Для верт=0 по Высота-1 Цикл
линия = верт*Ширина;
Напр_Y = -(верт+0.5д)+Высота/2д;
//распараллелим
ПараллельныеДействия.Для(0, Ширина-1, ПолучитьДелегат(,ТелоЦикла,"ДействиеПроц<Целое>"))
КонецЦикла;
КонецПроцедуры
//3) добавляем тело параллельного цикла
//---------------------------
//тело параллельного цикла
Процедура ТелоЦикла(гор тип Целое)
Напр_X = (гор+0.5д)-Ширина/2д;
Направление = Вектор.Нормализация( Новый Вектор(Напр_X,Напр_Y,Напр_Z) );
Кадр[линия+гор] = ТрассироватьЛуч( Камера, Направление );
КонецПроцедуры
Вот что у меня получилось:
И ещё
И всё
Сравнивать IL под .Net платформу с псевдобайткодом стековой машины 1с, думаю, не уместно, т. к. последний в алгоритмических задачах, мягко сказать, просаживается по производительности в сотни раз.
Цель статьи наглядно продемонстрировать возможности нового языка. Ранее недоступные.
(1) зато хороший пример того, что иногда производительность имеет значение…
пользоваться программой до переделки совершенно не возможно…
а еще, показывает, что при необходимости критичный код перевести на Перфоленту не трудно…
и картинки красивые получились, хоть на рабочий стол ставь 🙂
(2) кстати, да, интересный ход, пропускать слабые кадры по времени рендеринга… если шары расположились кучкой друг за другом, то картинка вряд-ли будет интересной!
генератор картинок на рабочий стол, стандартные обои кубунты некоторых релизов напомнило)
Отрендерил в 4К и посмотрел на большом телевизоре.. гораздо интереснее выглядит, чем сжатые кадры маленького разрешения, которые тут в скриншотах показаны…