Долго собирался заняться изучением 3D в FireMonkey, но всё как-то не было подходящей идеи. Что можно сделать такого в 3D не только ради «вау-эффекта», но и что бы свою работу где-либо применить? Всякие летающие кубики, шарики и бегающие по форме зеленые человечки — это, конечно, здорово, но…для чего? В общем думал я думал и придумал где применить свои навыки работы с 3D в FireMonkey. Буквально в конце прошлого 2012 года, те кто участвовал в конкурсе в блоге Всеволода Леонова наверняка узнали, что автор блога WebDelphi как-то связан с охраной окружающей среды…Наверняка сейчас те, кто этого не знал удивились :) Не удивляйтесь и поверьте мне на слово — в промышленной экологии математики, физикой и даже программирования хватает…иногда даже с избытком. Так вот решил я с помощью FireMonkey 3D визуализировать процесс загрязнения атмосферы. Нормальная такая задачка, обычно решаемая на плоскости. Ну, а нормально построенная 3D-модель повысит наглядность + можно же будет без проблем представлять картинку как угодно: сверху, сбоку, построит разрез поля и т.д. В общем должно получиться нормальное такое наглядное пособие для великих умов завтрашнего дня.
Краткое введение в суть задачи
Чтобы лишний раз не грузить Вас кучей законов и формул, которые используются в расчётах (думаю, что никому, кроме меня это будет неинтересно) я просто скажу, что визуализируемый мной процесс (загрязнение атмосферы) детально расписан в теории атмосферной диффузии. Жуткое такое название, которое, собственно, себя оправдывает. Если хотите вкратце узнать что это за теория и как она выглядит — ссылочка на небольшой отрывок из книги. Сей отрывок даст примерное представление о том, что необходимо запрограммировать в Delphi, чтобы решить задачу.
Если рассматривать процесс поверхностно, очень приближенно, то загрязнение одним источником (например, дымовой трубой) можно представить в виде вот такого рисунка:
Кривая на графике — это значение концентрации вещества на различных расстояниях от источника. Т.е. она (концентрация) сначала растет, потом достигает максимума и падает, но никогда не достигает нуля.
На практике же поля загрязнения строятся с учетом направления ветра. Например, одна из программ, которая умеет строить такие поля, считает поле по сектору направления ветров от 0 до 360 градусов и в итоге получаются вот такие изолинии концентраций:
Если строить таким образом поле загрязнения для одного источника, то на виде сверху мы получим ряд концентрических окружностей с центром в точке расположения источника, на виде сбоку — см. первый рисунок. Что получится в объеме тоже можно представить в уме…хотя зачем нам что-то представлять в уме, когда есть FireMonkey 3D? :) Вот, собственно, очень коротко и сжато я подвел разговор к постановке задачи — построить поле загрязнения в 3D.
При этом я решил выстроить это поле не только для одного источника, а для группы — при этом концентрации веществ должны буду просуммироваться и получится уже набор концентрических окружностей, а что-то, что уже в уме просто так не представить.
Наглядные примеры FireMonkey 3D
Какие материалы были изучены перед тем, как я засел в Delphi? Вот, что было предварительно изучено.
Визуализация математических функций
В качестве отправной точки в решении задачи я решил воспользоваться примером из статьи «Visualizing mathematical functions by generating custom meshes using FireMonkey«. На данный момент это, практически единственный пример, достаточно детально описывающий работу с TMesh. Похожая публикация также имеется и в «Блоге Delphi программиста», где эта же задача расписана чуть более подробно.
Пример, что тут говорить, действительно интересный. Выглядит красиво:
Но в FireMonkey 2.0., в силу её глобальной переработки, этот пример надобно немного «подработать напильником» — некоторые классы были перемещены, свойства отдельных классов удалены и т.д. В общем на данный момент самым ценным в этом примере, за что автору огромное спасибо, является процедура построения сетки — здесь основные моменты работают как надо. Код процедуры я уже копипастить не буду, т.к. посмотреть процедуру Вы можете в оригинале статьи.
FireMonkey — TMesh.
В этом видео Всеволод довольно детально и обстоятельно рассказывает нам о TMesh, об осях координат в FireMonkey, нормалях и т.д. В общем, перед началом работы стоит посмотреть этот вебинар от начала и до конца.
[youtube_sc url=»http://www.youtube.com/embed/B_nRIN55cZI»]В общем, вооружившись примерами, начал я думать как решить свою задачку.
Решение
Создание текстуры
В FireMonkey 1.0. собрать текстуру можно было так:
BMP := TBitmap.Create(1,360); for k := 0 to 359 do BMP.Pixels[0,k] := CorrectColor(HSLtoRGB(k/360,0.75,0.5));
В Delphi XE3 Вы получите ошибку т.к. у TBitmap нет больше свойства Pixels. Чтобы собрать TBitmap в точности как в примере, необходимо использовать следующий код:
var BD: TBitmapData; k:integer; begin BMP.Map(TMapAccess.maReadWrite,BD); for k := 0 to 359 do BD.SetPixel(0,k, CorrectColor(HSLtoRGB(k/360,0.75,0.5))); BMP.Unmap(BD); end;
TBitmapData содержится в модуле FMX.Types и имеет следующее описание:
TBitmapData = record private FPixelFormat: TPixelFormat; public Data: Pointer; Pitch: Integer; property PixelFormat: TPixelFormat read FPixelFormat; function GetPixel(const X, Y: Integer): TAlphaColor; procedure SetPixel(const X, Y: Integer; const AColor: TAlphaColor); end;
Методы HSLToRGB и CorrectColor содержатся в модуле System.UIConsts. В принципе, подобной текстуры мне на первом этапе работы было вполне достаточно. В дальнейшем, правда, пришлось немного изменить алгоритм выбора точки на этом битмапе, но об этом чуть позже.
Следующий шаг — генерация TMesh.
Генерация TMesh
Как я говорил выше, пример с визуализацией математических функций хорош. Но именно как пример. Дело в том, что если внимательно посмотреть на генерацию TMesh, а точнее на вот этот код:
while u < MaxX do begin v := -MaxZ; while v < MaxZ do begin P[0].x := u; P[0].z := v; P[1].x := u+d; P[1].z := v; P[2].x := u+d; P[2].z := v+d; P[3].x := u; P[3].z := v+d; [...] v := v+d; end; u := u+d; end;
То можно увидеть, что при задании вершин и индексов треугольников в TMeshData мы будем просчитывать некоторые точки по несколько раз. Для примера — это не критично и даже желательно — мы можем в том же отладчике наглядно увидеть как строится сетка, какие координаты используются и т.д. Для решения реальной задачи — проблема, т.к. для того поля, которое необходимо построить мне, значение Y в каждой точки связано с довольно не слабыми расчётами (см. ссылочку на отрывок из книги) и, если считать каждую точку по несколько раз, то итоговой картинки можно и не дождаться. Поэтому, чтобы как-то оптимизировать процесс построения сетки я сделал следующее:
1. Рассчитал необходимые значения концентраций в каждой точке, т.е. общий размер массива точек у меня вместо:
Round(Rect.Width/FDelta*Rect.Height/FDelta)*4
Стал в 4 раза меньше, т.е.
Round(Rect.Width/FDelta*Rect.Height/FDelta)
Дальше, как не трудно догадаться, в процедуре назначения вершин и индексов я просто провожу поиск точки с нужными мне координатами и записываю значение P.Y, т.е. формально это выглядит так:
while u < MaxX do begin v := -MaxZ; while v < MaxZ do begin P[0].x := u; P[0].z := v; P[1].x := u+d; P[1].z := v; P[2].x := u+d; P[2].z := v+d; P[3].x := u; P[3].z := v+d; [...] P[0].y :=GetPoint(PointF(P[0].X,P[0].Z)); [...] v := v+d; end; u := u+d; end;
где функция GetPoint ищет значение в заданной точке. Искать-то по-любому намного быстрее, чем решать кучу уравнений. Таким образом в методе генерации TMesh я не оставил ни одного расчёта — только поиск.
Ещё один момент генерации сетки — это координаты. Стоит помнить, что
Если не учитывать направление Y, то при решении моей задачи может оказаться, что концентрация уходит в минус (все точки TMesh на экране будут расположены под плоскость. ZOX), что с точки зрения здравого смысла не имеет никого смысла. Поэтому моя функция GetPoint не только ищет, то и меняет знак у значения концентрации, чтобы поле загрязнения на экране выглядело так как положено.
Третий момент работы с TMesh — масштаб. На экране у нас точки, в жизни — метры, дюймы, мили, километры и т.д. Чтобы уместить поле необходимых мне размеров на экран я просто задал коэффициент масштаба, который, например, говорит, что 1 пиксель на экране — это 10 метров в реальном пространстве. В итоге, изменяя этот коэффициент, я могу рассчитать поле хоть 1х1 метр, хоть 1000х1000 километров и уложить эти все данные в несколько десятков точек на экране.
Четвертый момент, на который следует обратить Ваше внимание — размерность
Mesh.Data.IndexBuffer
В рантайме Вы можете задать этой структуре размер вплоть до 2147483647 (integer), но на деле более 65536 (Word). В QC есть репорт на эту тему, но черт его знает баг ли это…В общем при назначении длины у IndexBuffer и VertexBuffer проверяйте дополнительно — сможете ли Вы записать такое количество вершин и индексов или нет. На всякий случай ниже представлены методы записи вершины треугольника и индекса точки, используемые в данный момент в FireMonkey:
{PByteArray = ^TByteArray; TByteArray = array[0..32767] of Byte; PWordArray = ^TWordArray; TWordArray = array[0..16383] of Word;} procedure TVertexBuffer.SetVertices(AIndex: Integer; const Value: TPoint3D); begin assert((AIndex >= 0) and (AIndex < Length)); {$R-} PPoint3D(@PByteArray(FBuffer)[AIndex * FVertexSize])^ := Value; {$R+} end; procedure TIndexBuffer.SetIndices(AIndex: Integer; const Value: Integer); begin assert((AIndex >= 0) and (AIndex < Length)); {$R-} PWordArray(FBuffer)[AIndex] := Value //MAX = 65536 {$R+} end;
В принципе, я вполне уложился в 32767 вершины, чтобы решить свою задачу. Хватит ли такого количества Вам — не знаю :).
Следующим моментом работы было — определение точек для текстуры.
Работа с текстурой
Плавные переходы одного цвета в другой, переливы и перекаты — это круто и красиво, но опять же не про мою задачу. Мне крайне необходимо сделать так, чтобы каждая изолиния поля чётко и ясно выделялась. Да, в этом случае я могу потерять в красоте, но иначе никак.
Что я решил сделать в итоге. В качестве исходника текстуры у нас имеется такой битмап:
В модели HSL значение тона (H) задается в градусах от 0 до 360. Если считать очень грубо, то на нашей текстуре значениям H от 70 до 120 будут соответствовать оттенки зеленого цвета, от 190 до 240 — оттенки синего и т.д. Для того, чтобы четко выделять изолинии на своем поле я решил сделать так:
Определяем для каждого интервала значений концентрации определенный сектор в HSL. Например,
- если концентрация составляет от 0.1 до 0.5. от максимально допустимой нормы, то это будет соответствовать зеленому сектору в HSL,
- если от 0.5 до 0.7. — синему сектору HSL
- и т.д.
Градаций можно задать сколько угодно, лишь бы потом, глядя на картинку, можно было четко определить какие значения концентраций имеются в заданной области.
Если надо детализировать цвета внутри сектора, то можно это сделать написав элементарную функцию линейной интерполяции.
У меня сейчас выстраивается пятицветное поле. Грубовато, но для примера сойдет. В итоге я написал простенькую функцию:
Которая получает в качестве входного параметра значение концентрации в заданной точке и возвращает в результате значение Y для точки на нашем битмапе текстуры. То есть назначение текстуры у меня стало выглядеть в программе так:
with VertexBuffer do begin TexCoord0[NP+0] := PointF(0,GetTextureY(P[0].y)); TexCoord0[NP+1] := PointF(0,GetTextureY(P[1].y)); TexCoord0[NP+2] := PointF(0,GetTextureY(P[2].y)); TexCoord0[NP+3] := PointF(0,GetTextureY(P[3].y)); end;
К чему в итоге привели все мои попытки решить задачу смотрим ниже.
Итоговые картинки
Первый результат — поле для трех источников загрязнения размером 5х5 км. Плоская картинка:
Второй результат — поле для трех источников размером 10х10 км. Объемная картинка:
Третий результат — отдельный источник. Поле 10х10 км
Вот такие получились результаты. На данный момент «заморочился» над построением различных сечений этого поля, чтобы можно было детально рассматривать всю картинку с разных сторон и ракурсов.
Если получиться из этой затеи что-либо дельное — обязательно отпишусь.
Отличная статья — теперь теория атмосферной диффузии в 3D визуализации будет несомненно ближе к народу, т.к. пространственная картина степени заср@нности техногенного ландшафта (не пусть с технологическим, который также заср@н не меньше) заставит людей более серьезно относиться к проблеме экологии. Давеча я стоял в «жуткий» подмосковный мороз (аж -17, привет Евгению Крюкову с его -37 в Улан-Удэ) в станционном павильончике — ждал электричку, грелся. На стене разглядывал плакаты а-ля «Гражданская оборона» с информацией, устаревшей как вато-марлевая повязка. Там сообщалось, что в случае разлития хлора бежать нужно перепендикулярно ветру и избегать оврагов. Я думаю, сделав 3Д картину распространения хлорного облака в условиях… Подробнее »
Всеволод Леоновб да мне как бэ второго диплома к.т.н. не требуется :) Где бы корку программиста по-быстрому нарыть? :)
У меня вопрос чуть в сторону…
Данные получены с помощью GISTool?
Если да, то…
Как удалось «подружить» GISTool с FireMonkey?
Или это по схеме (GISTool+VCL)+FireMonkey
Alex, GisTool в данном случае вообще не используется. У нас есть своя небольшая экологическая ГИС из которой я вытащил небольшой кусок кода для работы с координатной сеткой, а остальное — чистая FireMonkey
Проблема с размерностью Mesh.Data.IndexBuffer в том, что можно сослаться только на 65536
вершин (элемент массива — Word).
Не совсем понимаю, почему разработчики FireMonkey наложили такое ограничение.
Т.е. я могу нарисовать плоскость максимум 255х255 точек.
Для большинства задач этого наверно хватит, но что делать, если вершин будет больше?
Например при решении задач методом конечных элементов или разностными схемами кол-во узлов легко может превысить 65000.
Для отрисовки таких систем придется каким-то образом разбивать на подобласти.
Вы абсолютно правы. Есть ограничение на 65536 вершин. Сам долго ломал голову почему именно так и единственное, что обнаружил — это ограничение именно в железе, а не FMX. К сожалению не могу сейчас кинуть точную ссылку, но на одном из сайтов про видео-карты встречал фразу..что-то вроде «максимально возможное количество обрабатываемых вершин 65536». Может в этом корень «зла»? Я пока решаю проблему такого ограничения путем введения дополнительного коэффициента для масштабирования карты, т.е. по умолчанию у меня 1 пиксель = 1 метр, если ввожу коэффициент 4, то получаю, что в 1 пикселе 4 метр и уже в зависимости от этого выстраиваю картинку.… Подробнее »
Я столкнулся с такой проблемой, мне нужно отображать участок карты минимум 500х500. Ситуация не совсем понятная, так как на том же железе с использованием GLScene все работает нормально. Возможно там реализована программная «склейка». С другой стороны в TModel3D удалось загрузить спутник состоящий из 34000 meshe’й. Так что, вопрос еще открыт
Vlad, будьте добры, переименуйте заголовок вебинара «FireMonkey — TMesh. Вебинар с Евгением Крюковым.». Евгения Крюкова в нём вообще не наблюдается и только вскользь упоминается спустя ~50 мин(за 2-3 минуты до конца).
Это видео смотрел только из-за названия, чтобы услышать Евгения Крюкова. А слушать
этого чмыря невозможно. Шмаркает, кашляет, шипит, кряхтит — ему лечиться от простуды нужно (в психушке) или учиться (подготавливать себя к лекции в ванной комнате)! Совсем не приятно, когда чмо с хрюкающим звуком вытягивает сопли, а потом сглатывает их. Бррр!!
А в целом материал интересен, спасибо.