Как работают патчи Безье в чайнике Юты?

Я преждевременно опубликовал код игры в гольф, чтобы нарисовать чайник Юты с помощью этот набор данных (просто чайник). (Пересмотренный и опубликованный вызов чайника) Но когда я посмотрел глубже на данные, чтобы Придумав небольшой пример, я понял, что понятия не имею, что происходит с этими данными. Я хорошо разбираюсь в кривых Безье в 2D, реализованных де Кастельжау. Но для 3D работает ли то же самое?

Да! Оно делает!

Данные содержат патчи по 16 вершин в каждом. Есть ли стандартный порядок их расположения? И если они соответствуют 2D-кривым, тогда четыре угловые точки фактически касаются поверхности, а оставшиеся 12 являются элементами управления, верно?

Да!

Мой «первоначальный план» заключался в том, чтобы упростить форму до прямоугольников, спроецировать их на холст и нарисовать заливку серым цветом, вычисленную по величине скалярного произведения пятна, перпендикулярного вектору света. Если я так упросту, он вообще будет похож на чайник? Нужно ли использовать трассировку лучей для получения узнаваемого изображения?

Это субъективно. :-(

Хотя это может выглядеть как несколько вопросов, но все они являются аспектами этого: «Пожалуйста, любезный Гуру, научи меня некоторым нашивкам Безье? Что мне нужно знать, чтобы нарисовать чайник?»


Вот код, который я написал до сих пор. (использует эту матричную библиотеку: mat.ps)

%!
%%BoundingBox: 115 243 493 487
%-115 -243 translate

(mat.ps)run  %include matrix library

/tok{ token pop exch pop }def
/s{(,){search{ tok 3 1 roll }{ tok exit }ifelse }loop }def
/f(teapot)(r)file def
/patch[ f token pop { [ f 100 string readline pop s ] } repeat ]def
/vert[ f token pop { [ f 100 string readline pop s ] } repeat ]def
%vert == patch == %test data input

/I3 3 ident def      % 3D identity matrix
/Cam [ 0 0 10 ] def  % world coords of camera center viewpoint
/Theta [ 0 0 0 ] def % y-rotation x-rotation z-rotation
/Eye [ 0 0 15 ] def  % eye relative to camera vp
/Rot I3 def          % initial rotation seq

/makerot {
    Theta 0 get roty        % pan
    Theta 1 get rotx matmul % tilt
    Theta 2 get rotz matmul % twist
} def

/proj {
    Cam {sub} vop  % translate to camera coords
    Rot matmul  % perform camera rotation
    0 get aload pop Eye aload pop  % extract dot x,y,z and eye xyz
    4 3 roll div exch neg          % perform perspective projection
    4 3 roll add 1 index mul
    4 1 roll 3 1 roll sub mul exch % (ez/dz)(dx-ex) (ez/dz)(dy-ey)
} def


/R 20 def
/H -3 def
/ang 0 def
{
    300 700 translate
    1 70 dup dup scale div setlinewidth

    /Cam [ ang sin R mul  H  ang cos R mul ] def % camera revolves around Y axis at height H, dist R
    /Theta [ ang  H R atan  0 ] def % rotate camera back to origin
    /Rot makerot def % squash rotation sequence into a matrix

    patch {
        % Four corners
        %[ exch dup 0 get exch dup 3 get exch dup 12 get exch 15 get ]

        % Boundary curves
        [ exch
            dup 8 get exch dup 4 get exch dup 0 get exch %curveto4

            dup 14 get exch dup 13 get exch dup 12 get exch %curveto3

            dup 7 get exch dup 11 get exch dup 15 get exch %curveto2

            dup 1 get exch dup 2 get exch dup 3 get exch %curveto1

            dup 0 get exch %moveto
        pop ]

        { 1 sub vert exch get proj } forall

        moveto
            curveto curveto curveto curveto

        stroke
        %flushpage flush (%lineedit)(r)file pop
    } forall
    pstack
    showpage

    %exit
    /ang ang 10 add def
} loop

Вот исходный набор данных Newell Teapot.

И вот мой ужасно плохой образ:
Не знаю, как организованы вершины.

Обновление: исправление. Может они все-таки выложены "нормально". Выбор правильных углов по крайней мере дает симметричную форму:
Углы найдены.

Обновление: граничные кривые выглядят лучше.
пограничные кривые


person luser droog    schedule 28.01.2013    source источник
comment
В принципе, вы правы: у них 4 контрольных точки (4 угла). Те же принципы применимы к 3D-заплатам Безье: если вам нужно, чтобы они плавно соединялись, вы должны выровнять контрольные точки. После того, как вы плавно соедините достаточно большое количество участков, вы можете представить любую поверхность, которую хотите (с разумной точностью). Я давно написал редактор патчей Безье: не стесняйтесь взглянуть на github.com/ldematte/beppe. Из него мы делали канистры из-под кокса, корабли, самолеты; поэтому хорошо окунуться в чайник из Юты не составит труда.   -  person Lorenzo Dematté    schedule 28.01.2013
comment
Это отличная ссылка. Спасибо. Но, ох. Они упоминают Фоули внизу. У меня есть Фоли. Наверное, мне стоило посмотреть на это. Я тупица. :(   -  person luser droog    schedule 28.01.2013
comment
У меня тоже есть Фоли, и IIRC объяснение было слишком коротким, чтобы полностью понять решение проблемы (например, написание рабочего кода). Статья о гамасутре понравилась своей практичностью.   -  person Lorenzo Dematté    schedule 28.01.2013
comment
Спасибо за поддержку. Да, у меня на коленях Intro, и это всего два абзаца. Не упоминает представление данных.   -  person luser droog    schedule 28.01.2013
comment
@ dema80 Я хочу использовать актуальные исходные данные.   -  person luser droog    schedule 28.01.2013
comment
У меня есть пара идей :) что у вас в исходном наборе данных? Треугольники или объемные данные? В прошлом проекте у меня были объемные данные; оттуда я извлекал треугольники с помощью походных кубиков, и выглядело неплохо .. конечно, узнаваем как чайник :)   -  person Lorenzo Dematté    schedule 28.01.2013
comment
@ dema80 Я добавил ссылку на набор данных в вопросе. Вот просто чайник. Насколько я понимаю, это нашивки Безье. 16 очков за патч.   -  person luser droog    schedule 29.01.2013
comment
А, ладно ... исходный набор данных уже определен как набор из 32 патчей, и у вас проблемы с рендерингом ... Сегодня вечером я попытаюсь загрузить тот же набор данных в свой код и посмотреть, как он выглядит.   -  person Lorenzo Dematté    schedule 29.01.2013
comment
Foley Intro довольно тонок, но у Foley & vanDam Fundamentals есть немало, включая псевдокод для генерации меша.   -  person luser droog    schedule 27.03.2013


Ответы (2)


Бикубический участок поверхности Безье представляет собой массив трехмерных точек 4x4. Да, четыре угла касаются поверхности; а строки - это кривые Безье, а столбцы - тоже кривые Безье. Но алгоритм де Кастельжау основан на вычислении медианы между двумя точками и имеет одинаковое значение как в 3D, так и в 2D.

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

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

 /patch[ patch{ [exch { 1 sub vert exch get }forall ] }forall ]def

Затем нам понадобится алгоритм де Кастельжау для разделения кривых Безье. vop поступает из библиотеки матриц и применяет бинарную операцию к соответствующим элементам вектора и в результате создает новый вектор.

/median { % [x0 y0 z0] [x1 y1 z1]
    {add 2 div} vop % [ (x0+x1)/2 (y0+y1)/2 (z0+z1)/2 ]
} def   

/decasteljau { % [P0] P1 P2 P3  .  P0 P1' P2' P3'  P3' P4' P5' P3
    {p3 p2 p1 p0}{exch def}forall
    /p01 p0 p1 median def
    /p12 p1 p2 median def
    /p23 p2 p3 median def
    /p012 p01 p12 median def
    /p123 p12 p23 median def
    /p0123 p012 p123 median def
    p0 p01 p012 p0123  % first half-curve
    p0123 p123 p23 p3  % second half-curve
} def   

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

/splitrows { % [b0 .. b15]  .  [c0 .. c15] [d0 .. d15] 
    aload pop % b0 .. b15
    4 {                          % on each of 4 rows
        16 12 roll decasteljau   % roll the first 4 to the top
        8 4 roll                 % exch left and right halves (probably unnecessary)
        20 4 roll                % roll new curve to below the patch (pushing earlier ones lower)
    } repeat
    16 array astore              % pack the left patch
    17 1 roll 16 array astore    % roll, pack the right patch
} def

Уродливая утилита позволяет нам повторно использовать код строки для столбцов. Комментарии стека были необходимы для написания этой процедуры, поэтому они, вероятно, необходимы для ее чтения. n j roll перекатывает n элементов (влево) j раз; == верхние j элементов над n-м элементом (считая от 1). Таким образом, n постоянно уменьшается, выбирая куда для размещения элемента, а j выбирает, какой элемент поместить туда (перетаскивая за собой все остальное). Если бы было применено bind, эта процедура была бы значительно быстрее, чем процедура на основе словаря.

% [ 0  1  2  3
%   4  5  6  7
%   8  9 10 11
%  12 13 14 15 ]
/xpose {
    aload pop % 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
    15 12 roll % 0 4 5 6 7 8 9 10 11 12 13 14 15 1 2 3
    14 11 roll % 0 4 8 9 10 11 12 13 14 15 1 2 3 5 6 7
    13 10 roll % 0 4 8 12 13 14 15 1 2 3 5 6 7 9 10 11

    12 9 roll % 0 4 8 12 1 2 3 5 6 7 9 10 11 13 14 15
    11 9 roll % 0 4 8 12 1 5 6 7 9 10 11 13 14 15 2 3
    10 8 roll % 0 4 8 12 1 5 9 10 11 13 14 15 2 3 6 7
    9 7 roll % 0 4 8 12 1 5 9 13 14 15 2 3 6 7 10 11

    8 6 roll % 0 4 8 12 1 5 9 13 2 3 6 7 10 11 14 15
    7 6 roll % 0 4 8 12 1 5 9 13 2 6 7 10 11 14 15 3
    6 5 roll % 0 4 8 12 1 5 9 13 2 6 10 11 14 15 3 7
    5 4 roll % 0 4 8 12 1 5 9 13 2 6 10 14 15 3 7 11
    4 3 roll % 0 4 8 12 1 5 9 13 2 6 10 14 3 7 11 15
    16 array astore
} def
% [ 0 4  8 12
%   1 5  9 13
%   2 6 10 14
%   3 7 11 15 ]

/splitcols {
    xpose
    splitrows
    xpose
} def

Затем примените эти функции к данным патча. Опять же, каждый раз заново определяя патч.

/patch[ patch{ splitrows }forall ]def
/patch[ patch{ splitrows }forall ]def
/patch[ patch{ splitcols }forall ]def
/patch[ patch{ splitcols }forall ]def

Это дает возможность работать с более мелкими фрагментами.
каркас по подразделам

Добавьте тест на видимость.

/visible { % patch  .  patch boolean 
    dup % p p
    dup 3 get exch dup 0 get exch 12 get % p p3 p0 p12
    1 index {sub} vop % p p3 p0 v0->12
    3 1 roll {sub} vop % p v0->12 v0->3
    cross /normal exch def
    dup
    [ exch dup 0 get exch dup 3 get exch dup 12 get exch 15 get ]
    { Cam {sub} vop normal dot 0 ge } forall
    %add add add 4 div 0 lt
    or or or
} def

Производство
Применен тест видимости

Обновление: тест был выполнен в обратном направлении.
фиксированный тест видимости

Обновление: Тест бесполезен! На изображении видно, что нижняя часть не ориентирована наружу, и, конечно, отбраковка обратной стороны не препятствует тому, чтобы ручка была видна через горшок. Это требует удаления скрытой поверхности. А поскольку Postscript не поддерживает Z-буфер, я предполагаю, что это должен быть раздел двоичного пространства. Так что это возвращение к книгам для меня.

Обновление: добавьте преобразование Модель-> Мир, чтобы повернуть вещь вертикально.

/Model -90 rotx def % model->world transform

/proj {
    Model matmul 0 get             % perform model->world transform
    Cam {sub} vop                  % translate to camera coords
    ...

Продюсирую это.
вертикально

Полная программа на данный момент. (использует библиотеку матриц: mat.ps.) В ghostscript вы можете просмотреть анимированное вращение, удерживая [enter].

    %!
    %%BoundingBox: 109 246 492 487
    %-109 -246 translate

    (mat.ps)run  %include matrix library
    (det.ps)run  %supplementary determinant function

    /tok{ token pop exch pop }def
    /s{(,){search{ tok 3 1 roll }{ tok exit }ifelse }loop }def
    /f(teapot)(r)file def
    /patch[ f token pop { [ f 100 string readline pop s ] } repeat ]def
    /vert[ f token pop { [ f 100 string readline pop s ] } repeat ]def
    /patch[ patch{ [exch { 1 sub vert exch get }forall ] }forall ]def
    %vert == patch == %test data input flush quit

    /I3 3 ident def      % 3D identity matrix
    /Cam [ 0 0 10 ] def  % world coords of camera center viewpoint
    /Theta [ 0 0 0 ] def % y-rotation x-rotation z-rotation
    /Eye [ 0 0 15 ] def  % eye relative to camera vp
    /Rot I3 def          % initial rotation seq

    /Model -90 rotx def % model->world transform

    /makerot {
            Theta 0 get roty        % pan
            Theta 1 get rotx matmul % tilt
            Theta 2 get rotz matmul % twist
    } def

    /proj {
            Model matmul 0 get             % perform model->world transform
            Cam {sub} vop  % translate to camera coords
            Rot matmul  % perform camera rotation
            0 get aload pop Eye aload pop  % extract dot x,y,z and eye xyz
            4 3 roll div exch neg          % perform perspective projection
            4 3 roll add 1 index mul
            4 1 roll 3 1 roll sub mul exch % (ez/dz)(dx-ex) (ez/dz)(dy-ey)
    } def

    /median { % [x0 y0 z0] [x1 y1 z1]
            {add 2 div} vop % [ (x0+x1)/2 (y0+y1)/2 (z0+z1)/2 ]
    } def

    /decasteljau { % [P0] P1 P2 P3  .  P0 P1' P2' P3'  P3' P4' P5' P3
            {p3 p2 p1 p0}{exch def}forall
            /p01 p0 p1 median def
            /p12 p1 p2 median def
            /p23 p2 p3 median def
            /p012 p01 p12 median def
            /p123 p12 p23 median def
            /p0123 p012 p123 median def
            p0 p01 p012 p0123
            p0123 p123 p23 p3
    } def

    /splitrows { % [b0 .. b15]  .  [c0 .. c15] [d0 .. d15]
            aload pop % b0 .. b15
            4 {
                    16 12 roll decasteljau
                    %8 4 roll
                    20 4 roll
            } repeat
            16 array astore
            17 1 roll 16 array astore
    } def

    /xpose {
            aload pop % 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
            15 12 roll % 0 4 5 6 7 8 9 10 11 12 13 14 15 1 2 3
            14 11 roll % 0 4 8 9 10 11 12 13 14 15 1 2 3 5 6 7
            13 10 roll % 0 4 8 12 13 14 15 1 2 3 5 6 7 9 10 11

            12 9 roll % 0 4 8 12 1 2 3 5 6 7 9 10 11 13 14 15
            11 9 roll % 0 4 8 12 1 5 6 7 9 10 11 13 14 15 2 3
            10 8 roll % 0 4 8 12 1 5 9 10 11 13 14 15 2 3 6 7
            9 7 roll % 0 4 8 12 1 5 9 13 14 15 2 3 6 7 10 11

            8 6 roll % 0 4 8 12 1 5 9 13 2 3 6 7 10 11 14 15
            7 6 roll % 0 4 8 12 1 5 9 13 2 6 7 10 11 14 15 3
            6 5 roll % 0 4 8 12 1 5 9 13 2 6 10 11 14 15 3 7
            5 4 roll % 0 4 8 12 1 5 9 13 2 6 10 14 15 3 7 11
            4 3 roll % 0 4 8 12 1 5 9 13 2 6 10 14 3 7 11 14
            16 array astore
    } def

    /splitcols {
            xpose
            splitrows
            xpose exch xpose
    } def

    /patch[ patch{ splitrows }forall ]def
    /patch[ patch{ splitrows }forall ]def
    /patch[ patch{ splitrows }forall ]def
    /patch[ patch{ splitrows }forall ]def
    /patch[ patch{ splitcols }forall ]def
    /patch[ patch{ splitcols }forall ]def

    /color {normal light dot 1 add 4 div
%1 exch sub
setgray} def

    /visible { % patch  .  patch boolean
            dup % p p
            dup 3 get exch dup 0 get exch 12 get % p p3 p0 p12
            1 index {sub} vop % p p3 p0 v0->12
            3 1 roll {sub} vop % p v0->12 v0->3
            cross /normal exch def
            dup
            [ exch dup 0 get exch dup 3 get exch dup 12 get exch 15 get ]
            { Cam {sub} vop normal dot 0 ge } forall
            %add add add 4 div 0 lt
            or or or
    } def

    /drawpatch {

            % Four corners
            %[ exch dup 0 get exch dup 3 get exch dup 12 get exch 15 get ]

            visible {

                    [ exch
                            % control rows
                            %dup 4 get exch dup 5 get exch dup 6 get exch dup 7 get exch
                            %dup 11 get exch dup 10 get exch dup 9 get exch dup 8 get exch

                            % control columns
                            %dup 1 get exch dup 5 get exch dup 9 get exch dup 13 get exch
                            %dup 14 get exch dup 10 get exch dup 6 get exch dup 2 get exch

                            % Boundary curves
                            dup 8 get exch dup 4 get exch dup 0 get exch %curveto4
                            dup 14 get exch dup 13 get exch dup 12 get exch %curveto3
                            dup 7 get exch dup 11 get exch dup 15 get exch %curveto2
                            dup 1 get exch dup 2 get exch dup 3 get exch %curveto1
                            dup 0 get exch %moveto
                    pop ]

                    { proj } forall

                    moveto curveto curveto curveto curveto
                    %moveto lineto lineto lineto lineto lineto lineto lineto closepath
                    %moveto lineto lineto lineto lineto lineto lineto lineto closepath

                    stroke
                    %flushpage flush (%lineedit)(r)file pop

            }{
                    pop
            }ifelse

    } def

    /R 20 def
    /H -3 def
    /ang 10 def
    {
            300 700 translate
            1 70 dup dup scale div setlinewidth

            % camera revolves around Y axis at height H, dist R
            /Cam [ ang sin R mul  H  ang cos R mul ] def
            /Theta [ ang  H R atan  0 ] def   % rotate camera back to origin
            /Rot makerot def        % squash rotation sequence into a matrix

            patch {
                    drawpatch
            } forall
            pstack
            showpage

            %exit
            /ang ang 10 add def
    } loop
person luser droog    schedule 30.01.2013
comment
Обратился за помощью по созданию BSP на math.SE. - person luser droog; 08.02.2013
comment
Примечание: второй xpose в /splitcols должен быть xpose exch xpose, чтобы применить транспонирование к обоим из двух новых патчей (и другому exch, если сохранение порядка важно). - person luser droog; 27.11.2013
comment
Вышеупомянутая ошибка, вероятно, является причиной эффекта проволочной сетки на последнем изображении. И за счет использования скошенных соединений вместо скосов. - person luser droog; 19.04.2014

Основываясь на помощи по math.StackExchange, я пришел к промежуточной цели дополнения библиотеки матриц с помощью функция для вычисления определителей.

Итак, этот код проходит несколько неуклюжих начальных тестов, но я должен признать, что он чертовски уродлив:

GS>[[1 0][0 1]] det
GS<1>=
1
GS>[[0 1][1 0]] det =
-1
GS>(mat.ps) run
GS>3 ident
GS<1>det =
1
GS>[[1 2 3][4 5 6][7 8 9]] det =
0
GS>

Обновлять. Немного читабельнее.

Обновлять. Намного более читабельно с использованием точки и крестика. Еще раз спасибо, MvG.

(mat.ps) run % use dot and cross from matrix library

/elem { % M i j
    3 1 roll get exch get % M_i_j
} def

/det {
    dup length 1 index 0 get length ne { /det cvx /typecheck signalerror } if
    1 dict begin /M exch def
        M length 2 eq {
            M 0 0 elem
            M 1 1 elem mul
            M 0 1 elem
            M 1 0 elem mul sub
        }{
            M length 3 eq {
                M aload pop cross dot
            }{ /det cvx /rangecheck signalerror } ifelse
        } ifelse
    end
} def
person luser droog    schedule 09.02.2013
comment
Как насчет того, чтобы сохранить матрицу под каким-либо именем на время этой функции и иметь функцию для извлечения члена матрицы? Тогда вы можете написать 0 0 memb вместо dup 0 get 0 get. Затем вы можете записывать вещи более систематично, например жестко запрограммируйте правило Сарруса. Или вы можете записать det (A, B, C) как (A × B) ∙ C, то есть перекрестное произведение, за которым следует скалярное произведение. Поскольку det является симметричным, вы можете рассматривать свои столбцы или строки как векторы A, B, C. - person MvG; 09.02.2013