уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.

Долго собирался заняться изучением 3D в FireMonkey, но всё как-то не было подходящей идеи. Что можно сделать такого в 3D не только ради «вау-эффекта», но и что бы свою работу где-либо применить? Всякие летающие кубики, шарики и бегающие по форме зеленые человечки — это, конечно, здорово, но…для чего? В общем думал я думал и придумал где применить свои навыки работы с 3D в FireMonkey. Буквально в конце прошлого 2012 года, те кто участвовал в конкурсе в блоге Всеволода Леонова наверняка узнали, что автор блога WebDelphi как-то связан с охраной окружающей среды…Наверняка сейчас те, кто этого не знал удивились :) Не удивляйтесь и поверьте мне на слово — в промышленной экологии математики,  физикой и даже программирования хватает…иногда даже с избытком. Так вот  решил я с помощью FireMonkey 3D визуализировать процесс загрязнения атмосферы. Нормальная такая задачка, обычно решаемая на плоскости. Ну, а нормально построенная 3D-модель повысит наглядность + можно же будет без проблем представлять картинку как угодно: сверху, сбоку, построит разрез поля и т.д. В общем должно получиться нормальное такое наглядное пособие для великих умов завтрашнего дня.

Краткое введение в суть задачи

Чтобы лишний раз не грузить Вас кучей законов и формул, которые используются в расчётах (думаю, что никому, кроме меня это будет неинтересно) я просто скажу, что визуализируемый мной процесс (загрязнение атмосферы) детально расписан в теории атмосферной диффузии. Жуткое такое название, которое, собственно, себя оправдывает. Если хотите вкратце узнать что это за теория и как она выглядит — ссылочка на небольшой отрывок из книги. Сей отрывок даст примерное представление о том, что необходимо запрограммировать в Delphi, чтобы решить задачу.

Если рассматривать процесс поверхностно, очень приближенно, то загрязнение одним источником (например, дымовой трубой) можно представить в виде вот такого рисунка:image039

Кривая на графике — это значение концентрации вещества на различных расстояниях от источника. Т.е. она (концентрация) сначала растет, потом достигает максимума и падает, но никогда не достигает нуля.

На практике же поля загрязнения строятся с учетом направления ветра. Например, одна из программ, которая умеет строить такие поля, считает поле по сектору направления ветров от 0 до 360 градусов и в итоге получаются вот такие изолинии концентраций:

eco3

Поле загрязнения для группы источников

Если строить таким образом поле загрязнения для одного источника, то на виде сверху мы получим ряд концентрических окружностей с центром в точке расположения источника, на виде сбоку — см. первый рисунок. Что получится в объеме тоже можно представить в уме…хотя зачем нам что-то представлять в уме, когда есть FireMonkey 3D? :) Вот, собственно, очень коротко и сжато я подвел разговор к постановке задачи — построить поле загрязнения в 3D.

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

Наглядные примеры FireMonkey 3D

Какие материалы были изучены перед тем, как я засел в Delphi? Вот, что было предварительно изучено.

Визуализация математических функций

В качестве отправной точки в решении задачи я решил воспользоваться примером из статьи «Visualizing mathematical functions by generating custom meshes using FireMonkey«. На данный момент это, практически единственный пример, достаточно детально описывающий работу с TMesh. Похожая публикация также имеется и в «Блоге Delphi программиста», где эта же задача расписана чуть более подробно.

Пример, что тут говорить, действительно интересный. Выглядит красиво:

Пример использования TMesh

Пример использования TMesh

Но в 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 — вниз; Z — вглубь экрана

Если не учитывать направление 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. Например,

  1. если концентрация составляет от 0.1 до 0.5. от максимально допустимой нормы, то это будет соответствовать зеленому сектору в HSL,
  2. если от 0.5 до 0.7. — синему сектору HSL
  3. и т.д.

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

Если надо детализировать цвета внутри сектора, то можно это сделать написав элементарную функцию линейной интерполяции.

У меня сейчас выстраивается пятицветное поле. Грубовато, но для примера сойдет. В итоге я написал простенькую функцию:

function TCalculator.GetTextureY(Y: single): single;

Которая получает в качестве входного параметра значение концентрации в заданной точке и возвращает в результате значение 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 км. Плоская картинка:

Плоское поле для 3 источников

Плоское поле для 3 источников

Второй результат — поле для трех источников размером 10х10 км. Объемная картинка:

Объемное поле

Объемное поле

Третий результат — отдельный источник. Поле 10х10 км

Один источник

Один источник

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

Если получиться из этой затеи что-либо дельное — обязательно отпишусь.

s3img_4952069_66_2

0 0 голоса
Рейтинг статьи
уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.
Подписаться
Уведомить о
8 Комментарий
Межтекстовые Отзывы
Посмотреть все комментарии
Всеволод Леонов

Отличная статья — теперь теория атмосферной диффузии в 3D визуализации будет несомненно ближе к народу, т.к. пространственная картина степени заср@нности техногенного ландшафта (не пусть с технологическим, который также заср@н не меньше) заставит людей более серьезно относиться к проблеме экологии. Давеча я стоял в «жуткий» подмосковный мороз (аж -17, привет Евгению Крюкову с его -37 в Улан-Удэ) в станционном павильончике — ждал электричку, грелся. На стене разглядывал плакаты а-ля «Гражданская оборона» с информацией, устаревшей как вато-марлевая повязка. Там сообщалось, что в случае разлития хлора бежать нужно перепендикулярно ветру и избегать оврагов. Я думаю, сделав 3Д картину распространения хлорного облака в условиях… Подробнее »

Alex
Alex
30/01/2013 17:19

У меня вопрос чуть в сторону…
Данные получены с помощью GISTool?
Если да, то…
Как удалось «подружить» GISTool с FireMonkey?
Или это по схеме (GISTool+VCL)+FireMonkey

ramir
ramir
26/02/2013 16:23

Проблема с размерностью Mesh.Data.IndexBuffer в том, что можно сослаться только на 65536
вершин (элемент массива — Word).
Не совсем понимаю, почему разработчики FireMonkey наложили такое ограничение.
Т.е. я могу нарисовать плоскость максимум 255х255 точек.
Для большинства задач этого наверно хватит, но что делать, если вершин будет больше?
Например при решении задач методом конечных элементов или разностными схемами кол-во узлов легко может превысить 65000.
Для отрисовки таких систем придется каким-то образом разбивать на подобласти.

Alex
Alex
26/02/2013 17:30

Я столкнулся с такой проблемой, мне нужно отображать участок карты минимум 500х500. Ситуация не совсем понятная, так как на том же железе с использованием GLScene все работает нормально. Возможно там реализована программная «склейка». С другой стороны в TModel3D удалось загрузить спутник состоящий из 34000 meshe’й. Так что, вопрос еще открыт

SOAP
SOAP
04/07/2013 17:24

Vlad, будьте добры, переименуйте заголовок вебинара «FireMonkey — TMesh. Вебинар с Евгением Крюковым.». Евгения Крюкова в нём вообще не наблюдается и только вскользь упоминается спустя ~50 мин(за 2-3 минуты до конца).

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

А в целом материал интересен, спасибо.