Без графического интерфейса и с небольшим количеством PGA создавайте сложные 3D-модели

1. Введение

Необходимым условием создания краткой мысленной модели мира является способность создавать краткие мысленные модели простых трехмерных (3D) объектов. Животные, в том числе и люди, усваивают краткие ментальные модели простых трехмерных объектов в очень раннем возрасте. На компьютере создание 3D-модели объекта обычно включает множество манипуляций с графическим пользовательским интерфейсом (GUI) в программном приложении для 3D-моделирования (например, в приложении CAD). Однако многие из этих манипуляций с графическим интерфейсом недоступны для слепых. В качестве доступной альтернативы 3D-модели на компьютере также могут быть созданы на основе кратких текстовых файлов, которые определяют операции проективной геометрической алгебры (PGA). Учитывая, что он более краткий, эта доступная альтернатива может быть ближе к тому, как мозг лаконично создает мысленные модели простых трехмерных объектов.

В этом эссе демонстрируется использование Julia (язык программирования) и Makie (пакет для построения графиков) и ripga3d.jl (эталонная реализация 3D-проективной геометрической алгебры, как описано здесь) для создания моделей всех 3D-печатных компонентов компьютера. Прибой!¹. Фактически, тесная связь между 3D-моделированием и 3D-печатью является ключевой концепцией в предлагаемом подходе к созданию 3D-моделей с помощью программирования. В частности, вместо определения функции, которая создает одну категорию трехмерных объектов (например, сферы), в этом эссе определяется более общая функция xrotate, которая сочетает в себе extrusion и вращение многоугольника для создания широкого спектра трехмерных объектов. категории объектов (например, сферы, цилиндры, диски, винты, все платоновы тела). Например, следующий полый цилиндр создается вращающимся прямоугольником, который выдавливает (т. е. записывает в файл 3D-модели) треугольные грани, аппроксимирующие поверхность цилиндра.

¹Что такое серфинг!? Для слепых людей, недовольных своей безработицей или неполной занятостью из-за высокой стоимости и ограниченных возможностей текущих инструментов повышения производительности (например, однострочное 80-символьное обновляемое устройство Брайля стоит 15 000 долларов США), Eduneer разрабатывает эталонную реализацию с очень низкой стоимостью. тактильный дисплей Surf! (сокращение от «поверхность»), который подключается к смартфонам для передачи и приема таких форм, как человеческие лица или несколько строк крупного шрифта Брайля.

2. Файлы моделей STL и файлы моделей OBJ

Широко используемый формат файла для 3D-объектов — STL (сокращение от STereoLithography), созданный 3D Systems. По сути, формат файла STL представляет собой список определений треугольников, где каждый треугольник определяется

  • три вершины треугольника,
  • единичный вектор нормали к грани треугольника, и
  • цвет треугольника.

Довольно часто единичный вектор треугольника устанавливается равным нулю, потому что его можно вычислить по трем вершинам. Также довольно часто цвет треугольника устанавливается равным нулю, потому что модель не требует определения цвета для каждого треугольника. Поэтому довольно часто 14 из 50 байтов определения треугольника (или 28%) устанавливаются равными нулю. Другим недостатком формата файла STL являются повторяющиеся вершины: учитывая, что большинство вершин в типичной 3D-сетке появляются более чем в одном треугольнике, более экономичный подход к хранению 3D-сетки будет состоять в том, чтобы определить каждую вершину только один раз, а затем обратиться к это с индексом.

Это то, что делает формат файла OBJ. Созданный Wavefront Technologies, формат файла OBJ представляет собой список определений вершин, за которым следует список определений треугольников, причем каждый треугольник определяет свои три вершины тремя индексами в списке вершин. Хотя это не самый популярный формат файла для 3D-объектов, формат файла OBJ — это формат файла 3D-объекта, используемый в этом эссе из-за его эффективности и простой иерархии:

  • определить список вершин с трехмерными координатами каждой вершины,
  • определить список граней треугольника, ссылаясь на индексы элементов в списке вершин,
  • (в приложении) определить список 3D-компонентов, ссылаясь на индексы элементов в списке треугольников, и
  • (опять же в приложении) определить 3D-объект, ссылаясь на индексы элементов в списке 3D-компонентов.

Удобно, что одни и те же 3D-файлы OBJ могут быть построены Джулией и Маки, а также нарезаны и напечатаны AnkerMake.

3. Отображаемые пиксели и индексированные объекты

В этом эссе предлагается, чтобы пользователи могли эффективно манипулировать индексами вершин, граней треугольников и компонентами объектов в качестве альтернативы манипулированию отображаемыми пикселями. Первоначально невозможность визуально манипулировать 3D-объектом может показаться ограничением. Однако на самом деле именно эталонные индексы свободны от ограничений трех измерений. Существует много приложений для объектов с более чем тремя измерениями (например, новейшее поколение радарных систем для автономных автомобилей создает 4D-вид окружения автомобиля, где 4-е измерение — это скорость по направлению к автомобилю).

4. Код Julia против текста командного файла

Командный файл для конкретного приложения (например, G-код) не обладает такими широкими возможностями, как язык программирования общего назначения (например, Julia), но командный файл для конкретного приложения обычно более лаконичен, чем код, написанный на языке программирования общего назначения. В этом эссе показаны оба подхода. Командные файлы настраиваемых приложений имеют расширение файла .gcd, которое является сокращением от GACAD, что является аббревиатурой от Geometric Algebra Computer Aided Design. Листинги кода общего назначения Julia показывают детали создания сетки, представляющей 3D-объект.

Ниже приведено приблизительное описание формата файла .gcd, используемого для указания создания 3D-моделей объектов.

# file: gacad.txt
#
# This file defines the format of files with the file
# extension .gcd, which is short for GACAD, which is an
# acronym for Geometric Algebra Computer Aided Design.
#
# Similar to G-code files, each line specifies a single
# command that starts with a single character specifying
# the command type. In .gcd files, here is the list of
# possible command types:
# E: Evaluate (e.g., set the value of a variable)
# G: G-code (e.g., uniquely shaped printer head positioning)
# H: polygon Header (e.g., flag to close polygon figure)
# P: polygon Point (e.g., an off-axis circle makes a toroid)
# U: Unit (e.g., "mm", "cm", "m", "inch", "ft", "yd")
# V: View (e.g., a rotatable 3D plot or an extrusion animation)
#
# And here are examples of optional parameters:
# H LABEL "hollow cylinder" ; optional, default: ""
# H FLAGS 0x4 ; optional, default: 0x7
# U cm ; default: mm
# V PLOT ; default: AX (i.e., Animated eXtrusion)
#

Следующие примеры демонстрируют гибкость файлов .gcd. Ранее показанное анимированное выдавливание полого цилиндра было создано с помощью следующего файла .gcd.

# file: _cyl.gcd (model of hollow cylinder)
E ri = 1.85 # inner radius
E ro = 2.6 # outer radius
E h = 2*ri # cylinder height
P 0 0 0
P 0 0 h
P ri 0 h
P ro 0 h
P ro 0 0
P ri 0 0
V AX # View Animated eXtrusion

В приведенной ниже спецификации внутренняя сторона полого цилиндра удаляется путем очистки флага isClosed (бит 0).

# file: _cylf6.gcd (model of hollow cylinder; flags=0x6)
#
# There are currently three flags (default value
# for each is true) that control the generation
# of vertices and triangle faces:
# bit 0: isClosed
# bit 1: isHollow
# bit 2: isCentered
#
# This example clears the isClosed flag, meaning
# the polygon's end point is now not connected to
# the polygon's start point. Therefore, the inner
# side of the hollow cylinder does not get
# generated.
#
E ri = 1.85 # inner radius
E ro = 2.6 # outer radius
E h = 2*ri # cylinder height
H FLAGS 6
P 0 0 0
P 0 0 h
P ri 0 h
P ro 0 h
P ro 0 0
P ri 0 0
V AX # View Animated eXtrusion

В приведенной ниже спецификации при снятии флага isHollow (бит 1) сгенерированный цилиндр больше не является полым.

# file: _cylf4.gcd (model of hollow cylinder; flags=0x4)
#
# There are currently three flags (default value
# for each is true) that control the generation
# of vertices and triangle faces:
# bit 0: isClosed
# bit 1: isHollow
# bit 2: isCentered
#
# This example clears the isClosed flag (bit 0) 
# and the isHollow flag (bit 1), resulting in a
# solid cylinder.
#
E ri = 1.85 # inner radius
E ro = 2.6 # outer radius
E h = 2*ri # cylinder height
H FLAGS 4
P 0 0 0
P 0 0 h
P ri 0 h
P ro 0 h
P ro 0 0
P ri 0 0
V AX # View Animated eXtrusion

В приведенной ниже спецификации при снятии флага isCentered (бит 2) грани треугольника сверху и снизу цилиндра больше не пересекаются на оси вращения.

# file: _cylf0.gcd (model of hollow cylinder; flags=0x0)
#
# There are currently three flags (default value
# for each is true) that control the generation
# of vertices and triangle faces:
# bit 0: isClosed
# bit 1: isHollow
# bit 2: isCentered
#
# This example clears the isClosed flag (bit 0) 
# and the isHollow flag (bit 1) and the isCentered
# flag (bit 2), resulting in a solid cylinder that
# is triangulated to an off-center axis.
#
E ri = 1.85 # inner radius
E ro = 2.6 # outer radius
E h = 2*ri # cylinder height
H FLAGS 0
P 0 0 0
P 0 0 h
P 1 0 0 # these 2 new points define the off-center axis
P 1 0 h # " ...
P ri 0 h
P ro 0 h
P ro 0 0
P ri 0 0
V AX # View Animated eXtrusion

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

# file: _cylf5lax.gcd (cylinder, flags=5, filled with cone)
#
# This example clears the isHollow flag (bit 1), resulting
# in a solid cylinder. However, the rotation axis points
# (i.e., the first two points in the list of six Point
# coordinates) are lowered by 2 mm, resulting in the middle
# of the cylinder being cone shaped.
#
E ri = 1.85 # inner radius
E ro = 2.6 # outer radius
E h = 2*ri # cylinder height
H FLAGS 5
P 0 0 -2
P 0 0 h-2
P ri 0 h
P ro 0 h
P ro 0 0
P ri 0 0
V AX # View Animated eXtrusion

5. Линейная алгебра против геометрической алгебры

Повороты и перемещения являются обычными операциями при создании и просмотре 3D-моделей. Линейная алгебра — более широко известный подход к реализации поворотов и переводов. Однако в геометрической алгебре переводы являютсявращениями, но относительно точки, удаленной на бесконечное расстояние. Это слияние понятий (например, перенос и вращение - одно и то же) вместе с сокращением частных случаев (например, в геометрической алгебре две линии в одной плоскости всегда пересекаются, даже если они параллельны друг другу) делают геометрическую алгебру приложения более лаконичны, чем приложения линейной алгебры.

6. Оптимальный дизайн и эталонный дизайн

(ДЕЛАТЬ)

7. Примеры примитивных форм

(ДЕЛАТЬ)

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

8. Примеры составных фигур

(ДЕЛАТЬ)

9. Заключение

Создание 3D-модели требует множества дизайнерских решений. В этом очерке описаны некоторые из них.

  • Файлы моделей STL и файлы моделей OBJ. Я выбрал файлы моделей OBJ, потому что они более эффективно используют память и мне нравится их простая иерархическая структура.
  • Отображаемые пиксели по сравнению с индексированными объектами. Поскольку это более доступно, чем использование графических интерфейсов для создания 3D-моделей, я выбрал индексированные объекты.
  • Код по сравнению с командными файлами. Учитывая очень ограниченный объем шрифта Брайля, который может отображаться на современных обновляемых устройствах Брайля, командные файлы (например, файлы .gcd) имеют преимущество, поскольку содержат меньше текста.
  • Линейная алгебра по сравнению с геометрической алгеброй. Учитывая, что существует множество вариантов применения довольно интенсивной геометрии в 3D-моделировании (например, нарезка модели, не ограниченная монотонно увеличивающейся высотой плоскостей срезов), и учитывая, что интенсивная геометрия приложения имеют тенденцию быть более краткими, когда они реализованы с помощью геометрической алгебры, я решил использовать геометрическую алгебру.

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

В этом эссе представлен подход к созданию сложных 3D-объектов с использованием кратких командных файлов вместо графического пользовательского интерфейса. Основная цель — сделать создание 3D-моделей более доступным для слепых. Однако тот же подход иногда может быть удобным для зрячих.

Приложение

(ДЕЛАТЬ)

# file: gacad.jl (Geometric Algebra Computer Aided Design)
#
# Editing 3D objects with Julia code instead of
# a CAD application makes it more accessible to
# the blind, as described in the "3D Modeling for
# the Blind" essay at
# https://olarth.medium.com/3d-modeling-for-the-blind-ab487f11725b
#
# To "eat my own dog food" I'm using obj_edit.jl
# to create the models of all the 3D printed
# parts in the Surf! tactile display.
#
# What is Surf!?
# For blind people dissatisfied with their unemployment or
# under-employment due to the high cost and low capabilities
# of current productivity tools (e.g., a single line, 80
# character refreshable braille device costs $15,000), Eduneer
# offers a very low cost tactile display called Surf! (short
# for surface) for transmitting and receiving shapes such as
# human faces or multiple lines of large font braille.
#
include("../PGA/ripga2d3d4d/ripga3d.jl")
using GLMakie, GeometryBasics

mutable struct Obj
 iV::Int64  # vertex offset
 nV::Int64  # vertex count
 iF::Int64  # face offset
 nF::Int64  # face count
 nFPart::Int64 # for animation of mesh creation
end

# obj_xrotate: xrotate = extrude + rotate
# To genrate a 3D component that is symmetric
# about an axis of rotation, rotate a polygon
# about that axis while extruding (i.e., writing
# triangle face information to the 3D object
# file) from the polygon.
#
# arguments:
#  V: vertices starting with 2 points defining rotation axis
#  F: triangle faces "extruded" from rotating polygon
#  O: 3D component
#  PC: polygon coordinates
#  isClosed: set to connect polygon's end and start points
#  isHollow: set if the 3D object empty at axis of rotation
#  isCentered: set if axis of rotation = triangulization axis
#  ntheta: number of rotation angle increments
#  theta0: starting rotation angle
#  theta: full rotation angle
#
function obj_xrotate( # extrude while rotating
 V::Vector{Point{3, Float32}}, # mesh Vertices
 F::Vector{TriangleFace{Int64}}, # mesh Faces
 O::Vector{Obj},   # 3D object components
 PC::Matrix{Float32}, # Polygon Coordinates to rotate
 isClosed::Bool = true, # flag about polygon being closed
 isHollow::Bool = true, # flag about polygon shape
 isCentered::Bool = true,# flag about triangulization
 ntheta::Int64 = 32,  # # angle samples
 theta0::Float64 = 0., # initial angle
 theta::Float64 = 2*pi) # rotation angle
 
 # initialize indices and counts
 iV = length(V)
 nV = 0
 iF = length(F)
 nF = 0
 nFPart = 0
 hdrSize = isCentered ? 2 : 4
 nPR = size(PC,2) - hdrSize # # Polygon points Rotating
 
 # store rotation axis points in V and define pivot line
 push!(V, PC[:,1]); nV += 1; plt = nV
 push!(V, PC[:,2]); nV += 1
 if hdrSize == 4
  push!(V, PC[:,3]); nV += 1; plt = nV
  push!(V, PC[:,4]); nV += 1
 end
 
 # generate rotated vertices
 P = point(PC) # convert to PGA vectors
 L = P[:,1] & P[:,2] # rotation axis
 for itheta = 1:ntheta
  angle = (theta == 2*pi) ?
   theta0 + (itheta-1)/ntheta * theta :
   theta0 + (itheta-1)/(ntheta-1) * theta
  R = rotor(angle, L)
  P2 = R >>> P[:, hdrSize+1:end]
  PC2 = toCoord(P2)
  for iPR = 1:nPR
   push!(V, PC2[:,iPR]); nV += 1
  end
 end
 
 # generate triangle faces from vertices
 for itheta = 1:ntheta-1
  for iPR = 1:nPR-1
   i = nPR*(itheta-1) + iV + hdrSize + iPR
   push!(F, [i i+1 i+nPR]); nF += 1
   push!(F, [i+nPR i+1 i+nPR+1]); nF += 1
  end
  if isHollow
   if isClosed
    i = nPR*(itheta-1) + iV + hdrSize + nPR
    j = nPR*(itheta-1) + iV + hdrSize + 1
    push!(F, [i j i+nPR]); nF += 1
    push!(F, [i+nPR j j+nPR]); nF += 1
   end
  else
   i = nPR*(itheta-1) + iV + hdrSize + 1
   push!(F, [i i+nPR plt+1]); nF += 1
   push!(F, [i+nPR-1 plt i+2*nPR-1]); nF += 1
  end
 end
 if theta == 2*pi
  for iPR = 1:nPR-1
   i = nPR*(ntheta-1) + iV + hdrSize + iPR
   j = iV + hdrSize + iPR
   push!(F, [i i+1 j]); nF += 1
   push!(F, [j i+1 j+1]); nF += 1
  end
  if isHollow
   if isClosed
    i = nPR*(ntheta-1) + iV + hdrSize + 1
    j = iV + hdrSize + 1
    push!(F, [i j i+nPR-1]); nF += 1
    push!(F, [i+nPR-1 j j+nPR-1]); nF += 1
   end
  else
   i = nPR*(ntheta-1) + iV + hdrSize + 1
   j = iV + hdrSize + 1
   push!(F, [i j plt+1]); nF += 1
   push!(F, [j+nPR-1 i+nPR-1 plt]); nF += 1
  end
 end
 
 return Obj(iV,nV,iF,nF,nFPart)
end

macro approw(PC, V) # append row
 PC = esc(PC)
 V = esc(V)
 return quote
  vcat($PC, $V)
 end
end

function gcd_run(fn::String)
 
 # initialize
 PC = Array{Float32}(undef, 0, 3)
 V = Point{3, Float32}[]
 F = NgonFace{3, Int64}[]
 O = Obj[]
 hdrFlags = 0x7
 fig = nothing
 
 # read command file
 open(fn, "r") do io
  while !eof(io)
   strLine = readline(io)
   
   # ignore comments
   m = findfirst('#', strLine)
   if m != nothing
    m == 1 ? continue : 
     strLine = strLine[1:(m-1)]
   end
   
   # check for known command types
   if strLine[1] == 'E'
    eval(Meta.parse(strLine[2:end]))
   elseif strLine[1] == 'H'
    m = findfirst("FLAGS", strLine)
    m != nothing ? hdrFlags = parse(Int, 
     strLine[m[1]+length(m):end]) : nothing
   elseif strLine[1] == 'P'
    R = eval(Meta.parse(
     "[" * strLine[2:end] * "]"))
    PC = @approw PC R
   elseif strLine[1] == 'V'
    push!(O, obj_xrotate(V,F,O,Float32.(PC'),
     hdrFlags & 0x1 > 0,
     hdrFlags & 0x2 > 0,
     hdrFlags & 0x4 > 0))
    m = findfirst("AX", strLine)
    if m != nothing
     m = findlast('.', fn)
     if m != nothing
      fn2 = fn[1:m-1] * ".mp4"
      fig = obj_animate_extrusion(fn2,
       hdrFlags,
       V, F, O,
       Float32.(PC'))
     end
    end
   else
    println(strLine)
   end
  end
 end
 fig != nothing ? fig : nothing
end

function obj_animate_extrusion(fn::String, flags::UInt8,
 V::Vector{Point{3, Float32}}, # mesh Vertices
 F::Vector{TriangleFace{Int64}}, # mesh Faces
 O::Vector{Obj},   # 3D object components
 CP::Matrix{Float32}) # Polygon Coordinates to rotate

 # plot unrotated polygon
 fig = Figure(resolution = (500, 500))
 ax3d = Axis3(fig[1,1],
  elevation = pi/16,
  azimuth = -pi/4,
  limits = (-3.5,3.5, -3.5,3.5, -2,5),
  aspect = (1,1,1),
  xlabel = "x (mm)",
  ylabel = "y (mm)",
  zlabel = "z (mm)")
 n0 = flags & 0x4 > 0 ? 3 : 5
 CPR = copy(CP[:,n0:end])
 CPR_obs = Observable(CPR)
 scatterlines!(ax3d, CPR_obs, markersize=15)
 F2 = NgonFace{3, Int64}[]
 M = GeometryBasics.Mesh(V, F2)
 M_obs = Observable(M)
 wireframe!(ax3d, M_obs, linewidth=0.25)
 fig

 # record video of rotating polygon
 L = point(CP[:,1]) & point(CP[:,2]) # rotation axis
 P = point(CPR) # polygon coordinates as PGA vectors
 T = V[F[1]] # mesh's first triangle face 
 A =[atan(T[1][2],T[1][1]),
  atan(T[2][2],T[2][1]),
  atan(T[3][2],T[3][1])]
 nFaceAngle = maximum(A)
 nFaceMax = O[1].nF
 nFace = 0
 nFrame = 360
 record(fig, fn, 1:nFrame) do iFrame
  angle = 2pi*(iFrame-1)/nFrame
  R = rotor(angle, L)
  P2 = R >>> P
  CPR_obs[] = toCoord(P2)
  
  # if it is time to extrude triangle face(s)
  j = 0
  if angle >= nFaceAngle
   for iFace = nFace+1:nFaceMax
    T = V[F[iFace]] # mesh's next triangle face
    A =[atan(T[1][2],T[1][1]),
     atan(T[2][2],T[2][1]),
     atan(T[3][2],T[3][1])]
    B = A .< 0 # Boolean flags denoting negative angle
    A[B] .+= 2*pi
    nFaceAngle = maximum(A)
    j += 1
    if angle < nFaceAngle
     break
    end
   end # for each remaining face
   nFace += j
   F2 = F[1:nFace]
   M_obs[] = GeometryBasics.Mesh(V, F2)
  end # if it is time to extrude triangle face(s)
 end # recording
end

function obj_save(fn::String,
 V::Vector{Point{3, Float32}},
 F::Vector{NgonFace{3, Int64}})

 # open output file
 open(fn, "w") do io
  # write vertices
  nV = length(V)
  for iV = 1:nV
   str = @sprintf("v %.4f %.4f %.4f",
    V[iV][1], V[iV][2], V[iV][3])
   println(io, str)
  end
  
  # write faces
  nF = length(F)
  for iF = 1:nF
   str = @sprintf("f %d %d %d",
    F[iF][1], F[iF][2], F[iF][3])
   println(io, str)
  end
 end # output file
end

function utest()
 O = Obj[]
 push!(O, Obj(0,1,2,3))
 push!(O, Obj(4,5,6,7))
 display(O)
end

# quick and dirty conversion from .mp4 to animated .gif:
# ffmpeg -i _cyl.mp4 -r 24 -s 480x480 -loop 0 _cyl.gif