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

Код здесь написан языке Python, mesh импортируется из .stl файла. Откуда он будет импортироваться совершенно неважно, Python дальше будет работать с данными, которые имеют тип массивов numpy.

данные выглядят так
# Треугольники (список треугольников, в каждый из которых входят 3 точки)
[[[-5.0500002e+00  0.0000000e+00  0.0000000e+00]
  [-4.9604182e+00  2.5921434e-02 -3.2746387e-03]
  [-4.9604182e+00  2.5664667e-02 -4.8957970e-03]]

 [[-4.9604182e+00  2.6075900e-02 -1.6405566e-03]
  [-4.9604182e+00  2.5921434e-02 -3.2746387e-03]
  [-5.0500002e+00  0.0000000e+00  0.0000000e+00]]

 [[-4.9604182e+00  2.6127456e-02  0.0000000e+00]
  [-4.9604182e+00  2.6075900e-02 -1.6405566e-03]
  [-5.0500002e+00  0.0000000e+00  0.0000000e+00]]

 ...

 [[-4.8888855e+00  4.6034887e-02 -2.8962698e-03]
  [-4.9604182e+00  2.5921434e-02 -3.2746387e-03]
  [-4.9604182e+00  2.6075900e-02 -1.6405566e-03]]

 [[-4.8888855e+00  4.6034887e-02 -2.8962698e-03]
  [-4.9604182e+00  2.6075900e-02 -1.6405566e-03]
  [-4.8888855e+00  4.6125907e-02  0.0000000e+00]]

 [[-4.8888855e+00  4.6125907e-02  0.0000000e+00]
  [-4.9604182e+00  2.6075900e-02 -1.6405566e-03]
  [-4.9604182e+00  2.6127456e-02  0.0000000e+00]]]
# Нормали к треугольникам (вектора)
[[ 0.27986652 -0.94821906  0.15018432]
 [ 0.27986658 -0.95577824  0.09034719]
 [ 0.27986655 -0.95956516  0.03015531]
 ...
 [ 0.26912326 -0.95883155  0.09063575]
 [ 0.26912323 -0.96263045  0.03025224]
 [ 0.26912326 -0.96263045  0.03025234]]
И здесь это дело импортируется из STL файла, используя библиотеку numpy-stl
# pip install numpy-stl  # чтобы установить библиотеку
my_mesh = mesh.Mesh.from_file('название_файла.STL')
Normals = my_mesh.normals 
Triangles = my_mesh.vectors

# и сразу нормализуем нормали, на всякий пожарный
Nnorm = np.linalg.norm(Normals, axis=1) 
Nnorm = np.tile(Nnorm.reshape(len(Nnorm), 1), (1, 3))
Normals = Normals/Nnorm

Теперь у нас есть треугольники и единичные нормали к ним. Но для дальнейшей работы еще понадобится список площадей треугольников и их центров масс.

Получаем площади треугольников и центры масс треугольников
A, B, C = Triangles[:, 0], Triangles[:, 1], Triangles[:, 2] 
R1, R2 = C-A, B-A

# список центров масс
R = 1/3*(A+B+C)

# список площадей 
S = np.cross(R1, R2)
S = np.linalg.norm(S, axis=1)/2

Теперь у нас есть:

  • S - список площадей треугольников

  • R - список центров масс треугольников

  • Normals - список нормалей к треугольникам

  • Triangles - список треугольников (треугольник содержит 3 точки)

Теперь следует напомнить как вычисляются (чисто математически) массы, объемы, площади, центры масс, моменты инерции и что вообще это такое и зачем вообще нужно.

А те кто в теме и сомневаются, что все эти вещи можно вычислять для объемных тел используя только 2d сетку, скажу: это делать можно. И нужно, если лень разбивать 3d область пространства на маленькие элементы - тетраэдры там, призмы... Да, это делать возможно, и мы это сделаем. Но всё по порядку.

Масса - вычисляется как интеграл по объему (площади, линии):

m = \iiint \mathit{\rho dV}

здесь под интегралом (тройным) - плотность вещества. Объемная плотность вещества, имеющая размерность кг/м^3. Но, если в наличии имеется поверхностная плотность (кг/м^2), или линейная (?? кг/м), то интегралы соответственно будут двойные и простые.

Объяснять зачем нужна масса мало кому нужно, вернее об это знают не только лишь все. Масса на ускорение - это сила. Второй закон Ньютона, известный всем еще с седьмого класса.

Для большинства приложений в инженерии достаточно работать с плотностями, которые постоянны в пространстве и во времени =) Потому мы упростим себе жизнь и вынесем за все интегралы с которыми будем работать все плотности. И даже работать мы будем не с массами, а с площадями и объемами. А когда будем вычислить центры масс, плотности вообще сами по себе сократятся, и неважны.

Кстати, центр масс:

\overrightarrow r_{\mathit{цм}}=\frac{\iiint \overrightarrow r\mathit{dV}} V

Ну или момент первого порядка, или как там его называют в статистике.

В инженерном деле (конструкторском) центр масс важен, например, для описания движения твердых (недеформируемых) тел. Конструктора помещают туда связанную с телом координатную систему (как-то располагая её относительно тела, под каким-то углом в смысле) и с этих пор тело и связанная кс неразлучны друг с другом и неподвижны друг относительно друга.

Зачем помещают связанную систему в центр масс? Ну так принято, пойдем дальше.

Вот мы пришли к моментам инерции. Момент инерции - это то, что влияет на выбор конструктора, когда перед ним встает задача: как именно закрепить на теле связанную координатную систему.

Удачный выбор закрепления будет влиять вполне конкретно на уравнения движения этого самого тела. Момент инерции - это аналог массы во втором законе Ньютона для точки. Еще говорят "тензор инерции" - он участвует в описании вращательного движения тела, но это далеко не скалярная величина, это не просто число. Тензор инерции имеет валентность матрицы, то есть таблицы порядка 2 на 2 или 3 на 3, в зависимости от размерности пространства.

Ну а закрепляют кс исходя из симметрии тела (корабли, самолеты, дирижабли.. ) имеют зачастую симметрию относительно вертикальной плоскости. Вот поэтому одна из координатных плоскостей кс совпадает с ней. А дальше у нас остается только одна степень свободы как расположить - угол. И обычно одну из осей кс, находящихся в вертикальной плоскости, направляют горизонтально, параллельно земле. Тогда тензор инерции будет иметь почти идеальный диагональный вид, и, кстати, его можно было бы сделать диагональным в любом случае, для любого тела, но всё же кс располагают так как я описал. Ну чтобы диагональные элементы имели более ясный, практичный физический смысл. Что-то далеко я отошел от темы, вычисляется момент инерции так:

J_i=m_i\left[\begin{matrix}y_i^2+z_i^2&-x_iy_i&-x_iz_i\\…&x_i^2+z_i^2&-y_iz_i\\…&…&x_i^2+y_i^2\end{matrix}\right]

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

А еще тензор инерции можно пересчитывать в другие координатные системы смещенные относительно центра масс по теореме Гюйгенса. То есть, есть формула, связывающая моменты инерции в разных координатных системах, и зная в одном можно найти в другом имея только лишь вектор смещения. Но важно! теорема Гюйгенса работает только когда одна из кс расположена в центре масс. Если есть две кс вне центра масс, в формуле будут появляться дополнительные члены (моменты первого порядка).

не нужно это читать

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

Вообще и дизлайками не брезгую..

Итак, достаточно. Всё что нужно напомнилось (было напомнено), короче, вспомнено. Восполнено, ладно, клоунада как клоунада, она иногда надо.

А давайте посчитаем все эти вещи просто для поверхности, чтобы набить немного руку?

Масса поверхности получается как сумма площадей всех треугольников умноженная на поверхностную плотность:

m_{\mathit{об}}=ρ_{\mathit{пленки}} \cdot \sum _iS_i
def get_m_obolochka(S, rho):
    m = np.sum(S) * rho
    return m

Центр масс поверхности (оболочки):

\overrightarrow r_{\mathit{цм}\mathit{оболочки}}=\frac 1{m_{\mathit{об}}}\iint \overrightarrow rρ_{\mathit{пленки}}\mathit{dS}≈\frac{ρ_{\mathit{пленки}}}{m_{\mathit{об}}}\sum _i\overrightarrow r_iS_i=\frac{\sum _i\overrightarrow r_iS_i}{\sum _iS_i}
def get_cm_obolochka(S, R):
    S_sum = np.sum(S)
    cm = np.einsum('ij,i', R, S) / S_sum
    return cm

Момент инерции оболочки:

J_{\mathit{об}}=ρ_{\mathit{пленки}}\sum _iJ_i \cdot S_i

где матрица J_iвычисляется по приведенной где-то выше формуле, а еще здесь она не умноженная на i-тую массу.

def get_J_for_ob(rho, R, S):
	x, y, z = R[:, 0], R[:, 1], R[:, 2]
	x2, y2, z2 = x**2, y**2, z**2

	Jx, Jy, Jz = y2+z2, x2+z2, x2+y2
	Jxy, Jyz, Jxz = -x*y, -y*z, -x*z

	J = np.array([	[Jx, Jxy, Jxz],
									[Jxy, Jy, Jyz], 
									[Jxz, Jyz, Jz]
							])
	J = J.T # это нужно, потому что если подумать, то нам нужен список J[i] для всех треугольников
	# короче делаем выше правильный shape =)
  J = np.einsum('ijk,i->jk', J, S)*rho
	return J

Теперь, когда размялись, можно приступать к основному, тому, что написано в названии статьи. Вычислим все эти характеристики для объемного тела, которое ограничено mesh-ем, используя только mesh.

Для этого понадобится, внимание... теорема Остроградского-Гаусса:

\iint \left(\overrightarrow F,\overrightarrow n\right)\mathit{dS}=\iiint div \left(\overrightarrow F\right)\mathit{dV}

На мой взгляд это самая красивая теорема математики... на самом деле она вроде как есть частный случай операций над дифференциальными формами, но я не слишком математик, поверхностно знаком ;) Да... там и теорема Гаусса-Бонне и все в этом духе, это топология, детка. Это всё очень красиво, и может быть я этим когда-нибудь займусь, когда меня освободят из рабства.

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

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

Пренебрегая философией, и подбирая векторное поле следующим образом:

\overrightarrow F=\frac 1 3  \left[x,y,z\right]

мы видим, что дивергенция становится равна:

div \left(\overrightarrow F\right)=\frac{∂F_x}{∂x}+\frac{∂F_y}{∂y}+\frac{∂F_z}{∂z}=1

Это означает, что теперь зная лишь mesh поверхности, мы можем вычислить объем:

\iiint \mathit{dV}=V=\iint \left(\overrightarrow F,\overrightarrow n\right)\mathit{dS}≈\sum _i\left(\overrightarrow F_i,\overrightarrow n_i\right)S_i

Можно сказать, что здесь:

\overrightarrow F_i=\frac 1 3 \overrightarrow R_i

(Центры масс треугольников, напоминаю)

Код:

def Volume(S, R, Normals):
	V = np.einsum('ij,ij,i', R, Normals, S)*1/3 # einsum как работает можно найти в документации
	return V

Но можно обнаглеть, и пойти дальше объема:

\overrightarrow r_{\mathit{цм}}=\frac{\iiint \overrightarrow r\mathit{dV}} V

Используя ту же теорему и задав вектор F так (для вычисления координаты x):

\overrightarrow F^x=\frac 1 2 \left[x^2,0,0\right]

(чего-то индекс x не туда залез, я его хотел чутка ниже поместить)

Его дивергенция:

div \left(\overrightarrow F^x\right)=\frac 1 2\left(\frac{∂x^2}{∂x}+0+0\right)=x

Координата x центра масс тела:

x_{\mathit{цм}}=\frac 1 V\iiint \mathit{xdV}=\frac 1 V\iiint div \left(\overrightarrow F^x\right)\mathit{dV}=\frac 1 V\iint \left(\overrightarrow F^x,\overrightarrow n\right)\mathit{dS}≈\frac 1 V\sum _i\left(\overrightarrow F^x_i,\overrightarrow n_i\right)S_i

Аналогичным образом вычисляются остальные две координаты. Компактно это можно записать если ввести операцию покоординатного умножения (использующегося в numpy):

\overrightarrow a\ast \overrightarrow b=\left[a_xb_x,a_yb_y,a_zb_z\right]\overrightarrow r_{\mathit{цм}}=\frac 1 V\sum _i\left(\overrightarrow F_i\ast \overrightarrow n_i\right)S_i

где

\overrightarrow F=\frac 1 2\left[x^2,y^2,z^2\right]

Воплощение в коде:

def get_cm_V(S, R, Normals, V_t):
	F = R**2/2
	cm = np.einsum('ij,ij,i->j', F, Normals, S)
	return cm/V_t

Для объемного тела подсчитать тензор инерции можно используя только триангуляцию оболочки, аналогично как делалось для подсчета объема и центра масс:

J_{\mathit{xx}}=ρ\iiint \left(y^2+z^2\right)\mathit{dV}=ρ\iiint div \left(\overrightarrow F\right)\mathit{dV}=ρ\iint \left(\overrightarrow F,\overrightarrow n\right)\mathit{dS}=ρ\sum _i\left(\overrightarrow F_i,\overrightarrow n_i\right)S_i

Где вектор F можно, например, задать так:

\overrightarrow F=\left[x\left(y^2+z^2\right),0,0\right]

Для компоненты J_{xy} должно выполняться:

div \left(\overrightarrow F\right)=-xy

Соответственно можно задать F:

\overrightarrow F=\left[0,0,-xyz\right]

Аналогично и с остальными компонентами.

def get_J_for_V(S, R, Normals, rho):
	x, y, z = R[:, 0], R[:, 1], R[:, 2]
	x2, y2, z2 = x**2, y**2, z**2

	Fx, Fy, Fz = x*(y2+z2), y*(x2+z2), z*(x2+y2)
	Fxyz = -x*y*z

	Fx, Fy, Fz = Fx[:, None], Fy[:, None], Fz[:, None]
	Fxyz = Fxyz[:, None]

	N = len(x)
	zero = np.zeros((N, 1))

	Fxx, Fyy, Fzz = np.hstack((Fx, zero, zero)), np.hstack((zero, Fy, zero)), np.hstack((zero, zero, Fz))
	Fxy, Fxz, Fyz = np.hstack((zero, zero, Fxyz)), np.hstack((zero, Fxyz, zero)), np.hstack((Fxyz, zero, zero))

	Jx, Jy, Jz = np.einsum('ij,ij,i', Fxx, Normals, S), np.einsum('ij,ij,i', Fyy, Normals, S), np.einsum('ij,ij,i', Fzz, Normals, S)
	Jxy, Jyz, Jxz = np.einsum('ij,ij,i', Fxy, Normals, S), np.einsum('ij,ij,i', Fyz, Normals, S), np.einsum('ij,ij,i', Fxz, Normals, S)

	J = np.array([	[Jx, Jxy, Jxz],
					[Jxy, Jy, Jyz], 
					[Jxz, Jyz, Jz]
				])*rho

	return J
# не так элегантно, но работает вроде.. 

На этом всё, спасибо за внимание.

еще

Если кому не нравится мой стиль повествования и раздражает или обижает, не обижайтесь, пожалуйста, я тоже часто грущу на самом деле... грущу чаще, чем смеюсь и радуюсь, это нормально для таких как мы... Просто, когда сажусь писать на Хабр, чет пробивает на шутки какие-то дурацкие и клоунство. Ну как написал, так написал. Сейчас выложу.
И еще. Кому интересно, я занимаюсь дирижаблестроением, пока что маленьким, но вижу как в будущем из этого зерна прорастает сперва зелень, потом колос, потом полное зерно в колосе. И кому это интересно, пусть связывается со мной, может будем сеять вместе, и вместе пожинать плоды.. Можно тут в личных сообщениях, также я есть на фейсбуке, в телеграме.

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

И еще, скептикам, которые не верят, что маленькие дирижабли способны противостоять ветру:

да, это всего лишь симуляция, но я уверен, что она на 80-90 % совпадет с реальностью...

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

Подписывайтесь на мой ютуб-канал, это мой видео-дневник дирижаблестроителя

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


  1. Sdima1357
    22.09.2021 23:54

    А что произойдет если поверхноcть не замкнута? Типичный случай - небольшой дефект в триангуляции. В теории конечно проверяется(например нулевой суммой ориетированных проекций треугольников), но на практике...


    1. Andy_U
      23.09.2021 00:04

      Проще проверить, что нет ребер, принадлежащих одному треугольнику.


    1. ilmarin77
      23.09.2021 00:20
      +1

      интересней если там будут самопересечения поверхностей.


      1. Andy_U
        23.09.2021 01:51

        Типа бутылки Клейна?


        1. ilmarin77
          23.09.2021 02:18

          Например, или просто дефекты mesh'а.


          1. Andy_U
            23.09.2021 13:33

            Фокус с неориентируемой (односторонней поверхностью) и дырки в поверхности вызовут разные проблемы. Дырки приведут лишь к небольшими ошибкам.


  1. DreamShaded
    23.09.2021 04:46
    +2

    Тыща лайков для хабра вроде многовато)) может, снизите планочку?) Слог хороший, вас приятно читать, хотелось бы ещё материалов в таком ключе


  1. avdx
    23.09.2021 12:31
    +2

    Объем замкнутой триангулированной оболочки проще посчитать как сумму ориентированных объемов тетраэдров (треугольных призм), каждый из которых образован одним из треугольников оболочки (с учетом его ориентации) и произвольной точкой (например началом координат). Т.е. аналогично двумерному случаю, где площадь замкнутого невыпуклого многоугольника можно посчитать как сумму ориентированных площадей треугольников, образованных каждым ребром и произвольной точкой.
    На мой взгляд это проще для понимания и реализации, по крайней мере для вычисления нужны только операция умножения и сложения/вычитания и не нужно вычисление квадратного корня.


    1. Andy_U
      23.09.2021 13:29

      Как вы без квадратного корня посчитаете расстояние от поверхности до условного центра?


      1. avdx
        23.09.2021 13:33
        +1

        А для чего нужно считать это расстояние?


        1. Andy_U
          23.09.2021 13:50
          +1

          Ваша правда, оказывается, векторное-скалярное произведение дает точный результат. для объема тетраэдра.


    1. Rikhmayer
      23.09.2021 20:06

      Так там в формуле это самое, с произвольной точкой в нуле, нет? (Центр треугольника, нормаль)*площадь треугольника.


      1. avdx
        24.09.2021 11:52

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


        1. Rikhmayer
          24.09.2021 12:19

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

          А так получилось как в том мемчике:

          -вы интегралы считаете?

          -нет, только показываю

          -красивое