Вы когда-нибудь играли в Outer Wilds? Планеты там невероятно красивы. Это собственно стало основной мотивацией создать свою простую модель планеты, используя реальные географические данные о высотах и немножко магии Wolfram Language

Финальная анимация
Финальная анимация

Как разбросать точки на сфере

Очевидно, даже если мы стащим карту высот земли возникнет проблема проекции ее на сферу. Это очень сложная тема с триангуляцией (ведь потом надо сделать из этого полигоны) и прочим, про нее довольно много написано. К примеру

Как один из вариантов - мы пойдем от обратного - сначала создадим пробные точки на сфере, а затем уже запросим по ним из гео-данных значения высот.

Итак, нам нужно равномерно распределить N точек по сфере. Не нужно изобретать велосипед, если есть богатая стандартная библиотека

Graphics3D[{
  Sphere[],
  Red, PointSize[0.01], SpherePoints[1000]//Point
}]
Разбросали...
Разбросали...

Географические данные

К счастью или сожалению, в стандартной библиотеке WL уже лежит грубая карта всей Земли (на всякий случай). Если нужно поточнее, она пойдет в интернет и добудет больше. Собственно, к делу

GeoElevationData[GeoPosition[Here]]
Quantity[490, "Meters"]

здесь Here возвращает текущую широту и долготу в градусах. В функцию можно передать не одно значение, но и целый список. Собственно этот список мы возьмем из точек на сфере

points = SpherePoints[5000];
latLon = (180/Pi) {90Degree - ArcCos[#[[3]]], ArcTan[#[[1]], #[[2]]]} &/@ points;

elevation = GeoElevationData[GeoPosition[latLon]];
elevation = (elevation + Min[elevation])/Max[elevation];

Здесь мы переходим из декартовой системы координат в сферическую (геодезическую точнее)

\begin{matrix} lat =& 90^\circ - arccos(z/r) \\ lon =& arctan(x/y)\end{matrix}

получаем высоты и нормируем их.

Остается связать их с исходными точками на сфере, используя нормированную высоту как расстояние по нормали

surface = MapThread[(#1 (0.8 + 0.1 #2))&, {points, elevation}];

здесь мы масштабируем их на глаз, чтобы высота над уровнем моря лишь слегка "модулировала" поверхность сферы, таким образом мы получим видимый рельеф Земли

rainbow = ColorData["DarkRainbow"];

ListSurfacePlot3D[
  surface, 
  Mesh->None, MaxPlotPoints->100, 
  ColorFunction -> Function[{x,y,z}, rainbow[1.5(2 Norm[{x,y,z}]-1)]], 
  ColorFunctionScaling -> False
]
Действительно похоже на Землю!
Действительно похоже на Землю!

Генерируем облака

Какая же Земля без облаков? Какие же облака без Шума Перлина? Следующий кусок я честно украл на одном из форумов (никаких GPT вам!)

n = 128;
k2 = Outer[Plus, #, #] &[RotateRight[N@Range[-n, n - 1, 2]/n, n/2]^2];

spectrum = With[{d := RandomReal[NormalDistribution[], {n, n}]},
   (1/n) (d + I d)/(0.002 + k2)]; 

spectrum[[1, 1]] *= 0;

im[p_] := Clip[Re[InverseFourier[spectrum Exp[I p]]], {0, ∞}]^0.5

p0 = p = Sqrt[k2];

Image[im[p0 += p]]
То, что доктор прописал
То, что доктор прописал

Остается другая проблема - как сделать их трехмерными? Я не придумал ничего лучше, как использовать технику Marching Cubes и сгенерить low-poly облака. Однако для начала "растянем" двумерное изображение в трехмерное с затуханием по краям

With[{plain = im[p0+=p]}, Table[plain Exp[-( i)^2/200.], {i, -20,20}]]

Для того, чтобы превратить это скалярное поле в полигоны можно воспользоваться функцией ImageMesh, однако с ней довольно тяжело работать, если требуется применить нелинейные преобразование к этой сетке. А нам придется их сделать, иначе, как потом натянуть этот квадрат на сферу?

Воспользуемся внешней библиотекой wl-marchingcubes

PacletRepositories[{
    Github -> "https://github.com/JerryI/wl-marching-cubes" -> "master"
}]

<<JerryI`MarchingCubes`
With[{plain = im[p0+=p]}, Table[plain Exp[-( i)^2/200.], {i, -20,20}]];

{vertices, normals} = CMarchingCubes[%, 0.2, "CalculateNormals" -> False];

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

Polygon[{
  {1,2,3}, //треугольник 1
  {4,5,6}, //треугольник 2
  ...
}]

где ни одна из вершин не переиспользуется другим треугольником. Такой формат особенно прост для GPU, так как нужно отправить лишь один такой список в один из буферов WebGL. К счастью примитив Polygon поддерживает такой вариант

GraphicsComplex[vertices, Polygon[1, Length[vertices]]] // Graphics3D
Напоминает облака
Напоминает облака

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

Натягиваем на сферу

Здесь мы сталкиваемся с такой же проблемой, что и ранее. Как натянуть это на сферу? Для иллюстративных целей попробуем Такое преобразование в лоб, как будто наши x,y координаты это сферические углы

Неверно!

где \rho = \rho_0 + \alpha ~ z/depth. Вопрос зачем здесь некий угол сдвига \theta - так как такое преобразование создает серьезные артефакты на полюсах сферы, можно попытаться косметически поправить их сдвинув полюс в область, где меньше геометрии.

Если кто-то из хабровчан знает способ получше - прошу в комментарии ?

См. UPD в конце статьи

Итак, попробуем этот вариант

vertices = Map[Function[v,
    With[{
      \[Rho] = 50.0 + 0.25 (v[[3]] - 10), 
      \[Phi] = 2.0 Pi v[[1]]/127.0, 
      \[Theta] =  Pi/2 + Pi v[[2]]/127.0
    },
      {
        \[Rho] Cos[\[Phi]] Cos[\[Theta]], 
        \[Rho] Sin[\[Phi]] Cos[\[Theta]], 
        \[Rho] Sin[\[Theta]]
      }
    ]
  ]
, vertices];

{
  clouds = GraphicsComplex[0.017 vertices, Polygon[1, Length[vertices]]]
} // Graphics3D
Вроде бы даже неплохо
Вроде бы даже неплохо

Собираем все вместе

Можно расширить исходный SurfacePlot опциями и добавить через них дополнительные графические примитивы. Ах да, еще отключить стандартное освещение. Мы же в космосе

rainbow = ColorData["DarkRainbow"];

ListSurfacePlot3D[
  MapThread[(#1 (0.8 + 0.1 #2))&, {points, elevation}], 
  Mesh->None, MaxPlotPoints->100, 
  ColorFunction -> Function[{x,y,z}, rainbow[1.5(2 Norm[{x,y,z}]-1)]], 
  ColorFunctionScaling -> False, 
  Lighting->None,
  PlotStyle->Directive["Shadows"->True, "CastShadow"->True],

  Prolog -> {
    Directive["Shadows"->True, "CastShadow"->True],
    clouds,
    HemisphereLight[LightBlue, Orange // Darker // Darker],
    SpotLight[Orange, {-2.4909, 4.069, 3.024}]
  },
  
  Background->Black
, ImageSize->800]
Почти финальный результат
Почти финальный результат

Можно поиграться с освещением в реальном времени, добавив обработчик на свойство transform мелкой сферы на месте источника света

rainbow = ColorData["DarkRainbow"];
lightPos = {-2.4909, 4.069, 3.024};


ListSurfacePlot3D[
  MapThread[(#1 (0.8 + 0.1 #2))&, {points, elevation}], 
  Mesh->None, MaxPlotPoints->100, 
  ColorFunction -> Function[{x,y,z}, rainbow[1.5(2 Norm[{x,y,z}]-1)]], 
  ColorFunctionScaling -> False, 
  Lighting->None,
  PlotStyle->Directive["Shadows"->True, "CastShadow"->True],

  Prolog -> {
    Directive["Shadows"->True, "CastShadow"->True],
    clouds,
    HemisphereLight[LightBlue, Orange // Darker // Darker],
    SpotLight[Orange, lightPos // Offload],
    EventHandler[Sphere[lightPos, 0.001], {"transform" -> Function[t,
      lightPos = t["position"]
    ]}]
  },
  
  Background->Black
, ImageSize->800]

Анимация

Добавим простенькую анимацию движения источника света, а также вращения сферы облаков, чтобы разнообразить сцену

rainbow = ColorData["DarkRainbow"];
lightPos = {-2.4909, 4.069, 3.024};

rotationMatrix = RotationMatrix[0., {0,0,1}];
angle = 0.;

animation = CreateUUID[];

EventHandler[animation, Function[Null,
  lightPos = RotationMatrix[1 Degree, {1,1,1}].lightPos;
  rotationMatrix = RotationMatrix[angle, {0,0,1}];
  angle += 0.5 Degree;
]];

ListSurfacePlot3D[
  MapThread[(#1 (0.8 + 0.1 #2))&, {points, elevation}], 
  Mesh->None, MaxPlotPoints->100, 
  ColorFunction -> Function[{x,y,z}, rainbow[1.5(2 Norm[{x,y,z}]-1)]], 
  ColorFunctionScaling -> False, 
  Lighting->"Default",
  PlotStyle->Directive["Shadows"->True, "CastShadow"->True],

  Prolog -> {
    Directive["Shadows"->True, "CastShadow"->True],
    GeometricTransformation[clouds, rotationMatrix // Offload],
    HemisphereLight[LightBlue, Orange // Darker // Darker],
    SpotLight[Orange, lightPos // Offload]
  },

  Epilog -> AnimationFrameListener[lightPos // Offload, "Event"->animation],
  Background->Black
]

Чтобы запустить движение нужно "пнуть" обработчик, тогда обновление позиции источника света вызовет прерывание, которое снова обновит позицию светового тела и так далее синхронно с циклом отрисовки браузера

Посмотреть в живую

Среда разработки

Все примеры были показаны в открытой среде-разработки-блокноте WLJS Notebook

Спасибо за внимание ??‍♂️

UPD 10.01.2025

Как упомянули в комментариях такой способ проекции облаков очень наивный с очевидной особенностью на полюсах.

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

В целом нужно решить две проблемы

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

n = 256;
ratio = 1/2;

k2 = Outer[Plus, #, #] &[RotateRight[N@Range[-n, n - 1, 2]/n, n/2]^2];

k2 = k2[[;; ;; 1/ratio, All]];

spectrum = With[{d := RandomReal[NormalDistribution[], {n ratio, n}]},
   (1/n) (d)/(0.001 + k2)]; 
spectrum[[1, 1]] *= 0;

im[p_] := Clip[Re[InverseFourier[spectrum Exp[I p]]], {0, ∞}]^0.5

p0 = p = Sqrt[k2];

cloudTexture = im[p0 += p] // Image;

В повторяемости можно убедиться

Table[cloudTexture, {2}, {2}] // Grid 
  • преобразовать либо сетку, либо исходный шум, чтобы избежать искажений на полюсах

Я сразу упомяну, что не являюсь экспертом в области геодезии, а лишь "слышал" о некоторых концептах из вычислительной геометрии и 3D графики ;)

В любом случае мы будет "натягивать" эту текстуру в том или ином виде (в виде полигонов) на сферу. Это означает мы преобразуем все ее точки следуя одной из картографических проекций. В 3D графике есть распространенный способ UV-mapping, что является аналогом равнопромежуточной картографической проекции с точностью до коэффициентов, отвечающих за сдвиги углов.

При условии, что наши исходные координаты uv нормированы в пределах от 0 до 1

\begin{matrix}x \rightarrow \rho ~ cos(\phi) sin(\theta) \\ y \rightarrow \rho ~sin(\phi)sin(\theta)\\z\rightarrow \rho ~ cos(\theta)\end{matrix}

где \phi = 2\pi u, \theta = \pi v и \rho это высота поверхности в заданной точке. В конечном итоге нам потребуется проецировать не текстуру, а уже вершины полигонов построенные по ней. Так как это преобразование - в целом - линейное (мы соединяем точки внутри той же текстуры в треугольники и масштабируем все целиком), формулы не поменяются.

Представим, если мы совершим некое обратное преобразование над нашей исходной текстуре, то при проецировании ее на сферу они должны компенсировать друг-друга и мы увидим неискаженное изображение облаков. Тогда вопросы:

  • Чем является сейчас наша исходная текстура и что мы хотим увидеть в конце?

  • Что является неискаженным изображением?

  • Как мы в целом можем говорить о том, что мы хотим увидеть на сфере глядя на прямоугольное изображение шума Перлина в декартовой системе координат?

На эти вопросы можно было бы ответить в этой статье, если бы я был экспертом в поставленной теме. К сожаления, я не могу удовлетворять этому критерию, поэтому хотя бы поищем путь избавления от артефактов на полюсах. Глядя на текстуры из игр можно увидеть закономерность

https://richardrosenman.com/shop/spherical-mapping-corrector/

По краям текстуры, спроецированной со сферы детали растягиваются к верхнему и нижнему краям прямоугольника. Это как раз случай из вышеприведенной формулы, когда \theta \rightarrow 0, \pi или v \rightarrow 0, 1 - наши полюса.

Таким образом, если мы искусственно исказим исходную текстуру таким же образом, мы должно избавиться от особенностей на полюсах. Знаете на что похоже это преобразование?

Приведение проекции Мольвейде к равнопромежуточной картографической проекции

lat[y_, rad_:1] := ArcSin[(2 theta[y, rad] + Sin[2 theta[y, rad]])/Pi];
lon[x_, y_, rad_:1, lonzero_: 0] := lonzero + Pi x/(2 rad Sqrt[2] Cos[theta[y, rad]]);
theta[y_, rad_:1] := ArcSin[y/(rad Sqrt[2])];
mollweidetoequirect[{x_, y_}] := {lon[x, y, 1], lat[y]};

cloudTexture

newCloudTexture = ImageForwardTransformation[
  cloudTexture,
  mollweidetoequirect,
  DataRange -> {{-2 Sqrt[2], 2 Sqrt[2]}, {-Sqrt[2], Sqrt[2]}},
  PlotRange -> All
]

Остается повторить Marching Cubes на ней и преобразовать все вершины с помощью формулы выше

With[{plain = ImageData[newCloudTexture]}, Table[plain Exp[-( i)^2/200.], {i, -20,20}]];

{vertices, normals} = CMarchingCubes[%, 0.2, "CalculateNormals" -> False];

vertices = With[{
  offset = {Min[vertices[[All,1]]], Min[vertices[[All,2]]], 0},
  maxX = Max[vertices[[All,1]]] - Min[vertices[[All,1]]],
  maxY = Max[vertices[[All,2]]] - Min[vertices[[All,2]]]
}, Map[Function[v, With[{p = v - offset}, {p[[1]]/maxX, p[[2]]/maxY, p[[3]]}]], vertices]];

pvertices = Map[Function[v,
    With[{\[Rho] = 50.0 + 0.25 (v[[3]] - 10), \[Phi] =  2 Pi Clip[v[[1]], {0,1}], \[Theta] =  Pi Clip[v[[2]], {0,1}]},
      {\[Rho] Cos[\[Phi]] Sin[\[Theta]], \[Rho] Sin[\[Phi]] Sin[\[Theta]], \[Rho] Cos[\[Theta]]}
    ]
  ]
, vertices];

{
  clouds = GraphicsComplex[0.017 pvertices, Polygon[1, Length[vertices]]]
} // Graphics3D
Лучше? Лучше.
Лучше? Лучше.

Теперь собираем все вместе

Бонус: нормали облаков

Плоское затенение это может быть не так красиво. Давай-те включим расчёт нормалей и и спроецируем их также на сферу

With[{plain = ImageData[newCloudTexture]}, Table[plain Exp[-( i)^2/200.], {i, -20,20}]];

{vertices, normals} = CMarchingCubes[%, 0.2];

vertices = With[{
  offset = {Min[vertices[[All,1]]], Min[vertices[[All,2]]], 0},
  maxX = Max[vertices[[All,1]]] - Min[vertices[[All,1]]],
  maxY = Max[vertices[[All,2]]] - Min[vertices[[All,2]]]
}, Map[Function[v, With[{p = v - offset}, {p[[1]]/maxX, p[[2]]/maxY, p[[3]]}]], vertices]];

{pverticesn, pnormals} = MapThread[Function[{v,n},
    With[{\[Rho] = 50.0 + 0.25 (v[[3]] - 10), \[Phi] =  2 Pi Clip[v[[1]], {0,1}], \[Theta] =  Pi Clip[v[[2]], {0,1}]},
      {
        {\[Rho] Cos[\[Phi]] Sin[\[Theta]], \[Rho] Sin[\[Phi]] Sin[\[Theta]], \[Rho] Cos[\[Theta]]},

        n[[3]] {Cos[\[Phi]] Sin[\[Theta]], Sin[\[Phi]] Sin[\[Theta]], Cos[\[Theta]]} + 
        n[[2]] {Cos[\[Theta]] Cos[\[Phi]],Cos[\[Theta]] Sin[\[Phi]],-Sin[\[Theta]]} + 
        n[[1]] {-Sin[\[Theta]] Sin[\[Phi]],Cos[\[Phi]] Sin[\[Theta]],0}
      }
    ]
  ]
, {vertices, -normals}] // Transpose;

{
  clouds = GraphicsComplex[0.017 pvertices, Polygon[1, Length[vertices]], VertexNormals->pnormals]
} // Graphics3D

Если в GraphicsComplex задано явно VertexNormals, тогда при затенении поверхность будет интерполироваться по всем трем нормалям треугольника, вместо одной (рассчитывается автоматически графической библиотекой если других данных нет, тогда полигон выглядит как плоскость aka эффект из демосцен 80х).

И теперь соберем финальное изображение

Комментарии (21)


  1. KirillBelovTest
    09.01.2025 16:10

    Хабру нужно больше таких статей!


  1. karmael
    09.01.2025 16:10

    все прекрасно сударь, и не принимайте близко к сердцу, но!

    1. Ну Солнце то казалось бы физику, бог велел по орбите крутить, ну пускай даже вокруг "земли"

    2. Нуба в ДЗЗ выдают полюса =)


    1. JerryI Автор
      09.01.2025 16:10

      ;D спасибо

      1. Так и выходит, солнце матрицей поворота крутит по орбите (система отсчета на Земле)

      2. Блин, пытался раскодировать, так и не понял про Д33. ;(


      1. JerryI Автор
        09.01.2025 16:10

        А понял, вы про полюса облаков. Я так кстати и не понял, как можно меш квадратный натянуть на сферу. Вычислительная геометрия подсказывает, что без искажений - никак. А marching cubes на большом поле делать довольно дорого (если сразу шум генерить на сфере)

        Так что - если знаете - пишите!


        1. karmael
          09.01.2025 16:10

          ДЗЗ - дистанционное зондирование земли, отрасль полная перепроецирования приполярных снимков в меркатор (одна из систем проекций). проблема там буквально Ваша, при "растягивании" приполярной области, чем она ближе к полюсу, тем больше уходит в бесконечность.

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

          посложней, там полярные области проецируют не в меркатор, а в свой полярный EPSG (Coordinate Systems Worldwide), и потом уже жульничают на глобусе, склеивая по нужному меридиану.


          1. JerryI Автор
            09.01.2025 16:10

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

            Спасибо! Попробую


          1. Jijiki
            09.01.2025 16:10

            а почему так? ведь есть формула сферы и кватернионы вроде и шаг известен и вроде. сделал шаг обсчитал квадратик. сейчас перепроверился по кватерниону вроде всё сходится и будет +- как широкая обоя кинутая определенным образом из шага угла начиная с какогото полюса


            1. karmael
              09.01.2025 16:10

              потому, что в живой природе всё несколько сложней.

              Coordinate Conversions and Transformations including Formulas . - https://www.iogp.org/wp-content/uploads/2019/09/373-07-02.pdf


  1. iushakov
    09.01.2025 16:10

    Другой подход к созданию планеты показан тут - https://m.youtube.com/watch?v=lctXaT9pxA0

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


    1. JerryI Автор
      09.01.2025 16:10

      Верно! Тоже видел, и как я понял, облака у него шейдером сделаны. Довольно физично


  1. JerryI Автор
    09.01.2025 16:10

    Попробовал улучшить ситуацию. Берем текстуру шума (на этот раз 256х128)

    n = 256;
    k2 = Outer[Plus, #, #] &[RotateRight[N@Range[-n, n - 1, 2]/n, n/2]^2];
    
    spectrum = With[{d := RandomReal[NormalDistribution[], {n, n}]},
       (1/n) (d + I d)/(0.002 + k2)]; 
    spectrum[[1, 1]] *= 0;
    
    im[p_] := Clip[Re[InverseFourier[spectrum Exp[I p]]], {0, ∞}]^0.5
    
    p0 = p = Sqrt[k2];
    
    testImg = im[p0 += p][[128;;, All]] // Image 

    преобразуем

    lat[y_, rad_:1] := ArcSin[(2 theta[y, rad] + Sin[2 theta[y, rad]])/Pi];
    lon[x_, y_, rad_:1, lonzero_: 0] := lonzero + Pi x/(2 rad Sqrt[2] Cos[theta[y, rad]]);
    theta[y_, rad_:1] := ArcSin[y/(rad Sqrt[2])];
    mollweidetoequirect[{x_, y_}] := {lon[x, y], lat[y]};
    
    testImg = ImageForwardTransformation[
      testImg,
      mollweidetoequirect,
      DataRange -> {{-2 Sqrt[2], 2 Sqrt[2]}, {-Sqrt[2], Sqrt[2]}},
      PlotRange -> All
    ]

    Получаем следующее изображение

    Затем, как обычно Marching Cubes

    With[{plain = ImageData[testImg]}, Table[plain Exp[-( i)^2/200.], {i, -20,20}]];
    
    {vertices, normals} = CMarchingCubes[%, 0.2, "CalculateNormals" -> False];
    vertices = With[{
      offset = {Min[vertices[[All,1]]], Min[vertices[[All,2]]], 0},
      maxX = Max[vertices[[All,1]]] - Min[vertices[[All,1]]],
      maxY = Max[vertices[[All,2]]] - Min[vertices[[All,2]]]
    }, Map[Function[v, With[{p = v - offset}, {p[[1]]/maxX, p[[2]]/maxY, p[[3]]}]], vertices]];

    Теперь проецируем эти вершины на сферу

    pvertices = Map[Function[v,
        With[{\[Rho] = 50.0 + 0.25 (v[[3]] - 10), \[Phi] =  2 Pi Clip[v[[1]], {0,1}], \[Theta] =  Pi Clip[v[[2]], {0,1}]},
          {\[Rho] Cos[\[Phi]] Sin[\[Theta]], \[Rho] Sin[\[Phi]] Sin[\[Theta]], \[Rho] Cos[\[Theta]]}
        ]
      ]
    , vertices];
    
    {
      clouds = GraphicsComplex[0.017 pvertices, Polygon[1, Length[vertices]]]
    } // Graphics3D

    Видно другие артефакты, однако, на этот раз из-за того, что текстуру нельзя замостить, как я предполагаю...

    И это тоже интересная задача!


    1. karmael
      09.01.2025 16:10

      ну совсем же другое дело =)


      1. JerryI Автор
        09.01.2025 16:10

        Добавил в конце статьи. Задокументировал, но не без вопросов конечно.


  1. olegat19653
    09.01.2025 16:10

    Это что ИИ?


  1. Germanjon
    09.01.2025 16:10

    Пять лет назад пытался решить задачу "Как автоматически найти в горах Узбекистана высокие водопады". Тогда всё казалось простым - берёшь карту высот, определяешь водотоки; берёшь водотоки, определяешь падения высот.
    На практике - не удалось найти в открытых данных карту высот с хорошим разрешением и проект остановился.
    Интересно, сейчас ситуация получше?


    1. karmael
      09.01.2025 16:10

      очевидно что вы не там ищете, или просто не знаете необходимых терминов. карта высот называется srtm и выглядит как геопривязаный tiff в котором значение пикселя буквально высота. добывался примитивным гуглежом испокон веков как мне кажется


      1. Germanjon
        09.01.2025 16:10

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


        1. karmael
          09.01.2025 16:10

          30 метров на пиксель, при полном покрытии, это весьма приличное разрешение. Более того, достаочное что бы поискать большой водопад, и понять что ваш метод не пригоден для этого


    1. karmael
      09.01.2025 16:10

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


    1. karmael
      09.01.2025 16:10

      ну в конце то концов, - https://earthexplorer.usgs.gov/


  1. NEO-KLIZZERX
    09.01.2025 16:10

    Это прекрасно! Многим из хабра стоит у вас поучится