Текст
                    ПРИНЦИПЫ ПРОГРАММИРОВАНИЯ
В МАШИННОЙ ГРАФИКЕ


Programming Principles in Computer Graphics L Ammeraal Hogeschool Utrecht The Netherlands JOHN WILEY & SONS Chichester • New York • Brisbane • Toronto • Singapore
Серия МАШИННАЯ ГРАФИКА НА ЯЗЫКЕ СИ Л. Аммерал ПРИНЦИПЫ ПРОГРАММИРОВАНИЯ В МАШИННОЙ ГРАФИКЕ Перевод с английского В.А. Львова Москва "Сол Систем" 1992
ББК 32.97 А 62 УДК 681.3 Л. Аммерал \ 62 Принципы программирования в машинной графике. Пер. с англ. —М.: "Сол Систем", 1992.— 224 с: ил. ISBN 5-85316-001-Х (рус.) Практическое введение в машинную графику. Рассматриваются вопросы аналитической и проективной геометрии и программирования в машинной графике, задачи формирования В-сплайна, перспективных проекций и удаления невидимых линий. Алгоритмы доведены до **гото- вых к работе" графических программ на языке Си. В приложении дает- ся краткий обзор элементов языка Си. Для широкого круга читателей, применяющих персональные ком- пьютеры IBM PC или совместимые с ними для работы с графической информацией. А240409(№№-00$ (безобьявл) А И68(03)—92 «*зо0ъявл.) Издание подготовлено при участии МП "БИНОМ" © 1986. John Wiley & Sons © 1992, перевод на русский язык, оформление Sol System Ltd. © 1992, переработка программ Sol System Ltd. Подписано в печать 11.08.92. Формат 84Х 108/32. Тираж 30 000 экз. Зак. 271. «Сол Систем», Москва, 103104, ул. Остужева, д. 12/2, кв. 6. Отпечатано с диапозитивов на Книжной фабрике № 1 Министерства печати и информации России. 144003, г. Электросталь Московской области, ул. Тевосяна, 25. ISBN 5-85316-001-Х (рус.) ISBN 0-471-90989-0 (англ.)
ПРЕДИСЛОВИЕ В этой книге рассматриваются наиболее существенные эле- менты машинной графики, а именно — вопросы аналитической геометрии и программирования. Здесь объясняется, как програм- мисты могут использовать плоттеры и другие графические уст- ройства, без анализа подробностей их работы и без учета конк- ретных особенностей доступных устройств. Надеюсь, что чита- тель одобрит подобный приборно-независимый подход. В любом случае так можно избежать недоразумений, которые могут поя- виться при изучении конкретных программ и аппаратных реали- заций. Много внимания уделяется графическому представлению трехмерных объектов. В первых трех главах описываются нес- колько интересных программ, в том числе одна для определения кривой типа В-сплайна. Эти главы являются подготовительными для остальных частей книги. В главе 4 определяются традицион- ные преобразования для проволочных моделей. В противовес этой известной теме в главе 5 предлагается эффективный способ для удаления невидимых линий, являющийся, по моему мне- нию, совершенно новым. Подобно другим алгоритмам, описыва- емым в этой книге, способ удаления невидимых линий реализо- ван в работающей программе с именем HIDLINPX. Ряд приме- ров применения этой программы приводится в главе 6. Поскольку некоторые из этих программ могут быть полезны на практике, то фактически эта книга дает больше, чем это следует из заголовка книги, предполагающего рассмотрение "принци- пов". С другой стороны, целый ряд программ служит только ме- тодическим целям, не имеющим практического воплощения са- ми по себе. Но в этих примерах читатель может найти описание реализаций принципов программирования, которые могут ока- заться полезными при решении реальных задач.
6 ПРЕДИСЛОВИЕ Все программы в этой книге написаны на языке Си. Это мо- жет показаться странным, поскольку многие пользователи счи- тают, что для этих целей язык Паскаль может быть более эффек- тивным. Имея практический опыт программирования на многих языках в течение четверти века, я считаю Паскаль очень хоро- шим языком программирования, но язык Си просто прекрасен (поскольку я написал известные учебники на голландском языке по этим языкам программирования, то желаю долгих лет жизни обоим этим языкам!). Очень трудно доказать, что фигурные скобки {} также хорошо читаемы, как ключевые слова begin и end, но разница по длине очевидна. Я упоминаю об этом только потому, что привести полностью текст некоторых программ в этой книге оказалось возможным лишь вследствие компактности языка Си. До сих пор я оттягивал ответ на очень трудный вопрос: на ко- го рассчитана эта книга. Я не знаю достаточно хорошо учебных программ университетов и других учебных заведений в разных странах, чтобы рекомендовать эту книгу в качестве учебника для какого-либо семестра определенного года обучения. Вполне оче- видно, что, по крайней мере, отдельные части-этой книги будут полезны всем, кто преподает машинную графику. Например, простые непрозрачные объекты могут быть изображены в перс- пективе, путем подготовки файла входных данных для програм- мы HIDLINPX вручную, так что пользу от этой книги могут получить даже те читатели, которые не умеют программировать. В конце каждой главы приведены упражнения, но я глубоко убежден, что каждый преподаватель, работающий в этой обла- сти, легко может добавить другие задачи по своему вкусу. В профессиональной деятельности многие разрабатывают программные пакеты для автоматизированного проектирования. Я надеюсь, что эта книга будет полезной и в этой области и ока- жет влияние на развитие хороших программных продуктов. Л. Аммерал
Глава 1 ВВЕДЕНИЕ 1.1. МОТИВАЦИЯ НЕОБХОДИМОСТИ ГРАФИЧЕСКОГО ПРОГРАММИРОВАНИЯ Студенты иногда сомневаются в необходимости изучения не- которых предметов и в связи с этим задают соответствующие вопросы своим преподавателям. Если у преподавателя нет вре- мени на подробный ответ, то общий — и часто неубедительный — ответ просто содержит ссылку на экзаменационные требова- ния к этим знаниям. К счастью, такой вопрос очень редко возни- кает, когда дело касается машинной графики. По сравнению с обычными распечатками на печатающем устройстве графичес- кий вывод информации очень привлекателен, и даже те, кто не согласен с этим, убеждены, что применение машинной графики представляется очень полезным. Особый интерес к машинной графике проявляется тогда, ког- да графическое изображение получается в результате собствен- ной деятельности. Принцип "сделай сам" применим ко всем ви- дам искусства, но в машинной графике мы имеем уникальный случай иметь в собственном распоряжении очень точного и ис- полнительного помощника. Этот помощник может выполнить любые рисованные картинки, если только мы можем проинст- руктировать компьютер, как их делать. К сожалению, последнее утверждение является не исключением, а правилом. Большинст- во компьютерных пользователей не могут заставить программу вычертить ту картинку, которую они хотят иметь, даже если за- тратили массу денег на приобретение сложного программного обеспечения. Нам придется примириться с такой неблагоприят- ной ситуацией, так как средний пользователь не может сам на- писать все программное обеспечение, в котором он нуждается. Мы должны покупать программы, написанные другими. Но это
8 Глава 1. ВВЕДЕНИЕ совсем не означает, что нерационально учить студентов про- граммированию. Следует помнить, что для того, чтобы купить программу, она должна быть написана. Совершенно нереально предполагать, что в скором времени будет доступно программное обеспечение для любой конкретной цели, так что и в будущем необходимость программирования сохранится. Это особенно ха- рактерно для графического обеспечения, поскольку многие при- кладные программы, выдающие результат своей работы в виде таблиц, будут заменены программами, представляющими вы- ходные данные в графической форме. Даже те пользователи, которые сами не программируют, а покупают все необходимое программное обеспечение, получат определенную пользу от знания правил программирования. Сту- денты должны быть знакомы с концепциями, которые не подвер- гаются постоянным изменениям и остаются существенными в те- чение длительного времени. Такой фундаментальной концеп- цией, например, является алгоритм, позволяющий определить, закрывает ли некоторая поверхность в пространстве видимость отрезка прямой линии, тогда как технические детали коммерче- ских программных пакетов могут со временем изменяться. Ко- нечно, рано или поздно пользователи таких пакетов должны де- тально изучить их технические возможности, но перед этим пользователи должны иметь некоторое представление об алго- ритмах машинной графики. Такое представление бывает значи- тельно глубже, если алгоритмы изучаются не только теоретиче- ски, но и практически. Поэтому настоятельно рекомендуется на- учиться программировать, даже если в будущем работа по программированию не планируется. 1.2. ПРОГРАММИРОВАНИЕ ГРАФИКИ НА ЯЗЫКЕ СИ Естественные языки, хотя и интересны во многих аспектах, совершенно не подходят для описания алгоритмов. Поэтому не удивительно, что алгоритмы значительно лучше описываются на алгоритмическом языке. Алгоритмический язык, "понятный" компьютеру, называет- ся языком программирования высокого уровня, или просто язы- ком программирования. В дальнейшем будем иметь в виду имен- но такое применение, но несколько расширим понятие и будем считать язык программирования действительно "языком высо-
1.2. ПРОГРАММИРОВАНИЕ ГРАФИКИ НА ЯЗЫКЕ СИ 9 кого уровня", если не только компьютер, но так же и человек мо- жет понимать его по возможности эффективно и просто. Мы не будем требовать, чтобы язык был легко читаемым для всех. Ма- тематические символы непонятны большинству восьмилетних детей, но без них не могут обойтись математики и инженеры. Та- ким же образом и языки программирования совсем не обязатель- но должны быть понятными для всех, но они совершенно необхо- димы профессиональным программистам. Здесь мы будем при- менять язык Си не только потому, что он достаточно распространен, но главным образом из-за его высокого качества. Для программирования на языке Си нужно быть очень аккурат- ным, на языке Си логические ошибки приводят к синтаксиче- ским ошибкам гораздо реже, чем, например, на языке Паскаль. Поэтому такие логические ошибки в программе на языке Си мо- гут привести к неправильным результатам или к выводу сообще- ний о технических ошибках, которые могут оказаться совершен- но непонятными. Другими словами, ошибки могут привести к непредсказуемым результатам. Это постоянно следует иметь в виду при программировании на языке Си. При написании про- грамм всегда желательно, чтобы они были легко читаемыми, но это совершенно другой аспект. Люди, которые сами не использу- ют этот язык, полагают, что программы на языке Си восприни- маются с трудом. Но с проблемой читаемости всегда очень слож- но. Если одна строка программы на языке Си соответствует деся- ти строкам программы на языке Бейсик, то несерьезно утверждать, что одну строку языка Си труднее прочитать, чем десять строк языка Бейсик. Если попытаться разобраться в ко- роткой программе на языке Си, заранее предполагая, что она простая, поскольку короткая, то можно очень быстро разочаро- ваться. Об этом нельзя забывать при чтении примеров в этой книге: очень короткие программы на языке Си могут быть совсем не тривиальными и даже интересными! Для серьезного изучения семантики языка Си в конце книги упомянуто несколько книг о данном языке. Но для пояснения применения языка Си в этой книге ограничимся только такими конструкциями, которые являются специфичными для этого языка. Некоторые сведения о языке Си приведены в Приложе- нии. Рассмотрим здесь самую первую графическую Си-програм- му:
10 Глава 1. ВВЕДЕНИЕ /^SQUARES: Эта программа вычерчивает 50 квадратов, */ /* каждый следующий внутри предыдущего */ /* This program draws 50 squares inside each other */ main() { float xA, yA, xB, yB. xC, yC, xD, yD, xxA, yyA, xxB, yyB, xxC, yyC, xxD. yyD, p, q; int I; p-0.95; q-1.0-p; xA-2.0; xB-8.0; xC-8.0; xD-2.0; yA-0.5; yB-0.5; yC-6.5; yD-6.5; initgr(); for (i-0; K50; i-ьн) { move(xA, yA); draw(xB, yB); draw(xC, yC); draw(xD, yD); draw(xA, yA); xxA-p*xA+q*xB; yyA-p*yA+q*yB; xxB-p*xB+q*xC; yyB-p*yB+q*yC; xxC-p*xC+q*xD; yyC-p*yC+q*yD; xxD-p*xD+q*xA; yyD-p*yD+q*yA; xA-xxA; xB-xxB; xC-xxC; xD-xxD; yA-yyA; уВ-ууВ; yC-yyC; yD-yyD; } endgr<); Результат работы этой программы показан на рис. 1.1. Рис. 1.1. Результат работы программы SQUARES
L2. ПРОГРАММИРОВАНИЕ ГРАФИКИ НА ЯЗЫКЕ СИ 11 Программа содержит обращения к четырем графическим под- программам: initgr() инициирует графический вывод; moveix, у) перемещает перо (реальное или фиктивное) в точку с координатами ху у; drawix, у) вычерчивает отрезок прямой линии из текущей позиции пера в точку с координатами дс, у; endgri) выполняет заключительные операции по выводу оставшейся информации из выходного буфера. В языке Си для обозначения технических терминов "проце- дура" или "подпрограмма" применяется термин "функция". Об- ращение к функции записывается обязательно с парой круглых скобок, даже если аргументы отсутствуют. Перед обращениями к функциям move и draw обязательно должно стоять обращение к функции initgri). Аналогично вслед за последним обращением к функции move или draw должно стоять обращение к функции endgr. Обе функции, moveix, у) и draw(x, у), обеспечивают пере- мещение реального или фиктивного пера в точку с координатами х, у, причем при работе функции moveix, у) перо перемещается в поднятом положении, а при работе функции draw(x> у) — в опу- щенном. Все упомянутые четыре функции не относятся к языку Си. Это внешние функции, после компиляции любой программы они должны быть добавлены к ней с помощью редактора связей. Если эти программы недоступны непосредственно, то они могут быть выражены через обращения к другим доступным графичес- ким подпрограммам. .. В этой книге не будут рассматриваться вопросы об используе- мых технических средствах. Вместо обсуждения особенностей различных плоттеров и других графических устройств здесь все внимание будет сфокусировано на общих, приборно-независи- мых принципах программирования. При технических средствах, использовавшихся автором, подпрограмма initgr просит пользо- вателя выбрать один из возможных режимов работы — "немед- ленный" или "задержанный". Выбор режима "немедленный" приводит к тому, что сформированный графический вывод сразу же посылается на экран терминала, что обеспечивает возмож- ность взаимодействия. Режим "задержанный" приводит к запи- си информации о чертеже в файл на диске, который затем может
12 Глава 1. ВВЕДЕНИЕ быть преобразован для пересылки на различные устройства, та- кие как перьевой плоттер, матричное печатающее устройство, или на упомянутый ранее графический терминал. Все эти уст- ройства будут хорошо работать, если диапазоны изменения координат х и у будут удовлетворять условиям 0 < х < 9 и О < у < 7. То есть точка с координатами х, у располагается на расстоянии х дюймов справа от оси у и на расстоянии у дюймов над осью х. Иначе говоря, начало О системы экранных координат располагается в нижнем левом углу экрана. Те читатели, кото- рым это почему-либо не нравится, могут внести любые измене- ния в систему координат, особенно после изучения данной кни- ги! Кроме того, в главе 2 будет рассмотрена возможность задания границ рисунка более элегантным способом. Графическое изображение на рис. 1.1 состоит из 50 квадра- тов. Сначала вычерчивается квадрат ABCD, затем на стороне АВ выбирается точка А'такая, что АА'= 0.05 * АВ. Аналогичным образом вычисляется положение точек В', С', D' н а сторонах квадрата ВС, CD, DA, соответственно. После этого точкам А', В', С, D' присваиваются имена А, В, С, D и процедура повторяется. УПРАЖНЕНИЯ 1.1. Напишите программу для вычерчивания произвольного числа треугольников, располагаемых внутри друг друга, аналогично тому, как вычерчивались квадраты в программе SQUARES. 1.2. Напишите программу для вычерчивания следующей после- довательности отрезков прямых линий: из точки (1.0, 6.0) в точку (1.0, 1.0); из точки (1.0, 5.8) в точку (1.2, 1.0); из точки (1.0, 5.6) в точку (1.4, 1.0); из точки (1.0, 1.0) в точку (6.0, 1.0).
Глава 2 ДВУХМЕРНЫЕ АЛГОРИТМЫ 2.1. ПРЕОБРАЗОВАНИЕ И НОВЫЕ КООРДИНАТЫ Рассмотрим следующую систему уравнений: f х' = х + а \У-У Эти уравнения можно интерпретировать двояким образом: 1. Все точки на плоскости ху перемещаются вправо на расстоя- ние а — см. рис. 2.1 (а); 2. Координатные оси х и у перемещаются влево на расстояние а —см. рис. 2.1(6). (а) (б) Рис. 2.1. (а) — перенос; (б) — изменение координат Этот простой пример иллюстрирует принцип, применимый и в более сложных ситуациях. Мы и далее будем рассматривать си- стемы уравнений, обычно записываемых в виде произведений матриц, интерпретируя их как преобразования всех точек в фик- сированной системе координат. Однако та же самая система уравнений может интерпретироваться и как изменение системы координат. Пусть необходимо повернуть точку РОс, у) вокруг начала ко- ординат О на угол ср. Изображение новой точки на рис. 2.2 обоз- начим через Р'(*',/). Существуют четыре числа а, Ъ, с, d, такие,
14 Глава 2. ДВУХМЕРНЫЕ АЛГОРИТМЫ Р(х'.у') Р(х. у) О х Рис. 2.2. Поворот вокруг точки О на угол <р что новые координаты х' и /могут быть вычислены по значени- ям старых координат х и у из следующих уравнений: Г х' - ах + by \ у' = cx + dy Для получения значений а, Ь, с, J рассмотрим вначале точку Ос, у) ж (1>0). Полагая дсяв1иу = 0вуравнении (2.1), получим (2.1) Но в этом простом случае, как это видно из рис. 2.3(a), значения х' и у' равны соответственно cos <p и sin <p. Тогда будем иметь a = cos^> с = sin <p Аналогичным образом из рис. 2.3(6) следует Ъ = -sin (p d= cosp Тогда вместо системы уравнений (2.1) можем записать х -xcos<p-ysm<p [ у' = х sin <p + у cos f (2.2) (-sin^>, cos^ (cos^sin^) (0,1) (1,0) x (a) (6) Рис. 2.3. (a) — отображение точки (1,0); (б) — отображение точки (0,1)
2.1. ПРЕОБРАЗОВАНИЕ И НОВЫЕ КООРДИНАТЫ 15 В приведенной ниже программе изображение стрелки вычер- чивается после предварительного поворота вокруг точки О на 6°. /* QUADRANT: */ /* Эта программа чертит 14 стрелок, летящих в направлении */ /* против часовой стрелки относительно точки О первого */ /* квадранта координатной системы */ /* This program draws 14 arrows, flying counter-clockwise */ /* about О In the first quadrant of the coordinate system */ ♦include "math.h" float x[4H 6.0, 6.0, 5.9, 6.1}, /* See Figure below in this program */ y[4H"-0\25, 0.25, 0.0. 0.0}; /* См. рисунок ниже в данной программе */ main() { Intl. J; float pi, phi, cos_phi, sin_phi, xx, yy; pi-4.0*atan(1.0); phl-6*pi/180; cos__phl-cos(phi); slnphi-sin(phi); InltgrQ; for(l-1;i<-14;i++) { /* Rotate the arrow: */ /* Поворот стрелки */ for (|-0; j<-3; j++) { xx-x[j]; y\ry[j]; x[j^xx*cos_phi-yy*sin_phi; y[j^xx*sin_phi+yy*cos_phi; } /* Draw the rotated arrow: */ /* Рисование повернутой стрелки */ move(x[0]. у[0]); forQ-1; J<-3; J-м-) draw(x[J]. уЩ); draw(x[1].y[1]): } endgrQ; } В геометрии точки объекта обычно обозначаются прописны- ми буквами А, В.... Здесь будем обозначать их цифрами 0, 1.... В начальной позиции стрелка указывает вверх, ее центр распо- ложен в точке (6,0). В комментариях в правой части программы сделана попытка (правда, не очень успешная) показать стрелку в исходной позиции. Значения координат х и у для вершин лома- ной линии, изображающей стрелку, записаны в элементах мас- сивов х [i ] и у [i ] (i = 0, 1, 2, 3). Эти массивы — внешние: они оп- ределены вне функции main. Внешние массивы обладают очень полезным свойством — значения элементов массива можно Initial position * of the arrow: * Начальная позиция: * (6,0.25) * 1 * л. • / I \ * 2 / \ 3 * (5.9,0) I (6.1.0) * I * 0 * (6. -0.25) * • /*••*•***••****••**••**•**•
16 Глава 2. ДВУХМЕРНЫЕ АЛГОРИТМЫ инициализировать. Нумерация индексов элементов массива на- чинается с 0, в описании массива необходимо задать количество элементов массива. Таким образом, описание массива (которое также называется "декларацией" или "объявлением") //ш>х[4] = {6.0,6.0,5.9,6.1},... вводит четыре элемента массива с начальными значениями х[0] = 6.0 х[1] = 6.0 х[2] = 5.9 х[3] = 6.1 То обстоятельство, что начальное значение у [О ] задано отри- цательным, а все координаты для вычерчивания должны быть положительными, не вызывает сложностей, поскольку перед вы- черчиванием стрелка подвергается повороту, который переносит ее в область над осью х. На рис. 2.4 показан результат работы этой программы. ч ч ч ч \ \ Рис. 2.4. Результат работы программы QUADRANT 2.2. ПОВОРОТ Система уравнений (2.2) описывает поворот вокруг точки О — начала системы координат. Но часто это не то, что нам нужно. Если требуется выполнить поворот относительно заданной точки (дс0, у0), то в этих уравнениях можно заменить: х на х - *0, у — на У ~Уо>х' — нах'-*0и/ — нау'-у0: f х' - xQ = (х - х0) cos <р - (у-у0) sin <р 1 У -Уо= (х~~хо> sin^> + (y-yQ) cosp | х' = *0 + (х-х0) cos<р- (у-у0) sin<р (2 3
2.2. ПОВОРОТ 17 /* ARROWS30 */ /* Эта программа чертит 30 стрелок на окружности с центром в (хО, уО) */ /* This program draws 30 arrows, flying counter-clockwise about (x0, yO ) v */ #include "math.h" float x[4]-{ 0.0, 0.0, -0.08, 0.08}, /* See Figure below in this program */ y[4]-{-0.25, 0.25, 0.0, 0.0 }; /* См. рисунок ниже в программе */ main() { Intl. J; float pi, phi, cosphi, slnphi, dx, dy, xO-4.5. yO-3.5, r-3.0; pi-4.0*atan(1.0); phi-12*pi/180; cos_phi-cos(phi); sinphi-sin(phi); initgr(); /* Перенос в начальную позицию (xf>r, y0) :*/ /* Move to start position (xOfr, yO): for (|-0; j<4; J++) { xD}b-xf>r; y[j}b-y0; } for (Ю; К30; i-н-) { /* Rotate the arrow: */ t /* Поворот стрелки: */ j for (j-0; j<4; j-и-) t { dx-x[jbx0; dy-y[j}-y0; xfj^-x(Kdx*cos_phi-dy*sin_phi; > y[j^yf>»-dx*sin phi+dy*cos phi; i } /* Draw the rotated arrow: */ и /* Рисование повернутой стрелки */ move(x[0], y[0]); for 0-1; j<-3; j++) draw(x[J], yUD; draw(x[1], y[1]); } endgr(); f-k f* f-k /•k f-k f-k f-* f-k f-k f-k f-k f* f-k f-k V Initial position */ Начальная позиция */ */ (0,0.25) */ 1 ^ */ л * / / l\ */ 2/ \3 */ (-0.08, 0) I (0.08. 0) */ I */ I V 0 */ (0, -0.25) */ /••••*••••••*•••••**•*••***/ Результат работы программы показан на рис. 2.5. / Ч Ч Ч ч У Рис. 2.5. Результат работы программы ARROWS30
18 x Глава 2. ДВУХМЕРНЫЕ АЛГОРИТМЫ [*'/]=[*я rcosf s[n,p] ta [;■] - [■ (2.4) (2.5) 2.3. МАТРИЧНАЯ ЗАПИСЬ Система уравнений (2.2) может быть записана в виде одного матричного уравнения sin^ cos< или с использованием вектора-столбца [cosy? -^т< sin <р cos < В книгах по машинной графике запись с вектором-строкой (2.4) встречается чаще, чем с вектором-столбцом (2.5). Здесь также будет применяться запись типа (2.4). В такой записи /-я строка квадратной матрицы всегда является отображением /-го единичного вектора (здесь /=1,2). Вполне возможно записать в матричной форме систему уравнений (2.3) Однако первая часть этого уравнения не является чисто матрич- ным произведением. В более сложных ситуациях, когда поворот совмещается с другими преобразованиями, было бы более удобно иметь единое матричное произведение для каждого элементар- ного преобразования. На первый взгляд ето кажется невозмож- ным, если преобразование включает операцию переноса. Но, как мы увидим ниже, с помощью матрицы преобразования размером 3x3 это вполне реально. Начнем с простого переноса. Пусть точ- ка РОс, у) переносится в точку Р' Ос', у'), где х -х + а /=? + * Эти уравнения можно переписать в виде (2.7) [*' Л-[* У 1] Но с учетом будущих потребностей это уравнение лучше пере- писать в следующей форме Г1 0 01 [*' у' 1]= [х у 1] 0 1 0 (2.8) [а Ъ \\ Легко проверить, что уравнения (2.7) и (2J&) эквивалентны.
2 J. МАТРИЧНАЯ ЗАПИСЬ 19 Такую запись принято называть записью в системе "однородных координат". Однородные координаты более подробно обсужда- ются в параграфе 3.6. Запись каждого преобразования в форме произведения матриц позволяет совмещать несколько преобра- зований в одном. Чтобы показать такое совмещение преобразо- ваний, объединим поворот с двумя переносами. Поворот на угол <р вокруг начала координат О был описан уравнением (2.4). Заменим это уравнение следующим: Г coscp sin<p 0] [jc' у1 1]= [х у 1] -sinp cosp 0 <2.9) L 0 0 lj Теперь выведем новую версию уравнений (2.6) для описания поворота на угол <р вокруг точки (jc0, у0); это уравнение может быть выражено формулой [*' У 1]-[jc у 1]Д (2.10) где через R обозначена матрица размером 3 * 3. Для нахождения этой матрицы R будем считать, что преобразование состоит из трех шагов с промежуточными точками (ир v{) и (w2, v2). 1. Преобразование для переноса точки 6с0, у0) в начало коор- динат О [«! vx 11-U У ИГ где 2. 1 tf 0" 7"-| 0 1 01 Поворот на угол р относительно точки начала координат О [и2 v2 11 — [«! у{ П^о где 3. cos^> sin(p 0] | -sin <p cos <p 01 0 0 lm Перенос из начала координат в точку (jc0, y0) *о = (2.11) где [*' У 1]= [iu v, 1]Г 1 0 0' 0 1 0 1хоУо 1.
20 Глава 2. ДВУХМЕРНЫЕ АЛГОРИТМЫ Возможность комбинации этих шагов основана на свойстве ассоциативности матричного умножения, то есть (АВ)С = А(ВС) для любых трех матриц А, В и С, имеющих размерности, допу- скающие такое умножение. Для любой части этого уравнения мы можем просто записать ABC. Теперь найдем U' У l]-[i£2 v2 1]Т -{[!*! Vl l]R0}T -К Vj 11Л0Г = (И ПГ}л0г -[х у 1]ГЛ0Г = [х у 1 ]Л где R = Г^Г Это и будет искомая матрица, которая после выполнения двух матричных умножений дает cos^> sin<p 01 R = \-sin<p cos<p 0 где введены обозначения С\ С2 У Cj = х0 - x0cos <р + 3>0sin Ф c2 = y0-x0sm(p-yQcos<p 2.4. ОКНА И ОБЛАСТИ ВЫВОДА Часто встречаются ситуации, когда требуется вычертить объ- екты, размеры которых заданы в единицах совершенно несовме- стимых с экранной системой координат. Например, размеры зда- ния могут быть в сотни раз больше размеров желаемого изобра- жения. С другой стороны, молекула в реальности значительно меньше ее изображения на картинке. Наконец, имеются такие приложения, в которых объект является не какой-либо физиче- ской реальностью, а лишь графическим представлением соотно- шений между некоторыми значениями, например, на рис. 2.6 по- казана динамика доходов некоей фирмы в начале двадцатого ве- ка. Проблемно-ориентированные размеры выражаются в так на- зываемых мировых координатах. На рис. 2.6 числа 1901, 1902,
2.4. ОКНА И ОБЛАСТИ ВЫВОДА 21 Window Окно Profit <$> 250 000 200 000 150 000 . ЮО 000 50 000 • 0 Доход r—i __ - __. 1901 1902 1903 1904 Год Year Рис. 2.6. Столбчатая диаграмма в окне 1903,1904 и 50 000,100 000,150 000,200 000,250 000 выражают значения в мировых координатах. Введем теперь концепцию ок- на. Окно — это прямоугольник, в пределах которого вычерчива- ется объект (или его^шсть), как показано на рис. 2.6. Стороны прямоугольника параллельны координатным осям. Во избежа- ние затруднений в понимании происходящего очень важно отме- тить, что окно относится к объекту,.но не к изображению, кото- рое будет сформировано. Если, как обычно, введем горизонталь- ную ось х и вертикальную ось у, то окно на рис. 2.6 полностью определится значениями: *та*=1908 v ,=-150 000 •'ПИП у = 325 000 Очевидно, что размеры и положение окна определяются в си- стеме мировых координат. Эти значения могут показаться нео- жиданными для читателя, поскольку окно вводится для опреде- ления желаемого изображения на картинке и, на первый взгляд, более приемлемым было бы задать эти значения в дюймах, чем указать какое-то фиктивное значение дохода, равное -$150 000, в качестве минимального значения ymin по оси у. Однако задание окна в системе мировых координат является обычным и очень удобным на практике. Необходимо также задать прямоугольную область на экране, которая определит размеры желаемой картинки. Эта область на- зывается областью вывода. Она задается аналогично окну, то есть указываются минимальные и максимальные значения по
22 Глава 2. ДВУХМЕРНЫЕ АЛГОРИТМЫ координатным осям X и Y в единицах измерения на экране. Эти значения будут обозначаться прописными буквами X и Y. Ти- пичным примером задания области вывода могут быть значения *тах-7-5 У . «1.0 mm У «6.0 max Теперь окно нужно отобразить на область вывода. Например, заданное значение в мировых координатах х « 1898 должно быть преобразовано в экранную координату Xs" 1.5. Вначале вычисля- ются коэффициенты масштабирования по осям: /, max min X у — у max min У - Y . max min У V -V • •'max -'min /v- B нашем случае найдем, что /х = 0.6 и / = 0.0000105. Затем расстояние X - Хт^п точки изображения от левого края области вывода вычисляется умножением коэффициента / на соответ- ствующее расстояние х - хт[п от исходной точки до левого края окна. Расстояние У- Ут1п находится аналогично. Следовательно, координаты точки изображения будут определены из соотноше- ний Х " *min+4'(дс" хт\х) (2.12) min Jy v/ •'min' Закончим этот параграф тремя замечаниями: 1. Окно совсем не обязательно должно охватывать весь объект целиком. Если оно не охватывает весь объект, то части объ- екта, находящиеся вне окна, не вычерчиваются — они дол- жны быть отсечены. Эта операция носит название отсече- ние, более подробно она обсуждается в параграфе 2.5. 2. В общем случае коэффициенты /х и / различны. Для столб- чатой диаграммы это как раз то, что надо. Но совсем не го- дится, когда угловые соотношения на изображении должны быть точно такими же, как на объекте. В этом случае в ка- честве коэффициента масштабирования следует выбрать наименьшее из значений / и / . Поэтому рекомендуется х у
2.5. ОТСЕЧЕНИЕ ЛИНИЙ 23 3. заменить выражение (2.12) на формулы, основанные на пе- ресчете координат относительно центров окна и области вы- вода. Это будет описано в параграфе 2.6. Размеры и положение окна не всегда известны заранее. В параграфе 2.6 будет показан способ их вычисления вместо задания пользователем. 2.5. ОТСЕЧЕНИЕ ЛИНИЙ Предположим, что мировые и экранные координаты одинако- вы, то есть окно и область вывода совпадают. Поэтому в этом па- раграфе термин "окно" везде может быть заменен на термин "область вывода". Однако обычно считается, что отсечение вы- полняется по границам окна, а не области вывода, это и учитыва- ется в данном случае. На рис. 2.7 показан прямоугольник ABCD, который является окном. Все видимые отрезки прямых линий должны лежать внутри окна. То есть при вычерчивании отрезка прямой линии его части, лежащие вне окна, должны быть отсечены. Процесс отсечения должен выполняться автоматически. Команды на вы- черчивание треугольника PQR на рис. 2.7 интерпретируоются как команды на вычерчивание отрезков прямых линий Р'Р, PQ, QQ'. Прямоугольник ABCD вычерчивается раньше, так что вме- сто треугольника PQR нужно вычертить ломаную линию P'PQQ'. Углах УгЫп V *min max Рис. 2.7. Треугольник, подвергающийся операции отсечения
24 Глава 2. ДВУХМЕРНЫЕ АЛГОРИТМЫ Поскольку заданы только три точки Р, Q и R, координатная пара чисел (хр,,ур,) должна быть вычислена из значений коор- динат точек Up, yp) и 0cR, yR). Из рис. 2.7 видно, что наклон от- резка PR можно вычислить двумя способами, что приводит к сле- дующему уравнению: Ур'-Ур _ Уц-Уг Х-р' Лр Хт\ ~~" Лр Совмещая это уравнение с соотношением Ур'=утах получим ^R-V^max-V Отсюда легко вычисляются координаты точки Р', если из- вестно, что концевая точка Р находится внутри окна, а другая концевая точка RO^, yR) удовлетворяет неравенствам *min <XRK *max Однако необходимо рассмотреть значительно больше ситуа- ций. Большое разнообразие логических операций, которые нуж- но выполнить для решения этой задачи, делают проблему отсе- чения линий очень интересной с алгоритмической точки зрения. Из рис. 2.8 очевидно, что совершенно недостаточно отсечь отре- зок прямой линии PQ относительно прямой линии CD. Коэн и Сазерленд разработали алгоритм для отсечения отрезков пря- мых линий, который описан на языке Паскаль в книге Ньюмена и Спрула (1979). Здесь этот алгоритм будет представлен на язы- ке Си. С любой точкой РОс,у) будем ассоциировать четырехбитовый код h ь2 ь\ ьо где Ь. может быть либо 0, либо 1 (i = 0,1, 2, 3). Этот код содержит полезную информацию о положении точки Р относительно окна ABCD. В языке Си условные выражения вырабатывают значе- ния. Так значение выражения х < хт[п будет равно 1, то есть "ис- тина", если оно верно, и 0, то есть "ложь", если условие не вы- полняется. Используя это правило, можно записать
2.5. ОТСЕЧЕНИЕ ЛИНИЙ 25 Утах Ymin *min Рис. 2.8. Последовательные шаги отсечения Ь3= (х< xmin) Ы = (х > хтах) Ы = (y<ymin) ЬО = (у > ушах) /* точка Р слева от AD I* точка Р справа от ВС /* точка Р ниже АВ /* точка Р над CD V; V; V; V; Фактически могут существовать только девять из 16 возмож- ных битовых комбинаций, и они показаны на рис. 2.9. На языке Си значения кодов вырабатываются функцией int cod j(x, у) float x, у; { return (x<xmin)« 3 I (x>xmax)« 2 I (y<ymin)« 1 I (y>ymax); } Для понимания этого выражения необходимо знать, что вы- ражение Ъ « п означает, что битовое значение Ь сдвигается на п 1001 1000 1010 D А 0001 0000 оою с в 0101 01^0 оио Рис. 2.9. Значения кодов
26 Глава 2. ДВУХМЕРНЫЕ АЛГОРИТМЫ позиций влево. Кроме битовой операции сдвига « есть еще бито- вый оператор побитовой операции ИЛИ, записываемый как знак вертикальной черты (I). Этот оператор нельзя путать с ло- гическим оператором ИЛИ, обозначаемым двойной вертикаль- ной чертой (II). Менее эффективной записью вышеприведенно- го выражения в операторе возврата было бы выражение (х<х . )*8 + U>x )*4 + (y<v . )*2 + (y>y ) у mm7 v max7 v^ •'mm' y* •'max' Это выражение здесь приводится исключительно с целью облегчения восприятия предыдущего выражения. Описанная здесь функция code будет использоваться в функ- ции clip, в задачу которой входит анализ заданного отрезка пря- мой линии и вычерчивание только той его части, которая заклю- чена внутри окна ABCD, если такая часть существует. Эта функ- ция работает следующим образом. Если хотя бы один из кодов для точек Pj и Р2 содержит еди- ничный бит, то либо Рр либо Р2 перемещается из области вне окна к одной из границ окна или к ее продолжению. В последнем случае точка по-прежнему будет находиться вне окна и понадо- бится еще одно перемещение. Например, в случае, изображен- ном на рис. 2.8, за перемещением из точки Р в точку R должно последовать перемещение из точки R в точку S. Затем может по- требоваться отсечение другого конца отрезка, как это видно на рис. 2.8. Таким образом, процесс отсечения может быть много- ступенчатым, на каждом шаге расстояние между точками Р{ и Р2 уменьшается. Процесс завершается, как только обе точки ока- жутся в пределах окна. Оставшаяся часть отрезка PtP2 будет вы- черчена. Однако существует еще один важный случай, когда цикл должен быть завершен, а именно, когда обе точки Р{ и Р2 находятся вне окна и в то же время по одну сторону от окна. Эту ситуацию нельзя различить вначале, но она может возникнуть в процессе отсечения. Если концевые точки отрезка прямой линии находятся вне окна, то отрезок может пересекать окно, но может оказаться и полностью вне окна, как показано на рис. 2.8 и 2.10. На рис. 2.10 точки Pj и Р2 сначала не находятся одновременно ниже окна. Но в процессе отсечения точки Q и S определяют но- вые позиции точек Pj и Р2 соответственно. Поскольку теперь обе точки находятся ниже окна, то можно сделать вывод, что ничего не надо вычерчивать. Такое решение принимается на основе анализа значений code(x\, yl) и code(x2, y2). Точки Р{ и Р2 на-
2.5. ОТСЕЧЕНИЕ ЛИНИЙ 27 * J2 i ^— i ^— ^—""Q Pi Рис. 2. /О. Отрезок вне окна ходятся по одну сторону от окна тоща и только тогда, если их ко- ды содержат 1 в одной и той же позиции. Для трех точек Pj, Q и S третий бит слева СМ) в их кодах равен 1, тоща как для точки Р2 этот бит равен 0. Поэтому точка Р2 должна быть перемещена в точку S. Перемещение точки Pj в точку Q не является необходи- мым, но это перемещение выполняется потому, что оно не при- носит вреда, а реализация алгоритма получается проще. Подобно оператору побитовой операции ИЛИ> обозначаемо- му вертикальной чертой (I) и упомянутому ранее, в языке Си имеется оператор побитовой операции Я, обозначаемый знаком &. Напомним, что логические версии подобных операторов обоз- начаются символами I I и &&. Побитовые операторы (обозначае- мые знаками I и &) дают в результате битовые последовательно- сти, тоща как логические операций — простые значения 1 или О, обозначающие соответственно "истина" и "ложь". Не только 1, но и любое другое не равное 0 значение, представляет "истину", если оно используется в логической операции. Таким образом, циклическая конструкция while (cllc2) ... будет выполнять действия, обозначенные многоточием (...), до тех пор, пока вычисление побитовой операции с\ I cl будет выда- вать битовую последовательность, содержащую 1 в каком-либо
28 Глава 2. ДВУХМЕРНЫЕ АЛГОРИТМЫ бите, то есть пока в аргументах с\ или с2 существует хотя бы один бит, содержащий 1. С другой стороны, оператор if (cl & с2) return ; вызывает прямой выход из функции, если битовая последова- тельность, полученная в результате выполнения операции cl&c2, содержит хотя бы один единичный бит, то есть тогда, ког- да оба значения с\ и cl содержат единичный бит в одной и той же позиции. Полный текст функции clip содержится в конце сле- дующей программы, которая выполняет отсечение вложенных друг в друга пятиугольников. /* Программа CLIPDEMO */ /* Демонстрация работы алгоритма отсечения линий Коэна - Сазерленда*/ /* Demonstration of the Cohen & Sutherland line-clipping algorithm */ #include "math.h" float xmin-1.0, xmax-7.0, ymin-2.0, ymax-6.0; main() { inti; float r, pi, alpha, phiO, phi, xO, yO, x1, y1, x2, y2; pi-4.0*atan(1.0); alpha-72.0*pi/180.0; phiO-0.0; xO-4.0; y0-4.0; initgK); /* Вычерчивание границ окна */ /* The window is now drawn */ move(xmin, ymln); draw(xmax, ymin); draw(xmax, ymax); draw(xmin, ymax); draw(xmin, ymin); /* В пределах границ окна вычерчиваются 20 правильны*/ /* концентрических пятиугольников */ /* As far as permitted by the boundaries of the window, */ /* 20 concentric regular pentagons are drawn */ for(r-0.5;r<10.5;r-K).5) { x2-xf>r*cos(phi0); y2«y0+r*sin(phi0); for(M;K-5;l++) { phi-phiO+i*alpha; x1-x2; y1-y2; x2-xOH*cos(ph!);y2-yOfr*sin(phi); cllp(x1,y1,x2, y2); } } endgrQ; }
2.5. ОТСЕЧЕНИЕ ЛИНИЙ 29 int code(x, у) float x, у; { return (x<xmin)«3 I (x>xmax)«2 I (y<ymJn)« 1 I (y>ymax); } cljp(x1, y1, x2, y2) float x1, y1, x2, y2; { Int d-code(x1, y1), c2-code(x2, y2); float dx, dy; whlle(c1lc2) { if (d&c2) return; dx-x2-x 1; dy-y2-y 1; If(c1) { If (xKxmln) { y1 -н- dy*(xmin-x1)/dx; x1-xmln; } else If (x1>xmax) { y1 -н- dy*(xmax-x1)/dx; x1-xmax; } else If (yKymln) { x1 -H- dx*(ymln-y1)/dy; y1-ymln; } else If (y1>ymax) { xl -и- dx*(ymax-y1)/dy; y1-ymax; } c1-code(x1, y1); } else { if (x2<xmin) { y2 -^ dy*(xmin-x2)/dx; x2-xmin; } else if (x2>xmax) { y2 -н- dy*(xmax-x2)/dx; x2-xmax; } else If (y2<ymin) { x2 -H- dx*(ymln-y2)/dy; y2-ymin; } else if (y2>ymax) { x2 -н- dx*(ymax-y2)/dy; y2-ymax; } c2-code(x2, y2); } } move(x1, y1); draw(x2, y2); На рис. 2.11 показан результат работы этой программы. Рис. 2.11. Результат работы программы CUPDEMO
30 Глава 2. ДВУХМЕРНЫЕ АЛГОРИТМЫ 2.6. АВТОМАТИЧЕСКИЙ ПОДБОР РАЗМЕРОВ И ПОЗИЦИИ Чтобы картинка была нарисована в пределах границ области вывода, необходимо сначала выполнить отсечение по заданному окну, как было описано в параграфе 2.5, а затем отразить окно и его содержимое на область вывода, как было показано на рис. 2.4. Для большинства применений такая процедура вполне достаточ- на. Но в этом параграфе будет описан несколько иной подход, ко- торый отличается по следующим аспектам: 1) объект будет вычерчиваться целиком, так что отсечение не понадобится; 2) окно будет определено расчетом, а не задано заранее; 3) при отражении окна на область вывода будет использован одинаковый коэффициент масштабирования по обеим осям в горизонтальном и вертикальном направлениях. Из пункта 1 следует, что объект должен быть конечным. Для большинства применений это ограничение не является серьез- ным, но оно исключает панорамирование. Пункт 2 может быть реализован путем двойного просмотра данных, описывающих объект. Во время первого просмотра определяются границы окна *min' *max> Лшп' >W Вычерчивание производится во время вто- рого просмотра. Для этой цели будем использовать файл на дис- ке, чтобы исключить возможные затруднения из-за ограничения доступного объема памяти. Пункт 3 предполагает, что любой треугольник на картинке будет подобен исходному треугольнику на объекте, что означает неизменность угловых соотношений при отображении. Предположим, что на рис. 2.12 имеем заданный треугольник PQR, координаты вершин которого определены в системе миро- вых координат jcp = 1.0 jCq = 1.5 jcr=1.2 J>P = 0.8 Уд = 0.9 yR=l.l Тогда расчетными значениями параметров окна будут min max v . =0.8 у -1.1 •^min v*" •'max Заметим, что крайние точки объекта располагаются на границах окна, чего не было в случае, рассмотренном в параграфе 2.4.
2.6. АВТОМАТИЧЕСКИЙ ПОДБОР РАЗМЕРОВ И ПОЗИЦИИ 31 У 1 4 R(1.2, 1.1) Q (1.5, Q9) РО.О, 0.8) 1 2 Рис. 2.12. Объект, подлежащий масштабированию Чтобы предусмотреть некоторое свободное пространство со всех сторон экрана или листа бумаги, необходимо задать разме- ры области вывода несколько меньше, чем они могли быть на са- мом деле. Например, можно задать хт[п = 0.2 вместо 0.0. Область вывода будет полностью определена при выборе mm mm Как в параграфе 2.4, выполним расчет коэффициентов мас- штабирования: 8.2-0.2 X — X f _ max min х xmav - х„ max min 'у- Y - Y . max mm 1.5-1.0 6.5-0.5 1.1-0.8 = 16 = 20 В качестве коэффициента масштабирования / выберем наи- меньшее из значений /х и / . Напомним, что все расстояния ум- ножаются на коэффициент масштабирования, так что если будет выбран коэффициент масштабирования больше, чем/х или/ , то часть картинки определенно выйдет за пределы области вывода. В данном примере получим /=/=16
32 Глава 2. ДВУХМЕРНЫЕ АЛГОРИТМЫ Совершенно ясно, что такой одинаковый коэффициент мас- штабирования приведет к отображению треугольника, который точно равен по ширине размеру области вывода по оси х, но в на- правлении оси у останется свободное пространство. Его жела- тельно распределить поровну между нижней и верхней частями области вывода. Это можно реализовать, если для расчета кон- станты с^ вместо минимального значения Ymin, как в параграфе 2.4, выбрать позицию центра Yc. Аналогичным образом вычис- ляется и значение с1 ХС = 05 <W-*max> =0-5 d.0+1.5) = 1.25 УС - 05 <>Wn+ W - °'5 <0-8 + 1'1> - °'95 ХС = 05 <*min+ *max> = 05 <0'2 + 8'2> " 4'2 *С = 0-5 <W Утах> =0'5 <0.5 + 6.5) = 3.5 ^=^-/•^ = 4.2-16x1.25—15.8 с2-Ус-/-ус- 3:5-16x0.95 = 41.7 Теперь для любой точки объекта Ос, у) позиция отображае- мой точки (X, У) рассчитывается по формулам X=/x + c1 = 16jc-15.8 У-/-у + с2-16у-11.7 Составим программу, которая будет вычерчивать картинку, окно для которой не может быть задано заранее, и в этом приме- ре будет освещено несколько новых аспектов языка Си. Исполь- зуем случайные числа для генерации кривой непредсказуемой формы и размеров, где, как обычно, кривая аппроксимируется большим числом отрезков прямых линий. Автоматическое мас- штабирование и позиционирование позволяют решить задачу, практически не разрешимую иным путем. Для каждого отрезка будут генерироваться значения координат х и у, которые будут записываться в файл на диске. Точнее говоря, будем записывать так называемые структуры, содержащие тройки х у code где пара Ос, у) определяет координаты точки, в которую должно перемещаться перо, а параметр code может принимать значение 0 или 1, означающее состояние пера "поднято" или "опущено" соответственно. Другими словами, х у 0 означает moveiX, У) х у \ означает draw(X, У) где X и У — экранные координаты, соответствующие мировым
2.6. АВТОМАТИЧЕСКИЙ ПОДБОР РАЗМЕРОВ И ПОЗИЦИИ 33 координатам х и у. В этом примере будем использовать специаль- ную программу, которая генерирует кривую и записывает трой- ки чисел в файл Л,SCRATCH. За выполнением этой программы должен последовать запуск общей программы вычерчивания GENPLOT, которая дважды считывает тройки чисел. Первый раз — для определения параметров окна xmin, *max, у in, ymax и второй раз — для фактического выполнения операций переме- щения пера и вычерчивания, используя экранные координаты X, У, полученные путем пересчета координат х, у в системе миро- вых координат. В программе генерацию кривой будем начинать с точки нача- ла координат и перемещаться каждый раз на одну единицу рас- стояния. Всегда существует текущее направление^ и текущий угол поворота а. В исходном состоянии оба эти значения равны нулю. Перед каждым шагом угол а увеличивается на случайно выбранное значение угла в пределах от -6° до +6° (целое число). Полученный новый угол поворота добавляется к текущему на- правлению (р для получения нового направления. Ограничим максимальное значение кривизны и уменьшим шанс для вырож- дения в окружность. Для этого модифицируем описанный алго- ритм и будем задавать значение 0 для угла а каждый раз, когда его абсолютное значение превышает 15°. Тогда получим следую- щую программу: /* CURVGEN: Генерация случайной кривой */ /* Generation of a random curve */ #lnclude "math.h" #include "stdio.h" #include "time.h" /* Этот хидерный файл нужен при обращении к */ /* функции времени для получения случайного числа */ /* This header file defines the macro abs */ main() { inti. N-500; float x-0.0, y-0.0, xO.'yO, phi, direction(); pfopen(); pmove(x, y); for (1-1; K-ISU++) { xO-x; yO-y; phi-direction(); x-xO+cos(phi); y-yO+sin(phi); pdraw(x, y); } pfclose(); } 2-271
34 Глава 2. ДВУХМЕРНЫЕ АЛГОРИТМЫ float dlrection() { static int phi-O, alpha-O. first—1; /*Statlc variables are Initialized in the first call only! */ ЛСтатические переменные инициируются только при первом вызове!*/ float pl-3.1415926; long int seed; if (first) { first-O; tlme(&seed); srand((int)seed); } alpha-*-rand( )%13-6; If (abs(alpha)>15) alpha-O; phi-H-alpha; return ((float)phl*pi/180.0); } FILE *fp; struct {float xx; float yy; int ii;} s; pfopen() { fp-fopen("a.scratch", "wb"); } /* wb: untranslated */ /* wb - файл открыт для записи в бинарном режиме */ pmove(x, у) float x, у; { s.xx-x; s.yy-y; s.ii-O; /* 0 - pen up */ /* 0 - перо поднято */ fwrlte(&s, sizeof s, 1, fp); } pdraw(x, y) float x, y; { s.xx-x; s.yy-y; s.ii-1; /* 1 - pen down */ /* 1 - перо опущено */ fwrite(&s. sizeof s, 1, fp); } pfclose() { fclose(fp); } В функции direction показано обращение к функции инициа- лизации датчика случайных чисел srand. Ее аргумент seed опре- деляет начальное значение для генерации случайных чисел. Функция time применяется для присвоения значения аргументу seed, зависящему от текущего времени. Таким образом будут ге- нерироваться различные кривые при каждом новом запуске про- граммы. Функция rand выдает большое неотрицательное целое число. Оно преобразуется в целое число в диапазоне от 0 до 12 путем использования остатка от деления на 13, то есть 0<rand()%\3 < 12 -6<rand()%l3-6<6 Другие новые аспекты применения языка Си относятся к опе- рациям ввода и вывода. Для большинства функций ввода/выво- да в программу необходимо вставлять директиву ^include <stdio.h>, которая осуществляет "включение" файла стандарт- ных заголовков функций ввода/вывода. В языке Си различается орорматированный и не4юрматированный ввод/вывод. Наиболее часто используются следующие функции:
2.6. АВТОМАТИЧЕСКИЙ ПОДБОР РАЗМЕРОВ И ПОЗИЦИИ 35 scanf: форматированный ввод с терминала printf: форматированный вывод на терминал fscanf: форматированный ввод из файла fprintf: форматированный вывод в файл fread: неформатированный ввод из файла fwrite: неформатированный вывод в файл. Форматированный ввод/вывод имеет дело с читаемыми сим- волами: имеется структура строк, аналогичная строкам на пе- чатной странице. Неформатированные данные имеют такую же структуру, как они записаны в памяти. Например, целые числа представляются фиксированным количеством битов. В нашем случае с целью повышения эффективности был использован не- форматированный ввод/вывод. Файл "открывается" обращени- ем к функции fopen. Вторым аргументом может быть либо строка "г" для инициирования считывания, либо строка "и>" для записи. (Для неформатированного ввода/вывода некоторые компилято- ры требуют задания иных строк вместо "г" или "и>".) Перемен- ная 5 обозначает структуру, содержащую три числа: jc, у и code, которые будут записываться в файл. "Указатель" &s на эту пере- менную является первым аргументом для функции fwrite. Запись &s можно рассматривать как обозначение адреса переменной s. Второй аргумент, sizeofs, равен размеру одной структуры s, тре- тий аргумент, равный 1, означает количество структур, подле- жащих записи. Четвертым аргументом, //?, является указатель файла, полученный из обращения к функции fopen. Теперь мож- но рассмотреть общую программу, которая будет считывать тройки, определять размеры окна, выполнять пересчет мировых координат в экранные координаты и, наконец, вычерчивать кар- тинку в пределах заданной области вывода. Также вычерчива- ются небольшие уголки в углах области вывода и точка в середи- не нижней границы, так что абстрактную картинку можно ори- ентировать относительно верха и низа (если точка и уголки нежелательны, их легко можно исключить). Заметим, что в этой программе файл ASCRATCH открывает- ся дважды. Операции закрытия и открытия приводят к "пере- мотке" файла к началу. /* GENPLOT: */ /* A general adjusting and plotting program. */ /* The file A.SCRATCH contains Input data. */ 2**
36 Глава 2. ДВУХМЕРНЫЕ АЛГОРИТМЫ /* Общая программа определения размеров и вычерчивания */ /* В файле A.SCRATCH содержатся входные данные */ #include "stdio.h" main() { float x, у, xmin, xmax, ymln, ymax, X, Y, Xmin, Xmax, Ymin, Ymax, fx, fy, f, xC, yC, XC, YC, d. c2; FILE *fp; struct {float xx; float yy; Int II;} s; fp-fopen("a.scratch", "rb"); /* параметр "rb" необходим для чтения в бинарном режиме */ /* required In Lattice С for binary mode */ xmln-ymln-1e30; xmax-ymax—xmin; while (fread(&s, slzeof s, 1, fp)) { x-s.xx; y-s.yy; If (x<xmin) xmln-x; If (x>xmax) xmax-x; If (y<ymln) ymin-y; If (y>ymax) ymax-y; } fclose(fp); lnit_viewport(&Xmln, &Xmax, &Ymin, &Ymax); fx"KXmax-Xmln)/(xmax-xmln); fy-(Ymax-Ymln)/(ymax-ymln); Hfx<fy?fx:fy); xC4).5*(xmln+xmax); yOK).5*(ymin+ymax); XC-O.5*(Xmin+Xmax);YC-0.5*(Ymln+Ymax); d-XC-f*xC; c2-YC-f*yC; fp-fopen("a.scratch", "rb"); /* Lattice С: бинарный режим */ /* Lattice С: binary mode */ while (fread(&s, slzeof s, 1, fp)) { x-s.xx; y-s.yy; X-f*x+c1;Y-f*y+c2; if (s.ii) draw(X, Y); else move(X, Y); } fclose(fp); endgr(); } init_viewport(pXmin, pXmax, pYmin, pYmax) float *pXmln, *pXmax, *pYmin, *pYmax; { float Xmin, Xmax, Ymin, Ymax, eps-0.2; printf /* "Give viewport boundaries */ ("Задайте границы области вывода Xmin, Xmax, Ymin, Ymax\n"); scanf("%f %f %f %f", &Xmin, &Xmax, &Ymin, &Ymax); /* Отмечаются четыре угла области вывода: */ /* Show the four viewport corners: */ inltgr(); move(Xmin, Ymin+eps); draw(Xmin, Ymin); draw(Xmin+eps, Ymin); move(Xmax-eps, Ymin); draw(Xmax, Ymin); draw(Xmax, Ymin+eps); move(Xmax, Ymax-eps); draw(Xmax, Ymax); draw(Xmax-eps, Ymax); move(Xmin+eps, Ymax); draw(Xmin, Ymax); draw(Xmin, Ymax-eps);
2.7. ПРИМЕНЕНИЕ РЕКУРСИЙ 37 move((Xmin+Xmax)/2, Ymin); draw((Xmin+Xmax)/21 Ymin); /* Точка в середине нижней границы для ориентации */ /* Dot in the middle of bottom boundary for orientation */ *pXmin-Xm!n; *pXmax-Xmax; *pYmin-Ymin; *pYmax-Ymax; } Аргументами функции init_yiewport являются указатели. Обозначение float *pXmin означает, что *pXmin имеет тип float, так что pXmin будет иметь тип pointer to float (указатель на float). Унарные операторы & и * являются взаимно обратными, так что pXmin = &Xmin и Xmin = *pXmin. Результат работы про- граммы показан на рис. 2.13. Рис. 2.13. Результат работы программ CURVGEN и GENPLOT 2.7. ПРИМЕНЕНИЕ РЕКУРСИЙ Ряд задач может быть решен с применением рекурсии. Вот один из примеров: Для заданных трех чисел хс, ус и г соединить точки с коорди- натами xi= ХС + r cos ^i УгУс + rsirnp. « = 0,1,2,3,4,5; р.-Ы44в) что в результате дает звезду. Последовательно (как часть зада- чи!) выполнить подобную работу еще пять раз, но теперь с други- ми тремя числами: xc, = xc + 2rcos<p yc, = yc + 2r sin <p г' =0.5 г (/=0,1, 2,3,4; ^=36°+у-7Г)
38 Глава 2. ДВУХМЕРНЫЕ АЛГОРИТМЫ Выполнение задачи продолжается до тех пор, пока задавае- мая величина г будет не меньше 0.1. Для начальной (или глав- ной) задачи задаваемые числа будут равны хс - 0, ус - 0, г - 1. Как и ранее, будем использовать автоматическое масштабирова- ние и размещение. Ниже приводится программа, решающая по- ставленную задачу. /* STARS: Звезды различных размеров */ /* ' Stars of various sizes */ #include "math.rT maln() { pfopen(); starfO.O, 0.0, 1.0); pfclose(); } star(xC, yC, r) float xC, yC, r; { float phi, r_half, redouble, factor-0.0174533; /* factor- pi/180 */ int I; if (r<0.1) return; pmove(xC+r, yC); for(i-1;i<-5;i++) { phl-i*144*factor; pdraw(xC+r*cos(phi), yC+r*sin(phi)); } r_half-0.5*r; r_double-2*r; for (Ю; i<5; I-H-) { phi-{36+i*72)*factor; star(xC+r_double*cos(phi), yC+r_double*sin(phl), rhalf); } } #include "stdio-h" FILE *fp; struct {float xx; float yy; iht ii;} s; pfopen(){ fp-fopen(Ha.scratch", "wb"); } pmove(x, y) float x, y; { s.xx-x; s.yy-y; s.ii-0; /* 0 - pen up */ /* 0 - перо поднято */ fwrite(&s, sizeof s, 1, fp); } pdraw(x, y) float x, y; { s.xx-x; s.yy-y; s.ii-1; /* 1 - pen down */ /* 1 - перо опущено */ fwrite(&s, sizeof s, 1, fp); } pfclose() { fclose(fp); }
2.7. ПРИМЕНЕНИЕ РЕКУРСИЙ 39 ***+: Рис. 2.74. Результат работы программы STARS После этой программы должна быть вызвана программа GENPLQT, описанная в предыдущем параграфе, результат их совместной работы показан на рис. 2.14. Следующим примером может быть известное " Пифагорово дерево". Оно часто изображается так, как показано на рис. 2.15. Каждый из прямоугольных треугольников в этом дереве имеет внутренний угол, равный 45°. Опять воспользуемся генератором случайных чисел для создания более общей программы, которая может сформировать не только рис. 2.15, но также генерировать и менее регулярные деревья. Углы, задаваемые равными 45° для рис. 2.15, в общем случае будут задаваться случайным образом в пределах между (45 - delta)0 и (45 + delta)9, где значение delta задается в качестве входного параметра вместе с параметром п, определяющим глубину рекурсии. Регулярная версия, изобра- женная на рис. 2.15, получается при задании delta = 0 и п = 7. На рисунке параметр п определяет количество треугольников на
40 Глава 2. ДВУХМЕРНЫЕ АЛГОРИТМЫ пути от корня до листьев дерева. Сердцевиной программы будет рекурсивная функция squarejmdjriangle ("квадрат и треуголь- ник") с параметром п, определяющим глубину рекурсии, в каче- стве первого аргумента. Если значение параметра п больше ну- ля, то задачей функции squarejandjtriangle, как это определяет- ся названием, будет вычертить квадрат и~над ним треугольник, а затем еще дважды обратиться к самой себе с соответствующими новыми аргументами, первый из которых задается равным п - 1. Размер и положение квадрата полностью определяются четырь- мя параметрами: х0, у0, а и <р (см. рис. 2.16). Для вычерчивания треугольника необходимо знать угол а. Этот угол, выраженный в градусах, равен 45 + deviation, где deviation равно одному из це- лых чисел ряда -delta, -delta + 1,..., delta, выбираемому случай- ным образом, как это делалось в параграфе 2.6. На рис. 2.16 необходимые точки пронумерованы последова- тельными числами 0, 1,2, 3, 4. Координаты х0, у0 точки О зада- ются в обращении к функции. Для вычисления остальных точек вначале рассмотрим более простую ситуацию при р = 0, то есть когда сторона 0 1 квадрата занимает горизонтальное положение. Рис. 2.15."Пифагорово дерево", регулярная версия
2.7. ПРИМЕНЕНИЕ РЕКУРСИЙ 41 Рис. 2.16. Нумерация точек В этом положении координаты точек определить очень просто. Они записываются в массивах х и у. Затем вся конструкция пово- рачивается вокруг точки О на угол <р таким же образом, как это выполнялось в параграфе 2.3. Результат поворота записывается в массивах ххиуу. /* PYTH_TREE: Вариант дерева Пифагора */ /* Variants of the tree of Pythagoras */ #include "math.h" #include "time.h" #define pi 3.1415927 int delta; long int seed; main() { int n; pfopen(); time(&seed); srand((int)seed); printf /* "Give angle delta in degrees (0 < delta < 45)" */ ("Задайте угол delta в градусах (0 < delta < 45)\n"); scanf("%d", &delta); printf /* "Give recursion depth n") */ ("Задайте глубину рекурсии n\n"); scanf("%d", &n); square_and_triangle(n, 0.0, 0.0, 1.0, 0.0); pfclose(); > , square_and_triangle (n, xO, y0, a, phi) Int n; float xO, yO, a, phi; { float x[5], y[5], xx[5], yy[5], cphi, sphi, d, c2, b, c, alpha, calpha, salpha; int i, deviation; /* phi and alpha in radians */ /* delta in degrees */
42 Глава 2. ДВУХМЕРНЫЕ АЛГОРИТМЫ If (n- -О) return; /* углы phi и alpha в радианах */ /* угол delta в градусах */ devlatlon-rand()%(2*delta+lHelta; alpha-(45+devlatlon)*pl/180.0; x[0}-x[3hx0; х[1}-х[2НОа; у[0]-у[1}-у0; у[2]-у[3]-у(На; calpha-cos(alpha); salpha-sln(alpha); c-a*calpha; b-a*salpha; x[4}-x[3]+c*calpha; y[4}-y[3}*c*salpha; /* Rotation about (xO, yO) through angle phi; */ /* this was explained In Section 2.3 */ /* Поворот вокруг точки (xO, yO) на угол phi; */ /* эта операция была описана в параграфе 2.3 */ cphl-cos(phl); sphl-sln(phl); d-xf>-xO*cphl+yO*sphl; c2-y0-x0*sphl-y0*cphi; " for(l-0;K5;l++) { xx[lHti]*cphl-y[l]*sphl+c1; yy[l}*x[i]*sph1+y[l]*cphl-bc2; } pmove(xx[3], yy[3]); for (1-0; l<5; I++) pdraw(xx[l], yy[ljj; pdraw(xx[2], yy[2]); square_and_trlangle(n^1, xxf3], уу[3], с, phl+alpha); square_and_trlangle(n-1, xx[4], yy[4], b, phl+alpha-0.5*pi); #lnclude "stdlo.h" FILE *fp; struct {float xx; float yy; Int II;} s; pfopen(){ fp4open("a.scratch", nwbw); } pmove(x, y) float x, y; { s.xx-x; s.yy-y; s.lK); /* 0 - pen up */ /* 0 - перо поднято */ fwrite(&s, slzeof s, 1, fp); } pdraw(x, y) float x, y; { s.xx*x; s.yy-y; s.lM; /* 1 - pen down V /* 1 ■* перо опущено */ fwrlte(&s, slzeof s, 1, fp); } pfclose(){fclose(fp);} Эта программа формирует файл A.SCRATCH, который дол- жен быть обработан программой GENPLOT из параграфа 2.5. Графический результат работы программы для delta ш 30 и п «* 7 показан на рис. 2.17.
2.8. СГЛАЖИВАНИЕ КРИВЫХ 43 Рис. 2.17. Типичный результат работы программы PYTHJTREE 2.8. СГЛАЖИВАНИЕ КРИВЫХ В автоматизированном проектировании и управлении стан- ками часто требуется построить гладкую кривую или гладкую поверхность по набору заданных точек. Здесь рассмотрим только двухмерное пространство, поэтому ограничимся анализом кри- вых в плоскости ху. Анализ плоских кривых послужит основой для построения поверхностей на более поздней стадии. Из нескольких возможных способов построения гладких кри- вых выберем форму В-сплайна. Из заданной последовательности точек выбираются две соседние точки и между ними строится кривая кубического полинома на основе позиций четырех точек — двух уже упомянутых и двух соседних с ними точек. В-сплайн обеспечивает получение более гладких кривых, чем другие спо- собы сглаживания за счет того, что получаемые кривые не прохо- дят точно через заданные точки. Математически гладкость кри- вых выражается в терминах непрерывности параметрических представлений x(t) и y(t) и их производных. Кривые типа В- сплайна обладают свойством непрерывности даже вторых произ- водных x"(t) и У (0 в точках стыковки двух соседних сегментов
44 Глава 2. ДВУХМЕРНЫЕ АЛГОРИТМЫ (а) (б) (в) Рис. 2.18. (а) — нулевая производная разрывна; (б) — первая производная не непрерывна; (в) — вторая производная не непрерывна кривой. Из рис. 2.18 можно видеть, как выглядят кривые, если их нулевая, первая и вторая производные не непрерывны в некото- рой точке. Кривая на рис. 2.18(b) может считаться гладкой, но она не удовлетворяет строгим требованиям, которые выполня- ются в способе В-сплайна. После такого краткого обсуждения рассмотрим этот метод в работе. Будем использовать параметрическое представление кривых. Любая точка части кривой между двумя заданными по- следовательными точками Р и Q будет иметь координаты x(t) и y(t), ще t увеличивается от 0 до 1, если отслеживается часть кри- вой от точки Р до точки Q. Можно считать, что t — это время. Если имеются заданные точки p0u0,:v0) *V*n' уп^ то часть кривой В-сплайна между двумя последовательными точками Р. и Р^.+1 получается путем вычисления функций x(t) и y(t) для изменения t от 0 до 1 x(t) = {(a^t + a2)t^al} +aQ y(t) = {(b3t + b2)t + bl}+b0 Эти уравнения содержат следующие коэффициенты: аъ = (~*ы + 3xi" 3xi+i + *м>/6 a2=(xhl-2Xi + xi+l)/2 (213) fli-<-*H+*jH>/2 *6"<*Ы+4*1 + *М)/б
2.8. СГЛАЖИВАНИЕ КРИВЫХ 45 а коэффициенты by b2, bx, b0 вычисляются по значениям у._х, у., y.+v y^2 аналогичным образом. Вышеприведенные формулы пригодны для эффективных вычислений. Вычисление значений х(0 производится быстрее по правилу Горнера, чем по обычному полиномиальному выражению. Коэффициенты д3, а2, ах, #0 вы~ числяются только однажды для каждого сегмента кривой, что очень важно, поскольку на каждом сегменте кривой может вы- числяться большое число промежуточных точек x(t) и y(t). Для получения некоторого представления о свойствах кривой в точках стыковки двух сегментов рассмотрим функцию x(t) и ее первую и вторую производные для значений t=0 и t = 1 (функция y(t) будет обладать аналогичными свойствами) х(0) = а0 = UM + 4xi + xM) /6 Используя уравнение (2.13), после упрощения получаем дс(1) = Ц. + 4х/+1+дс/+2)/6 Можно видеть, что значение х(0) не равно в точности дс-коор- динате jc. точки Р;.: оно зависит от позиций точек P._j и Р.+1. Из рис. 2.19 видно, что точка В является точкой сегмента АВ, но од- новременно и начальной точкой сегмента ВС. Рис. 2.19. Три последовательные точки Для первого сегмента имеем { А = Р, В = Р/+1, ОРм хв* = х(0) = (хА + 4хъ + хс) /6 где через дсв* обозначено вычисленное значение координаты х для точки В. Рассматривая эту же точку как принадлежащую сегменту ВС, получим А = Р.+1, В = Р/( С = Р.+1 хв* = х(0) = (д:А + 4хв + хс)/Ь
46 Глава 2. ДВУХМЕРНЫЕ АЛГОРИТМЫ Отсюда видно, что оба способа вычисления значения х дают одинаковый результат, что означает непрерывность функции x(t) в точке В. Продифференцировав x(t) дважды, найдем произ- водные х' (0 и х'' (0. Подставляя в них значения t * 0 и t - 1, как это было сделано для x(t), можно будет убедиться, что производ- ные непрерывны в точке В. Поскольку функция y(t) и ее первые две производные тоже непрерывны, то становится ясно, что кри- вая В-сплайна очень гладкая. Для расчета любого сегмента кривой между точками Р. и Рм используются также точки Рн и Р^2. Из этого следует, что пер- вый сегмент кривой будет располагаться между точками Р1 и Р2, а последний — между точками Р 2 и Р 1# Так что начальная и конечная точки всей кривой будут располагаться вблизи Pj и Рп_р но не вблизи Р0 и Рл. Приведенная ниже программа считы- вает числа п хо Уо хх Уу хп уп из файла CURV.DAT. При выводе каждая из п + 1 точек обозна- чается маркером в виде креста. Затем вычерчивается кривая В- сплайна. /* CURVFIT: Сглаживание кривой с применением В-сплайна */ /* Curve fitting using В splines */ #lnclude "stdio.h" #define MAX 100 #define N 30 main() { float x[MAX]t yfMAX], eps-0.04, X, Y, t, xA, xB, xC. xD, yA, yB, yC, yD, aO, a1, a2, аЗ, ЬО, Ы, Ь2, Ь3; Int n, i, j, first; FILE *fp; fp-fopen("curv.dat", "r"); if (fp- -NULL) { printf /* "There is no file curv.dat" */ ("Нет файла curv.datAn"); exlt(1); } fscanf(fp, "%d", &n); for(i-0;i<-n;i++) if (fscanf(fp, "%f %f\ x+l, y+i)<-0) { printf /* "Reading beyond the end of file curv.dat" */ ("Считывание выходит за пределы файла curv.dat\n"); exit(1); }
2.8. СГЛАЖИВАНИЕ КРИВЫХ 47 initgK); /* Mark the given points: */ /* Заданные точки отмечаются маркером: */ for(i-0;i<-n;i++) { X-x[i]; Y-y[i]: move(X-eps, Y-eps); draw(X+eps, Y+eps); move(X+eps, Y-eps); draw(X-eps, Y+eps); } first-1; for(i-1;i<n-1;i++) { xA-x[M]; xB-x[i]; xOx[i+1]; xD-x[l+2]; yA-y[i-1]; yB-y[i]: yC-y[i+1]; yD-yfi+2]; a3-(-xA+3*(xB-xC>fxD)/6.0; b3-(-yA+3*(yB-yC}fyD)/6.0; a2-{xA-2*xB+xC)/2.0; Ь2-(уА-2*уВ+уС)/2.0; a1-(xC-xA)/2.0; M-(yC-yA)/2.0; aO-(xA+4*xB+xC)/6.0; Ь0-(уА+4*уВ+уС)/6.0; for(j-0;J<-N;j++) { t-{float)j/(float)N; X-((a3*t+a2)*t+a 1)*t+a0; Y-((b3*t+b2)*t+M)*t+b0; if (first) { first-0; move(X, Y);} else draw(X, Y); } } endgr(); fclose(fp); Если в файле С URV. DAT будут записаны числа: 20 .75 .5 .75 1.5 2.25 2.5 3 5 6.25 6.5 6.5 (6.5 6.25 5 3 2.5 2.25 1.5 .75 .5 .75 1.4 1 .6 .4 .7 .9 .85 .75 .8 .85 1 1.15 1.2 1.25 1.15 1.1 1.3 1.6 1.4 1 .6
48 Глава 2. ДВУХМЕРНЫЕ АЛГОРИТМЫ то в результате работы программы получим изображение, пока- занное на рис. 2.20. X Рис. 2.20. Результат работы программы CURVFIT УПРАЖНЕНИЯ 2.1. Напишите программу для вычерчивания набора из N эллип- сов, для которых определены параметрические уравнения х = хо + ^/^ cos 9 y = y0+((N-i)R/N) sin<p Возьмите фиксированные значения х^, >>0, R, N; например, х0 = ^' ^о = ^.5, Я = 3, N = 40. Пусть i изменяется в диапазоне от 1 до N - 1. Пусть для каждого значения i угол последова- тельно принимает значения 0°, 6°, 12°,..., 360°. 2.2. Напишите программу для вычерчивания набора из 30 треу- гольников. Вершины первого треугольника заданы в точках (1, 1), (6, 1), (3, 5). Каждый следующий треугольник пол- учается путем поворота предыдущего на угол 3° вокруг точ- ки (3, 3). 2.3. Реализуйте рекурсивный алгоритм отсечения линий, осно- ванный на делении отрезка пополам. Предположим, что за- даны окно и отрезок прямой линии PQ. Если обе точки Р и Q находятся внутри окна, то отрезок вычерчивается полно- стью. Это условие охватывает также и тот случай, когда од- на точка находится внутри окна, а вторая — на границе. Существуют такие ситуации, когда можно легко опреде- лить, что ничего не надо вычерчивать. Проработайте их са- мостоятельно. Во всех других случаях необходимо найти точку М посередине отрезка PQ и рекурсивно применить эту процедуру к отрезкам РМ и MQ. Обеспечьте устойчивость к границам окна и исследуйте влияние количества делений отрезков пополам на эту устойчивость.
49 2.4. Разработайте и реализуйте алгоритм для отсечения частей линий, если окно задано в форме треугольника. 2.5. Модифицируйте программу STARS из параграфа 2.7 так, чтобы звезды не налагались друг на друга. 2.6. Используйте случайные числа для генерации последова- тельности, скажем, из 30 точек и примените программу сглаживания для проведения кривой примерно через эти точки. 2.7. Напишите программу, которая формирует более реалистич- ное дерево, чем на рис. 2.17. Пример изображения показан на рис. 2.21. Рис. 2.21. Дерево
Глава 3 ГЕОМЕТРИЧЕСКИЙ ИНСТРУМЕНТ ДЛЯ АЛГОРИТМОВ ТРЕХМЕРНОЙ ГРАФИКИ 3.1.ВЕКТОРЫ Необходимо иметь определенную математическую подготов- ку, чтобы понимать и писать программы для трехмерной графи- ки. Эта книга не является учебником по математике, и здесь предполагается, что читатель уже знаком с математическими приемами, используемыми в данной главе, особенно с векторами и детерминантами. Вектор — это направленный отрезок прямой линии, харак- теризуемый только его длиной и направлением. На рис. 3.1 пока- заны два представления одного и того же вектора а = PQ e b - RS. Следовательно, при переносе вектор не изменяется. На рис. 3.2 начальная точка вектора b совпадает с конечной точкой вектора a. Тогда сумма векторов а и b определяется как вектор с, прове- денный из начальной точки вектора а в конечную точку вектора b, поэтому можно записать с = а+Ь / * Рис. 3.1. Равные векторы
3.1. ВЕКТОРЫ 51 Рис. 3.2. Сложение векторов Длина вектора а обозначается I a I и равна расстоянию между его начальной и конечной точками. Вектор с нулевой длиной на- зывается нулевым вектором и обозначается 0. Обозначение -а применяется для вектора, имеющего длину I a I, направление ко- торого обратно направлению вектора а. Для любого вектора а и вещественного числа с вектор са имеет длину I с I I а I. Если а = 0 или с = 0, то са = 0, в противном случае вектор са совпадает по направлению с вектором а, если с > 0, или имеет противополож- ное направление, если с < 0. Для любых векторов u, v, w и веще- ственных чисел с, k будем иметь u+v=v+u (и + v) + w = и + (v + w) и + 0 = и и + (-и) = 0 c(u + v) =cu + cv (с + Л)и = си + £и с (Ли) = (ск)и 1и = и 0и = 0 На рис. 3.3 показаны три единичных вектора i, j, k. Они вза- имно перпендикулярны, имеют длину, равную 1, и определяют направления координатных осей. Можно сказать, что векторы i, j, k образуют тройку ортогональных единичных векторов. Коор- динатная система является правой, это означает, что если пово- рот от вектора i к вектору j на 90° соответствует повороту винта с правой резьбой, то вектор к совпадает с направлением переме- щения винта. Точка О в начале координатной системы часто является на- чальной точкой всех векторов. Любой вектор v может быть запи- сан как линейная комбинация единичных векторов i, j, k v = дл + у] + zk
52 Глава 3. ГЕОМЕТРИЧЕСКИЙ ИНСТРУМЕНТ Рис. 3.3. Правая координатная система Вещественные числа х, у, z определяют координаты конечной точки Р вектора v = OP. Этот вектор v может быть обозначен либо в виде строки, либо в виде столбца v = [х у z ] или Числа х, >', z иногда называют элементами вектора v. 3.2. СКАЛЯРНОЕ ПРОИЗВЕДЕНИЕ Скалярное произведение двух векторов а и b обозначается через а • b и определяется как ab= lal Ibl cosy еслиа*0иЬ#0 ~- ab = 0 еслиа = ОилиЬ = 0 где у — угол между а и Ь. Применяя это правило к единичным векторам i, j, k, находим ii = jj = kk=l ij=:ji = jk = kj = ki = ik = 0 Приравнивая а - b в уравнении (3.1), получаем а-а = lalz, так что lal = Vla-al Важные свойства скалярного произведения: c(ku-vy=ck(u-\) (cu + £v) w = cuv/ + k\w uv = vu u • u = 0 только если u = О (3.2) 2
3.3. ДЕТЕРМИНАНТЫ 53 Скалярное произведение векторов u = [Wj w2 w3 ] и v = [Vj v2 v3 ] может быть вычислено как u-v = w1v1+w2v2 + w3v3 Это легко доказать, переписав правую часть уравнения it v = (и{\ + и2] + w3k) • (Vji + v2j + v3k) в виде суммы девяти скалярных произведений и проанализиро- вав их на основании выражения (3.2). 3.3. ДЕТЕРМИНАНТЫ Перед описанием векторного произведения обратим внима- ние на детерминанты. Чтобы решить следующую систему двух линейных уравнений: [ахх + Ьу = сх {а2х + о2у-с2 необходимо умножить первое уравнение на коэффициент bv a второе — на коэффициент -Ь{ и сложить, тогда получим (ахЬ2-а2Ьх)х = Ь2сх~Ьхс2 После этого можно первое уравнение умножить на -я2, а второе — на ах и также сложить. В результате получим (alb2-a2b{)y = alc2-a2cl Если ахЬ2 - а2Ьх не равно нулю, то можно выполнить деление и найти Ь2с1-Ь1с2 а\с2"агс\ 34 {ахЬ2-а2Ъх) ' у {ахЬ2-а2Ьх) Выражение в делителе можно записать в форме а2 Ь2 В этом случае оно называется детерминантом второго порядка. Следовательно, ах Ъх а2 Ъ2 = ахЬ2-а2Ьх С помощью детерминантов уравнение (3.3) может быть запи- сано в виде D\ D2 *=-Р7"> У--7Г. <^*°>
54 Глава i. ГЕОМЕТРИЧЕСКИЙ ИНСТРУМЕНТ 1 а\ *1 1 1 \а2 Ь2 D, = 1 с, Ьх 1 1 с2 Ь2 D~> = 2 а. с. 1 1 а2сг1 где Заметим, что D{ получается путем замены /-го столбца в D на правую часть системы уравнений (3.3) (/= 1 или 2). Такой способ решения системы линейных уравнений называется "правилом Крамера". Этот способ пригоден не только для системы двух уравнений (хотя с точки зрения затрат машинного времени он оказывается очень дорогим для больших систем). Определим де- терминант третьего порядка в виде уравнения D = \а2 Ь2 С2 а3 Ь3 с3 = й1 h съ ~а2 h ci ьъ сз + аз Ь2 с2\ и детерминант четвертого порядка £> = а\ bi c\ d\ а2 Ь2 с2 с2 аЪ ЬЪ СЪ d3 а4 b4 c4 dA Ь2 с2 <12 ЬЪ с3 d3 b4 c4 d4 ~а2 Ъх с, dx b3 с3 d3 b4 c4 d4 +аУ h ci d\ b2 c2 d2 *4 C4 dA -aA b\ c\ d\ b2 c2 d2 h сз d3 и так далее. Детерминанты имеют много интересных свойств, некоторые из них перечислены ниже. 1. Значение детерминанта не изменится, если строки записать в виде столбцов в том же порядке, например: а2 Ь2 а\ а2 Ь\ Ь2 2. Если произвести взаимную замену двух строк (или двух столбцов), то значение детерминанта будет умножено на -1: К ь\ с\ \а2 Ь2 С2 \аз h сз ss — а\ ь\ с\ аз ьз сз а2 Ъг с2 3. Если любую строку (или столбец) умножить на коэффици- ент, то значение детерминанта также будет умножено на этот коэффициент. Например:
3.3. ДЕТЕРМИНАНТЫ 55 сах cb а2 Ъ2 4. Если строка (или столбец) изменяется путем добавления соответствующих элементов другой строки (или столбца), умноженных на константу, то значение детерминанта не из- менится. Например: а\ *1 с1 а2 ъг с2 а^+ках Ь3+кЬх с3+ксх = ах Ьх с, а2 Ь2 с2 аз h сз 5. Если строка (или столбец) является линейной комбинацией некоторых других строк (или столбцов), то значение детер- минанта равно нулю. Например: а2 h с2 ■О \3al-2a2 3ftj-262 3cj-2c2| Существует много полезных применений детерминантов. Де- терминантные уравнения, выражающие геометрические свойст- ва, элегантны и легки для запоминания. Например, уравнение для прямой линии в двухмерном пространстве, R2, проходящей через две точки Pj (хх, ух) и Р2(*2, Ут}'может быть записано в ви- де \х \х1 1*2 У У\ У2 1| 1: l| = 0 (3.5) Такая запись становится очевидной, если, во-первых, рас- сматривать уравнение (3.5) как специальное обозначение ли- нейных уравнений по х и у и, следовательно, представляющих прямую линию в пространстве R2, и, во-вторых, можно убедить- ся, что координаты обеих точек Pj и Р2 удовлетворяют этому уравнению, поскольку при их подстановке в первую строку по- лучим две одинаковые строки. Аналогично, плоскость в трехмер- ном пространстве, R3, проходящая через три точки Р х (jCj, ух, z{), Р2(*2, у2, z2), Р3(*з> % 23^' будет описываться уравнением -0 X *1 Х2 1 *з У У\ У2 у* 2 Zl Z2 z3 1 1 1 1
56 Глава 3. ГЕОМЕТРИЧЕСКИЙ ИНСТРУМЕНТ 3.4. ВЕКТОРНОЕ ПРОИЗВЕДЕНИЕ Векторное произведение двух векторов а и b обозначается а*Ь и равно вектору v, который обладает следующими свойствами. Если а = сЬ для некоторого скаляра с, то v = 0. В противном слу- чае длина вектора v равна I v I = I a I I b I sin у где у — угол между векторами а и Ь, а направление вектора v перпендикулярно обоим векторам а и b и таково, что a, b, v, именно в таком порядке, образуют правостороннюю тройку. По- следнее означает, что если вектор а поворачивается на угол у < 180° в направлении к вектору Ь, то вектор v имеет направле- ние перемещения винта с правой резьбой, поворачиваемого в том же направлении. Из этого определения можно вывести следую- щие свойства векторного произведения: (ka) х b = Л(а х b) для любого вещественного числа к ax(b + c)=axb + a*c а х ь = -b х а В общем случае а х (b x с) * (а * Ь) * с. Используя правую ортогональную систему координат, как и в параграфе 3.1, с единичными векторами i, j, k, будем иметь i ж i = j х j = kxk = 0 ixj = k, jxk = i, kxi = j jxi = -k, kxj = 4, ixfc = -j Используя эти значения векторных произведений в расши- ренной записи векторного произведения а х b = (tfji + а2] + a3k)x(Z>1i + Ь2] + Ь3Ю получим а х b = (<22*з " а3*2*' + ^3*1 "" Л1*3^ + *а1*2 ~ а2*1^ что может быть записано в виде axb = или в форме, более удобной для запоминания: \а2 аЪ 1*2 Ь3 \ + а3 а\ ь3 ъх J + а\ аг\ axb = а2 аЪ b2 h
3.4. ВЕКТОРНОЕ ПРОИЗВЕДЕНИЕ 57 Рис. 3.4. Параллелограмм с площадью I а * b I Рис. 3.5. Векторное произведение к - а х b Это скорее мнемоническая запись, чем реальный детерминант, поскольку элементами первой строки являются векторы, а не числа. Если через векторы а и b обозначены соседние стороны параллелограмма, как на рис. 3.4, то площадь этого параллелог- рамма равна длине вектора а * Ь. Это непосредственно следует из записи I а х ь I = I a I I b I sin у. На рис. 3.5 векторы а и b лежат в плоскости, проходящей че- рез оси х и у. Предположим, что ось z выходит из листа бумаги к
58 Глава 3. ГЕОМЕТРИЧЕСКИЙ ИНСТРУМЕНТ читателю и соответствует правой координатной системе. Тогда для трехмерного пространства а= [ах а2 0], Ь= [Ьх Ь2 0] а*Ь = аЛ а. к 0 Ъг 0 *1 Ьг Таким образом, вектор а * b будет иметь то же направление, что и вектор к, но только в том случае, если детерминант а\ а2 имеет положительный знак. Это налагает условие, что вектор а, поворачиваемый по направлению к вектору b на угол меньше 180°, вращается в положительном направлении (против часовой стрелки) тогда, и только тогда, когда D > 0. Этот способ будем ниже использовать для определения, обходятся ли вершины тре- угольника А, В, С в направлении против часовой стрелки при их перечислении именно в этом порядке. На рис. 3.6. имеем u=[Wj w2] = AB, v-fvj v2]-AC Рис. 3.6. Точки А, В, С обходятся против часовой стрелки
3.5. ДЕКОМПОЗИЦИЯ ПОЛИГОНОВ НА ТРЕУГОЛЬНИКИ 59 £> = «с *с 1 Х\ Уа ' ^В^А УВ-УА ° *c-*A Ус~Уа ° *в"*А ^А *C~*A *СГ*А _ И1 "2 vl V2I Вершины А, В, С, именно в этом порядке, обходятся в направле- нии против часовой стрелки, если, и только если, вектор и пово- рачивается в сторону направления вектора v на угол у < 180° про- тив часовой стрелки. Это означает, что направление обхода то- чек А, В, С может быть установлено на основе анализа детерми- нанта |*А*А Ц 1*в>в <с *с следующим образом: D > 0 — точки А, В, С обходятся против часовой стрелки; D < 0 — точки А, В, С обходятся по часовой стрелке; D = 0 — точки А, В, С лежат на одной прямой. 3.5. ДЕКОМПОЗИЦИЯ ПОЛИГОНОВ НА ТРЕУГОЛЬНИКИ В главах 4 и 5 будут формироваться изображения трехмерных объектов, границы поверхностей которых могут быть полигона- ми. Это не очень серьезное ограничение, поскольку кривые по верхности могут аппроксимироваться большим числом поли- гонов, точно так же, как линии аппроксимируются последова- тельностью отрезков прямых линий. Обработка произвольных, полигонов может привести к очень сложным ситуациям, особен- но если нужно различать видимые и невидимые части отрезков прямых. На рис. 3.7 показан пример такой ситуации. Если внутренние углы при всех вершинах полигона меньше 180°, то такой полигон называется выпуклым. На рис. 3.8(6) внутренний угол при вершине Р больше 180°. Такую вершину будем называть невыпуклой. Все другие вершины на рис. 3.8 — выпуклые. Если полигон имеет хотя бы одну невыпуклую вер- шину, то весь такой полигон будем называть невыпуклым. Если А и В — две точки на границе выпуклого полигона, то и весь отрезок АВ будет принадлежать полигону. Для невыпуклых
60 Глава 3. ГЕОМЕТРИЧЕСКИЙ ИНСТРУМЕНТ Рис. 3.7. Два полигона частично перекрывают друг друга (а) (б) Рис. 3.8. (а) — выпуклый полигон; (б) — невыпуклый полигон полигонов это условие может не соблюдаться. Невыпуклость полигонов является источником сложностей, это же касается и переменного числа вершин полигонов. По этой причине уделим большое внимание треугольникам. Вполне очевидно, что тре- угольники всегда имеют фиксированное число вершин и они обязательно выпуклые. Особый интерес к ним выявляется в свя- зи с рассмотрением произвольных полигонов, поскольку любой полигон может быть разбит на конечное число треугольников. Это и будет предметом рассмотрения данного параграфа. Операция разбивки выпуклого полигона на треугольники чрезвычайно проста, как это видно из рис. 3.9. Если вершины полигона пронумеровать последовательно Р0, Рр ..., Рп_р а за- тем вычертить диагонали Р0Р2> ^VV "' ^о^л-2' то этого будет
3.5. ДЕКОМПОЗИЦИЯ ПОЛИГОНОВ НА ТРЕУГОЛЬНИКИ 61 Pfi P3 Р ' р Ъ / t ч (а) (б) Рис. 5.9. (а) — диагонали внутри полигона; (б) — диагональ PQP3 использовать нельзя достаточно. В невыпуклом полигоне, как на рис. 3.9(6), этот про- стой способ работать не будет, поскольку некоторые из диагона- лей PqP2» ^0^3' •••» ^0^п-2 М0ГУТ выходить за пределы полигона. Составим теперь программу, которая будет считывать коор- динаты вершин полигона и выполнит разбиение полигона на тре- угольники. Требуется, чтобы вершины были указаны обяза- тельно в порядке обхода против часовой стрелки. Например, у полигона на рис. 3.9(6) верной окажется последовательность Р4Р5Р6Р0Р1Р2Р3, а последовательность Р6Р5Р4Р3Р2Р1Р0 будет непригодной. Для полигона с п вершинами сначала указывается количество вершин л, затем последовательно перечисляются п пар координат всех вершин в порядке обхода полигона против часовой стрелки. В результате будет получен чертеж полигона с вычерченными диагоналями, полностью разбивающими весь полигон на треугольники. Перед вычерчиванием диагоналей не- обходимо удостовериться, что все диагонали лежат полностью внутри полигона. Предположим, что Р._р Р., Р.+1 обозначают три соседние вер- шины, причем будем считать, что Р_{ = P j и Рп = Р0, чтобы можно было рассматривать случаи, когда i - 0 и / = п - 1. Напом- ним, что вершины должны быть перечислены в порядке обхода против часовой стрелки. В этом случае Р^. будет выпуклой верши- ной тогда, и только тогда, когда три вершины Р^, P., Pf.+1 имен- но в этом порядке будут обходиться в направлении против часо- Для этой цели используем способ, хорошо работающий для широкого класса полигонов. Однако могут существовать и такие полигоны, для которых этот способ дает ошибку. См., например, упражнение 3.3 в конце этой главы.
62 Глава 3. ГЕОМЕТРИЧЕСКИЙ ИНСТРУМЕНТ Рис. 3.10. Диагональ Р Р вне полигона вой стрелки. В качестве контрпримера рассмотрим рис. 3.10, в котором обход тройки Р^Рз выполняется по часовой стрелке. Вершина Р2 является невыпуклой, и диагональ PjP3 лежит вне полигона. Таким образом, диагональ Р^Р^ может быть только кандидатом для наших целей, если три вершины P._j (x(_v yhl), Р/Ц, yt), Р/+1Ц+р З^-i)» именно в этом порядке, обходятся в на- правлении против часовой стрелки, то есть если D = \хы xi 1 **1 Ум yt Ум 1 1 1 >0 Это условие является необходимым, но, к сожалению, недо- статочным, как это показано на рис. 3.11. Здесь точки Р0, Рр Р2 обходятся именно в таком порядке, в направлении против часо- / -7*- Рис. 3.11. Диагональ PQP^ частично находится вне полигона
3.5. ДЕКОМПОЗИЦИЯ ПОЛИГОНОВ НА ТРЕУГОЛЬНИКИ 63 вой стрелки, но отрезок Р0Р2 нельзя использовать для деления полигона на треугольники. Такой ситуации можно избежать, если принимать во внимание также длину диагоналей. Будем выбирать наикратчайшую диагональ Р|._1Р^2» которую может иметь выпуклая вершина Р/ между точками Phl и Р/+1. Эта диа- гональ используется для отсечения треугольника Р^Р/Р^+р За- тем таким же образом проверяется оставшийся полигон Р0, Рр ..., Рм, Р/+1,..., P^_j и так далее. Технически это реализуется введением целочислен- ного массива v0,..., vnt_v содержащего номера вершин оставше- гося полигона. Вначале задаем т-п и v.=i (H), 1, ..., л-1). Каж- дый раз при отсечении треугольника число т уменьшается на единицу. Если подобная программа предназначается для прак- тического применения, то желательно выполнить некоторое чис- ло испытаний на допустимость входных данных. Программа, ко- торая надежно отвергает любой непригодный набор данных, мо- жет быть названа устойчивой. В нашей ситуации необходимо провести испытание, действительно ли заданный набор коорди- нат точек вообще описывает полигон. Например, совершенно непригодна последовательность 4 112 2 2 112 поскольку обход точек в заданном порядке приведет к ситуации, показанной на рис. 3.12, но в качестве полигона такую фигуру принять нельзя. Рис. 3.12. Результат недопустимой последовательности
64 Глава 3. ГЕОМЕТРИЧЕСКИЙ ИНСТРУМЕНТ Из других проверок следует обратить внимание на: - максимальное количество точек п, например, п < 500; - минимальное и максимальное значения координат; - ориентацию обхода точек в направлении против часовой стрелки. Несмотря на их очевидную важность, большинство проверок здесь опущено и они оставлены для упражнений читателя. С дру- гой стороны, в программу включены некоторые специальные средства, которые, вообще говоря, можно опустить. Это относит- ся к представлению диагоналей в виде штриховых линий вместо сплошных. Пусть все штрихи должны иметь одинаковую длину. Штриховая линия не должна начинаться или кончаться пробе- лом, в начале и в конце штрихи должны быть полной длины, как это показано для отрезка PQ на рис. 3.13. Р . Q Рис. 3.13. Штриховая линия Читателю предлагается самостоятельно решить эту проблему и сравнить свое решение с функцией dash из следующей про- граммы: /* POLY_TRIA: Разбиение полигона на треугольники */ /* Dividing a polygon into triangles */ #define NMAX 500 #defineBIG 1.0e30 #include "math.h" int n, v[NMAX]; float x[NMAX]. y[NMAX]; main() { int i, h, j, m, I, imin; double diag, mindiag; printf("%s\n%s\n", /* "Give n, followed by n coordinate pairs (x, y) of the vertices. ", "in counter-clockwise order"); */ "Задайте число вершин n и затем п пар координат вершин (х, у)", "в порядке обхода против часовой стрелки"); scanf(M%d'\ &n); if(n>-NMAX) { printf /* "n too large" */ ("число n слишком велико"); exit(1); } for(H); i<n; i++){ scanf("%f %f", &x[i]. &y[i]); v[i}-i; } initgr(); draw_polygon(); m-n; while (m>3) { min_diag-BIG;
3.5. ДЕКОМПОЗИЦИЯ ПОЛИГОНОВ НА ТРЕУГОЛЬНИКИ 65 for (i-0; Km; i++) { h- (i- -0 ? m-1 : M); j- (i- -m-1 ? 0 : i+1); if (counter_clock(h, i, j, &diag) && diag<min_diag) { min_diag-diag; imin-J; } } i-lmin; h- (i- -0 ? m-1 : i-1); J- (i- -m-1 ? 0 : i+1); if (mindiag- -BIG) errorgr /* "wrong sense of rotation" */ ("неправильное направление обхода"); dash(x[v[h]],y[v[hD,x[vD]],y[va]]); m--; for(H;Km;l++)v[l]-v[l+1]; } endgr(); } error_gr(str) char *str; { endgrQ; prlntf("%s\n", str); exit(1); } int counter_clock(h, i, j, pdist) int h, i, j; double *pdlst; { double xh-x[v[h]], xHx[v[i]], xJ-x[vD]]. yh-y[v[h]], yi-ytv[i]], yj-y[v[j]], x_hl, y_hl, x_hj, yhj, Determ; x_hl-xl-xh; y_hi-yi-yh; x_hj-xj-xh; y_bj-yj-yh; * pdist - x_hj * x_hj + y_hj * y_hj; Determ - xhi * y_hj - x_hj * y_hi; return Determ>1e-6; } draw_polygon() { int i; move(x[n-1], y[n-1J; for (i-0; i<n; i++) draw(x[i], y[i]); } dash(xl, y1, x2, y2) float x1, y1, x2, y2; { Inti, к; float xdif-x2-x1. ydif-y2-y1, pitchO-0.3, dx, dy; к-2 * (int)ceil(sqrt(xdif*xdif+ydif*ydif)/pitchO)+ 1; dx-xdif/k; dy-ydif/k; for(l-0;i<k;i+-2) { move(x1+i*dx, y1+i*dy); draw(x1-Ki+1)*dx, y1+(i+1)*dy); } Следующая входная последовательность 12 116164444353 522223333414 Л-271
66 Глава 3. ГЕОМЕТРИЧЕСКИЙ ИНСТРУМЕНТ К 7 s / Ч / ч • ч / V J / / / 1 > II - ч /\ ч / ч / V «^ "■"" \ \ \ 4 Рис. 3.14. Результат работы программы POUJTRIA приведет к выводу изображения, показанного на рис. 3.14. 3.6. ОДНОРОДНЫЕ КООРДИНАТЫ В этом параграфе рассматривается математически интерес- ная задача, связанная с построением перспективной проекции, и поэтому ее часто включают в книги по трехмерной машинной графике. Здесь будут обсуждаться только геометрические свой- ства. Хотя рассуждения сравнительно просты, но они довольны длинные^ и носят теоретический характер. Цель заключается в более четком понимании обозначений типа [ху 1 ] и [xyz 1 ]. Чи- татели, интересующиеся практическими аспектами графическо- го программирования, могут вполне пропустить этот параграф без серьезных последствий для понимания остальных частей книги. К изучению этого параграфа можно вернуться на более поздней стадии. В параграфе 2.3 уже использовалась запись [х у 1 ] для обоз- начения матрицы, состоящей только из одной строки, иногда на- зываемой вектором-строкой. Такая запись может также рас- сматриваться как частный случай записи [ху w ], где числа л, у, w называются однородными координатами. Эти три числа одно- родных координат применяются для обозначения точки в двух- мерном пространстве. В проективной геометрии однородные ко- ординаты применялись задолго до возникновения и распростра- нения машинной графики. В главе 4 будем рассматривать перспективные преобразования. Фактически они являются цен- тральным проецированием, исследуемым в различных книгах по проективной геометрии. Здесь обсудим лишь вкратце несколько задач из этой увлекательной, но достаточно трудной для понима-
3.6. ОДНОРОДНЫЕ КООРДИНАТЫ 67 ния ветви математики, избегая формальных определений и стро- гих доказательств теории. На рис. 3.15 имеем ось х и ось w, так что точка задается парой координат (jc, w). Любая точка P(jc, у), не лежащая на оси х, имеет свою центральную проекцию Р'(ХУ 1), определяемую как точка пересечения прямой линии ОР с прямой линией /, описываемой уравнением w» 1. Точка начала координат является центром проекции. Отрезок прямой линии РО может рассматриваться как луч света из объекта Р в глаз, расположенный в точке О. Для точек Q(0, н>) и Q' (0, 1) получим два подобных треугольника OPQ и OP'Q', так что у- X _ P'Q' _ PQ _ х 1 OQ' OQ w Все точки Ос, w) со свойством х = wX лежат на линии ОР и имеют одну и ту же проекцию Р\ Если будем интересоваться только проекциями на прямую линию /, а не фактическими зна- чениями х и и>, то имеет значение только их отношение. Вполне естественно использовать только одну координату X вместо пары (X, 1), если учитывать только точки на прямой линии /. Если же все-таки требуется использовать координатную пару, то подой- дет любая пара чисел Ос,и>), удовлетворяющая условию x/w = X, если принять такое соглашение. В геометрическом смысле коор- динатная пара (wX> w) любой точки Р, отличной от О, на прямой Рис. 3.15. Двухмерная центральная проекция
68 Глава 3. ГЕОМЕТРИЧЕСКИЙ ИНСТРУМЕНТ линии ОР' может служить обозначением точки Р'. Это и реали- зуется в случае применения однородных координат. В общем случае любая точка (Х{, Xv ..., Хп) в n-мерном про- странстве записывается как точка (wX{, wX2,..., wXny w) в (n+D- мерном пространстве, где w — любое ненулевое вещественное число. Эта группа из я+1 чисел определяет однородные коорди- наты исходной точки в /г-мерном пространстве. Читатель, воз- можно, знаком с определением проекции, которая является ото- бражением множества точек в (п+1) -мерном пространстве в одну в я-мерном пространстве. Однородные координаты возникли из- за необходимости обратного преобразования из n-мерного про- странства в (я+1)-мерное пространство. Точка (5,7) в двухмер- ном пространстве, например, может быть записана в однородных координатах как (15, 21, 3) или как (500, 700, 100) и так далее. Хотя все операции относятся к двухмерному пространству, эти тройки можно принимать за точки в трехмерном пространстве. Очевидно, что обозначение (Х> У) представляет собой обычную запись точки в двухмерном пространстве для точки (X, У, 1) в трехмерном пространстве. Точка Р' является центральной про- екцией для любой точки РОс, у у w), если x/w = X и уЫ- У. Как и ранее, точка начала координат О будет центром проекций, а все точки проецируются на плоскость w = 1. Для определения термина однородные (координаты) вос- пользуемся уравнением аХ + йУ+с = 0 (3.6) описывающим прямую линию в двухмерном пространстве. Заме- няя X и У на х/ w и у/ w, получаем a(x/w) + b(y/w) +c = 0 или ax + by + cw = 0 (3.7) Уравнение (3.7) обычно принято называть однородным, по- скольку оно имеет одинаковую структуру в терминах ах, by, cw. Отсюда числа х, у, w закономерно называть однородными коор- динатами точки (X, У). Если опять принять, что двухмерное про- странство располагается в плоскости w= 1 в координатной систе- ме xyw, то уравнение (3.7) описывает плоскость, проходящую через начало координат и заданную прямую линию.
3.6. ОДНОРОДНЫЕ КООРДИНАТЫ 69 Если считать, что запись Ос, у, w) используется как иная фор- ма записи для Ос/и>, y/w), то необходимо будет потребовать, что- бы значение w не было равно нулю. Однако при этом однородные координаты едва ли имели какие-либо преимущества перед обычными координатами и читатель мог бы вообще выразить со- мнение относительно наличия каких-то преимуществ. Напри- мер, рассмотрим систему из двух линейных уравнений, каждое из которых описывает прямую линию в двухмерном пространст- ве: bX + ^y+Cj-O (38) \a2X + b2Y+c2 = 0 (ЛЛ) Если две прямые линии параллельны, то они не пересекаются и не существует пары чисел (X, У), удовлетворяющей системе (3.8). Таким образом, для нахождения общей точки для прямых линий необходимо применять правило с некоторым исключени- ем, которое не совсем элегантно. При замене координат X и У на однородные координаты х, у> w ситуация несколько улучшится <alX + biy + clW = 0 [ а2х + b2y + cjw = О Уравнения из системы (3.9) можно интерпретировать как плоскости, проходящие через точку начала координат О. Эта си- стема имеет, по крайней мере, одно тривиальное решение х = у= w = 0. Для геометрической интерпретации зададим для коэффи- циентов конкретные значения. Пусть, например, заменим систе- му (3.8) на систему Г2Х+ЗУ- 6=0 [4Х + 6У-24 = 0 описывающую две параллельные прямые линии, изображенные на рис. 3.16. Тогда систему уравнений (3.9) можно заменить на 2х + 3у- 6н> = 0 (ЗЛО.а) 4* + 6;y-24w = 0 (3.10.6) Эта система эквивалентна системе Г2х + 3>> = 0 1 н> = 0 так что решение состоит из всех троек (3£, -2к, 0), где к — любое вещественное число. В трехмерном пространстве эти точки обра- зуют прямую линию, проходящую через точки О и (3, -2, 0),
70 Глава 3. ГЕОМЕТРИЧЕСКИЙ ИНСТРУМЕНТ причем эта прямая линия представляет собой линию пересече- ния двух плоскостей, заданных уравнениями (ЗЛО). Возвраща- ясь к двухмерному пространству плоскости и>в 1, напомним, что каждая точка (X, У) ассоциируется с прямой линией (wXy wY, w) в трехмерном пространстве. Для ненулевых значений н^эта ассо- циация почти тривиальна. Теперь станет понятной очень важная причина применения однородных координат. Для каждой пря- мой линии в двухмерном пространстве добавим один объект, на- зываемый бесконечно удаленной точкой. Эта бесконечно уда- ленная точка не может быть обозначена в обычной системе коор- динат, а в системе однородных координат — может. Например, бесконечно удаленная точка на прямой линии, описываемой уравнением (ЗЛО.а), записывается как (3, -2, 0) или в виде лю- бой тройки (3£, -2k, 0) для ненулевого к. Поскольку эти тройки являются решением системы уравнений (3.10), то бесконечно удаленную точку можно считать точкой пересечения двух па- раллельных линий, изображенных на рис. 3.16. Считается, что бесконечно удаленная точка находится в двухмерном простран- стве. Как мы видели, каждая точка в двухмерном пространстве ассоциируется с прямой линией в трехмерном пространстве, поэ- тому желательно выяснить, с какой прямой линией ассоциирует- ся бесконечно удаленная точка (3, -2, 0). Поскольку эта линия должна проходить через точку начала координат О(0, 0, 0), то искомая линия должна быть линией, проходящей через точку О и точку (3, -2,0), то есть лежащей в плоскости и>=0. Вполне резонно назвать точку (3,-2, 0) бесконечно удален- ной точкой, поскольку ее можно рассматривать как предельную точку (3, -2, w) при w, стремящемся к нулю, а эта тройка в одно- родных координатах эквивалентна точке (3/и>, -21 w, 1), которая при малых значениях w удалена очень далеко. Введение беско- нечно удаленной точки позволяет утверждать, что любые две различные прямые пересекаются в одной точке. Аналогично в проективной геометрии можно утверждать, что две любые раз- личные плоскости имеют линию пересечения. Если плоскости параллельны, то все точки этой линии пересечения в однородных координатах записываются в виде 0с, у, z, 0). Этот вопрос не бу- дем рассматривать детально, а лучше вернемся к двухмерному пространству и покажем другие новые проявления возможнос- тей, предоставляемых однородными координатами.
3.6. ОДНОРОДНЫЕ КООРДИНАТЫ 71 В обычных, не однородных, координатах линейное преобра- зование в двухмерном пространстве может быть записано в виде [X' Г]= [X Y]A Поскольку [ 1 0 ] А = [а{ а2 ] и [0 1 ] А = [bx b2 ], то строки матрицы Л отображают соответственно точки [1 0]и [0 1 ]. Вне зависимости от способа определения матрицы А начало координат О не изменяется, так что [0 0 ] А - [0 0 ], поэтому этим способом нельзя выразить операцию переноса. Однако в одно- родных координатах точка в двухмерном пространстве задается тройкой (дс, у, z) и преобразование записывается в виде [х' у' w']= [x у w]A а\ а2 аЪ Ь\ Ь2 ЬЪ Мы имеем С2 C3J [1 0 0]Л=[а1 а7 а,] Рис. 3.16. Параллельные прямые линии
72 Глава 3. ГЕОМЕТРИЧЕСКИЙ ИНСТРУМЕНТ Так как точка [100] — бесконечно удаленная точка на оси х, то первая строка [ах а2 аъ ] матрицы А представляет собой отобра- жение этой бесконечно удаленной точки на оси х. Аналогично вторая строка матрицы [Ь{ Ъ2 Ь^ ] будет отображением бесконеч- но удаленной точки на оси у. Поскольку [0 0 l]A-[c{ c2 с3] то можно считать, что третья строка матрицы [Cj с2 с3 ] является отображением точки начала координат [001]. Это означает, что однородные координаты позволяют выразить любые преобразо- вания путем матричного умножения. Фактически в этом нет ни- чего нового и это свойство уже использовалось в параграфе 2.3. Однако операция переноса — не единственное новое свойство, предоставляемое таким матричным умножением. С его помощью можно преобразовать параллельные прямые линии в пересекаю- щиеся. Покажем это на примере преобразования полного прямо- угольного квадранта в треугольник. На рис. 3.17 изображен произвольный треугольник, вершины которого заданы в прямоугольной системе координат точками A(tfp a2)y B(ij, Ь2) и С(Ср с2). Добавим третью координату, рав- ную 1, как формальное средство образования однородных коор- динат. Исключением являются бесконечно удаленные точки (1, 0, 0) и (0, 1, 0) на координатных осях, у которых третья однород- ная координата равна нулю. Образуем матрицу М, такую, что матричное умножение [х' у' z']-lxy z]M отобразит точку О на точку С, точку (1, 0, 0) — на точку А и точку (0, 1,0) — на точку В. Для любых ненулевых значений а,/5, у точки А, В и С будут точками требуемого изображения треугольника, если М = аах аа2 а (3.11) \рьх рь2р ус1 ус2 у Это легко проверить, поскольку имеем очевидное соотношение [1 0 0]М= [аах аа2 а] правая часть которого является просто другим обозначением точки А на рис. 3.17. На первый взгляд кажется, что константы a,/J, у в уравнении (3.11) не нужны и могут быть установлены
3.6. ОДНОРОДНЫЕ КООРДИНАТЫ 73 B(bt, ь2,1) (1.0,0) О{0,0,1) Р(1,0,1) СЦс2,1) Р' А(ага2,1) Рис. 3.17. Квадрант, преобразованный в треугольник равными 1. Однако они нужны для того, чтобы можно было за- дать заранее установленное отображение U' для так называемой единичной точки U. Точка U' может быть выбрана в любом месте внутри треугольника. Поскольку точка U(l, 1, 1) отображается наточку U'(Ир uv ы3), то имеем [1 1 \\М-\их u2 1] — сокращенное обозначение следующей системы трех линейных уравнений относительно переменных a, /J, у: а2а + Ъф + с2у = и2 а+ /3+ у=1 Система имеет единственное решение, которое следует из то- го факта, что тройки (ар а2, я3), (Лр Ъ2, Ь^ и (ср с2, с3) обозна- чают вершины треугольника. Решение этой системы для а,Дуи подстановка результата в уравнение (3.11) дает искомое значе- ние матрицы А/. Прямые линии преобразуются в прямые линии, но их парал- лелизм не сохраняется. Например, вертикальная линия, прохо- дящая через точку U на рис. 3.17, превращается в прямую ли- нию, проходящую через точки В и U'. Это дает геометрическое средство для нахождения проекции Р' для точки Р. Имея вычис- ленное значение матрицы М, точку проекции Р' можно найти аналитически как произведение [101JM
74 Глава 3. ГЕОМЕТРИЧЕСКИЙ ИНСТРУМЕНТ Заметим, что все бесконечно удаленные точки отображаются на отрезок АВ, а все бесконечно удаленные точки параллельных линий — на единственную точку на отрезке АВ. Это дает основа- ние рассматривать весь квадрант как плоскую перспективу, в ко- торой треугольник ABC является картиной, а отрезок АВ пред- ставляет собой линию горизонта. На этом закончим анализ однородных координат. Другие ин- тересные свойства однородных координат читатель может найти в учебниках по проективной геометрии, например в книге Hop- kins and Hails (1953). 3.7. ПЕРЕНОС И ПОВОРОТЫ В ТРЕХМЕРНОМ ПРОСТРАНСТВЕ Если каждая точка РОс, у, z) отображается на точку Р'(*'> У у z)B соответствии с уравнениями \х' = х + ах у'=у + а2 z' = z + a$ где £j, я2> а\ — константы, то этот процесс называется переносом в трехмерном пространстве. Такой перенос может быть записан в матричной форме [*' у' z' 1]-[jc у z \]T Г = О 1 О До (3.12) 41 u2 u3 Читатель, ознакомившийся с параграфом 3.6, может легко распознать, что первая, вторая и третья строки матрицы Т соот- ветствуют отображениям бесконечно удаленных точек на коор- динатных осях, а четвертая строка — отображению точки [0 0 0 1 ]. Последнее означает, что в однородных координатах точка [aj а2 Д3 1 ] является отображением точки начала коорди- нат О. Поворот вокруг координатных осей может быть описан матрицей без использования однородных координат. Ради крат- кости так и будем поступать, обращаясь к однородным координа- там только в тех случаях, когда они действительно необходимы. Будем использовать правую координатную систему, считая вра- щение вокруг оси положительным, если оно соответствует поло-
j. 7. ПЕРЕНОС И ПОВОРОТЫ В ТРЕХМЕРНОМ ПРОСТРАНСТВЕ 75 cb Рис. 3.18. Вращение в положшпельном направлении вокруг координатных осей жительному направлению этой оси по правилу винта с правой резьбой. Это показано на рис. 3.18. Рассмотрим поворот вокруг оси z на угол а и для сокращения обозначим cos a » с и sin a = s. Тогда можно записать [х1 у' z']=U у z]R Г с \-s [о s 0] с 0 0 lj что непосредственно следует из уравнений (2.4) в параграфе 2.3. Я Матрицу Rz можно использовать для получения матриц Rx и , определяющих поворот вокруг соответствующих осей, чисто формальным образом, то есть без применения картинки. Это де- лается путем циклических перестановок, получаемых заменой каждой из букв х, у, z на последующую, считая, что за буквой z следует буква х. Матрица Rz превратится в матрицу Rx циклическим перено- сом каждой строки на одну позицию и затем выполнением анало- гичной операции для столбцов: Г с s 0" ks с 0 [О 0 1 "\ s -^ /^ " 0 0 11 с s 0 -s с 0J [1 0 L0 0 с S 01 S с\ -л
76 Глава 3. ГЕОМЕТРИЧЕСКИЙ ИНСТРУМЕНТ Так же матрица Rx преобразуется в "последующую" матрицу R-. *х = Г1 0 01 0 с s _0-s с \^ s ^ /^ "0-jcl 1 0 0 0 с s\ \с 0 Is 0 1 0 -si о! с\ V -Л. Суммируя сказанное, получим следующие матрицы: 1 0 0 1 10 cos a sin а (3.13) |0 -sin а cos а J "cos а 0 -sin а] 0 10 (3.14) _ sin а 0 cos а J "cos а sin а 0] |-sina cos а 0 (3.15) 0 0 1J Для поворота точки вокруг оси х на угол а матрица Rx ис- пользуется следующим образом: [х' у' z']-[xyz]Rx Матрицы R и Rz применяются аналогично. Как объяснялось в параграфе 2.1, уравнения для преобразо- ваний могут интерпретироваться как изменения координат. Пе- ренос точки на определенное расстояние вправо описывается те- ми же уравнениями, как перенос системы координат на такое же расстояние влево. На практике удобнее перемещать координат- ную систему вместо точки, но для этого требуется инвертирова- ние матрицы. К счастью, инверсия матриц Г, /?х, R , Rz (уравне- ния (3.12)—(3.15)) может быть записана немедленно: Г"1- Г1 0 0 0] 0 10 0 0 0 10 Га1 ~а2 ~а3 l J (3.16)
J. 7. ПЕРЕНОС И ПОВОРОТЫ В ТРЕХМЕРНОМ ПРОСТРАНСТВЕ 11 V- л.Г' = 10 0 0 cos a -sin а 0 sin а cos a cos а 0 sin а 0 1 О -sin а 0 cos a (3.17) (3.18) V- cos а -sin а 0 1 sin а cos а 0 (3.19) О 0 1 J Теперь можно найти матрицу R для поворота вокруг любой прямой линии, проходящей через точку начала координат О. Для определенности будем полагать, что поворот осуществляет- ся вокруг вектора v, начало которого расположено в точке О. Тогда положительное направление вращения соответствует на- правлению вектора по правилу винта с правой резьбой. Как и ра- нее, поворот будем производить на угол а. Если концевая точка вектора v задана в ортогональных коор- динатах, то сначала вычислим его сферические координаты р,ву<р (см. рис. 3.19) р = ivl = Vv^+ v2+vj Еслир = 0, то будем считать, что в = f = 0. В противном случае arctan (v2hx) 0 = к + arctan (v2/vj) я/2 Ъл/г если Vj > 0 если Vj < 0 если Vj = 0 и v2 ^ 0 если Vj = 0 и v2 < 0 <р = arccos (v3/p) Читателю, возможно, известно обратное вычисление Vj =p sin <p cos в, v2 =p sin <p sin в, v3 =p cos <p Теперь стратегия заключается в таком изменении системы координат, чтобы вектор v (ось вращения) совпадал с новым на- правлением положительной полуоси z. Начнем с поворота осей х и у вокруг оси z на угол в. В соответствии с уравнением (3.19) [х' у' z']= [x у z]R -1 V- cos a -sin а 0 sin а cos а 0 0 0 1
78 Глава 3. ГЕОМЕТРИЧЕСКИЙ ИНСТРУМЕНТ Ось х' имеет положительное направление вектора (vp v2, 0). Теперь повернем оси x'hz' вокруг оси у' на угол <р до совпадения оси z" с вектором v (см. рис. 3.19). Обращаясь к уравнению (3.18), это условие запишем как 1х" у" z"]=[x' у' г']Я -1 costp 0 sin a 0 1 0 -sin а 0 cos а Фактический поворот вокруг вектора v на угол а теперь мож- но выполнить как поворот вокруг оси z". Из уравнения (3.15) получим 1х'" У" z"']=[x" у" z"]Rw Г cos a sin а 0] Л e hsina cos a 0 [ 0 0 lj К этому моменту достигнуто выполнение соотношения [*'" у'" z'"h[xyz]R-lR-{Ry Рис. 3.19. Сферические координаты
3.7. ПЕРЕНОС И ПОВОРОТЫ В ТРЕХМЕРНОМ ПРОСТРАНСТВЕ 79 К сожалению, координаты х"\ у"', z'" относятся к самой по- следней системе координат, тогда как их необходимо выразить в исходной системе. Обозначим эти координаты в исходной систе- ме через дс*, у*, z*. Переход к исходной системе инвертирован- ных матриц R~x и R " (которые будут совпадать с матрицами Rz и R ) в обратном порядке для преобразования точки х"\ у'", /": [х* у* z*]=[x'" у'" z'"]/y*z Это означает, что полный поворот вокруг вектора v на угол а вы- числяется по следующей формуле: где [х* у* z*]= [x"' у'" z'"]Rz lRy lRyRyR У z R -1. *v = " cos в -sin 0 0 sin© cos0 0 0 0 1 cos^> 0 sin<p 0 1 0 -sin <p 0 cos <p J "cos a sin a 0 -sin a cos a 0 0 0 1 cos <p 0 -sin <p 0 i 0 [ sin<p 0 cos^> J cos б sin© 0" -sin0 cos0 0 0 0 1_ Для последующего применения запишем V R *,"Ч Ч*А 41 42 43 r21 r22 r23 .r31 r32 r33. (3.20) До сих пор обсуждалось решение задачи о повороте относи- тельно вектора, привязанного к точке начала системы координат О. Теперь нужно устранить это последнее ограничение и поста- вить задачу определения поворота относительно вектора, начало которого расположено в любой произвольной точке А(а^, а2, д3).
80 Глава 3. ГЕОМЕТРИЧЕСКИЙ ИНСТРУМЕНТ Для этого будем использовать вектор v для вычисления матрицы R в уравнении (3.20) таким же образом, как и ранее. Затем нуж- но выполнить три следующих шага: 1. Обращаясь к уравнению (3.16), выполним перенос из задан- ной точки в точку начала координат О, используя однород- ные координаты и следующую матрицу: г-' = Г 1 0 0 Г*! 0 0 0] 1 0 0 0 1 0 -*2-аз li 2. Теперь можем осуществить поворот относительно оси, про- ходящей через О, как и ранее, но матрицу R из уравнения (3.20) необходимо расширить тривиальным образом, чтобы можно было использовать однородные координаты Л* = ГП Г12 Г13 U r21 r22 r23 ° г31 Г32 r33 ° 0 0 0 1J 3. Применить преобразование, обратное шагу 1, используя матрицу Т = Г 1 0 0 0" 0 10 0 0 0 10 Lal a2 а3 1. 1 \ После этого матрица обобщенного поворота вычисляется как Rc ,en't~1r*t и ее можно использовать следующим образом: [**У* z* 1]=U У z 1]*GEN УПРАЖНЕНИЯ 3.1. Исследуйте эффект выбора а =(} = у = 1 в уравнении (3.8) параграфа 3.6 и напишите программу для отображения ряда точек квадранта в заданный треугольник. Получите треу- гольник и изображения точек (1, 1), (1,2), (1,3),...; (2, 1), (2, 2), (2, 3),... и так далее в графической форме.
УПРАЖНЕНИЯ 81 3.2. Напишите программу для обобщенного поворота. Входными данными для программы будут: 1) координаты двух точек, определяющие вектор v = АВ с началом в точке А; 2) угол а. Поворот будет выполняться вокруг вектора v на угол а; 3) массив точек для поворота. Результатом работы программы должны быть координаты повернутых точек. Каждая точка описывается тройкой коор- динат в ортогональной системе. Поскольку пока еще не были рассмотрены средства для графического вывода трехмерных объектов, результаты работы программы должны быть выве- дены в числовой форме. 3.3. Найдите полигон, который не может быть правильно обра- ботан программой POLY_TRIA, и разработайте более общий алгоритм. Подсказка: Если треугольник отсекает часть полигона, то ни одна из вершин этого полигона не должна попадать внутрь треугольника.
Глава 4 ПЕРСПЕКТИВНЫЕ ИЗОБРАЖЕНИЯ 4.1. ВВЕДЕНИЕ На рис. 4.1 двухмерное изображение куба представлено вме- сте с некоторыми дополнительными линиями. На этом изобра- жении отрезки АВ и AD не параллельны нижнему или верхнему краю листа бумаги, так что можно было бы утверждать, что они не горизонтальны. Однако они обозначают горизонтальные реб- ра куба ABCDEFGH в трехмерном пространстве, поэтому в принципе их можно называть горизонтальными. По этой же при- чине можно утверждать, что два отрезка АВ и DC параллельны, неявно предполагая, что они находятся в трехмерном простран- стве. По этой терминологии параллельные горизонтальные ли- нии встречаются в так называемой точке схода. Все точки схода лежат на одной прямой линии, которая называется линией гори- зонта, -Заметим, что линия горизонта и точка схода являются особенностью изображения и реально не существуют в трехмер- ном пространстве. В течение многих веков эта концепция ис- пользовалась художниками для получения реалистичных изо- бражений трехмерных объектов. Такие изображения обычно на- зываются перспективными. Изобретение фотографии предложило новый (и более лег- кий) способ формирования перспективных изображений. Суще- А Рис. 4.I. Точки схода на горизонте
4.1. ВВЕДЕНИЕ 83 ствует строгая аналогия между камерой, применяемой в фото- графии, и человеческим глазом. Наш глаз представляет собой очень сложный инструмент, а фотокамера является его простей- шей имитацией. Но в последующем обсуждении слово глаз впол- не может быть заменено словом камера, если хотим подчерк- нуть, что желательно получить двухмерную твердую копию. Очевидно, что картинка будет зависеть от положения глаза. Особо важное значение имеет расстояние между глазом и объек- том, поскольку "эффект перспективы" будет обратно пропорци- онален этому расстоянию. Если глаз расположен очень близко от объекта, то получим сильный эффект перспективы, как на рис. 4.2(a). Здесь можно четко видеть, что продолжения изображений параллельных линий на картинке пересекаются. С другой сторо- ны, если глаз расположен далеко от объекта (по сравнению с раз- мером объекта), то параллельные линии объекта будут казаться параллельными и на картинке. Это показано на рис. 4.2(6). (а) (б) Рис 4.2. Расположение глаза: (а) - близко к объекту; (б) - далеко от объекта Кроме классического и фотографического способов существу- ет способ получения перспективных изображений на основе ана- литической геометрии. Читатель уже знаком с представлениями точек в двухмерном и трехмерном пространствах их координата- ми (Ху У) и Ос, >>, z) соответственно. При необходимости получе- ния перспективной проекции задается большое количество точек PU, у, z), принадлежащих объекту, для которых предстоит вы- числить координаты точек изображения Р'(Х, У) на картинке. Для этого нужно только преобразовать координаты точки Р из так называемых мировых координат Ос, y> z) в экранные коорди- наты (X, Y) ее центральной проекции Р'. Будем предполагать, что экран расположен между объектом и глазом Е. Для каждой точки Р объекта прямая линия РЕ пересекает экран в точке Р'.
84 Глава 4. ПЕРСПЕКТИВНЫЕ ИЗОБРАЖЕНИЯ Это отображение удобно выполнять в два этапа. Первый этап бу- дем называть видовым преобразованием - точка Р остается на своем месте, но система мировых координат переходит в систему видовых координат. Второй этап называется перспективным преобразованием. Это точное преобразование точки Р в точку Р\ объединенное с переходом из системы трехмерных видовых координат в систему двухмерных экранных координат: Мировые координаты Uw, yw, zw) Видовое преобразование Видовые координаты (дсе, уе, ze) Перспективное преобразование Экранные координаты (X, Y) 4.2. ВИДОВОЕ ПРЕОБРАЗОВАНИЕ Для выполнения видовых преобразований должны быть зада- ны точка наблюдения, совпадающая с глазом, и объект. Жела- тельно, чтобы система мировых координат была правой. Будет удобно, если начало ее координат располагается где-то вблизи центра объекта, поскольку объект наблюдается в направлении от Е к О. Предположим, что это условие выполняется. На практике это означает, что, возможно, потребуется некоторое преобразо- вание координат, заключающееся в вычитании из исходных зна- чений координат положения центральной точки объекта. Это очень простое преобразование будет включено в программу, но без записи в математической форме. Пусть точка наблюдения Е будет задана в сферических координатахру 0, <р по отношению к мировым координатам. То есть мировые координаты могут быть вычислены по формулам: хЕ=р sin <p cos в уЕ=р siny? sin# (4.1) zE=p cos <p Обозначения сферических координат схематически изображены на рис. 4.3. Говорят, что вектор направления ЕО (равный -ОЕ) определяет направление наблюдения. Из точки наблюдения Е можно видеть точки объекта только внутри некоторого конуса, ось которого совпадает с линией ЕО, а вершина — с точкой Е. Ее ли заданы ортогональные координаты хЕ, уЕ, zE точки Е, то
4.2. ВИДОВОЕ ПРЕОБРАЗОВАНИЕ 85 Рис. 4.3. Сферические координаты точки наблюдения Е Рис. 4.4. Система видовых координат
86 Глава 4. ПЕРСПЕКТИВНЫЕ ИЗОБРАЖЕНИЯ можно вычислить ее сферические координаты по методике, из- ложенной в параграфе 3.7. Нашей конечной задачей будет вычисление экранных коор- динат X, У, для которых оси X и Y лежат в плоскости экрана, рас- положенной между точками Е и О и перпендикулярной направ- лению наблюдения ЕО. Начало системы видовых координат рас- полагается в точке наблюдения Е (рис. 4.4). При направлении взгляда из Е в О положительная полуось хе направлена вправо, а положительная полуось уе - вверх. Такое направление осей по- зволит нам впоследствии определить экранные оси в тех же на- правлениях. Направление оси ze выбирается таким образом, что значения координат увеличиваются по мере удаления от точки наблюдения. Такие определения осей логичны и удобны, но их взаимное соответствие таково, что система видовых координат является левосторонней. Хотя такой вывод может показаться не- суразным, но это совершенно обычная ситуация в машинной гра- фике и не вызовет появления каких-либо проблем. (Заметим, что система мировых координат всегда выбирается как правосто- ронняя.) Видовое/преобразование может быть записано в форме К yeze l]=[*w yw zw 1]K (4.2) где К- матрица видового преобразования размерами 4x4. Для нахождения матрицы V предположим, что преобразования ото- бражения могут быть составлены из четырех элементарных пре- образований, для которых легко написать свои матрицы преоб- разований. Матрица V получается путем перемножения этих че- тырех матриц. Фактически каждое из четырех преобразований изменяет координаты и, следовательно, определяется матрицей, обратной матрице, соответствующей преобразованию точки. Перенос начала из О в Е Выполним такой перенос системы координат, при котором точка Е становится новым началом координат. Матрица для та- кого изменения координат выглядит так: Г 1 0 0 01 L хе Уе ze J
4.2. ВИДОВОЕ ПРЕОБРАЗОВАНИЕ 87 Рис. 4.5. Новые оси после переноса Новая система координат показана на рис. 4.5. Поворот координатной системы вокруг оси z Обращаясь к рис. 4.5, повернем систему координат вокруг оси z на угол л/2 - 0 в отрицательном направлении. В результате ось у совпадет по направлению с горизонтальной составляющей от- резка ОЕ, а ось х будет расположена перпендикулярно отрезку ОЕ. Матрица для такого изменения координат будет совпадать с матрицей для поворота точки на такой же угол в положительном направлении. Матрица 3x3 для этого поворота равна ' cosOr/2-0) sin<Ji:/2-0) (Г -sin(tt/2-0) costo/2-0) 0 0 0 1 sin0 cos0 0 "I |-cos0 sin0 0 (4.4) 0 0 lj Новое положение осей показано на рис. 4.6. Поворот системы координат вокруг оси х Поскольку новая ось z должна совпадать по направлению с отрезком ЕО, повернем систему координат вокруг оси х на угол
88 Глава 4. ПЕРСПЕКТИВНЫЕ ИЗОБРАЖЕНИЯ Рис. 4.6. Новые оси после поворота вокруг оси z ж—<р в положительном направлении, что соответствует повороту точки на угол ~-(л-(р) = ср-л. Из уравнения (3.13), получаем матрицу: '10 0 1 0 cos (<p -л) sin (<p -л) [0 -ът(<р-л) соъ(<р-л)\ ГО 0 ] 0 -cos6 -sin0 (4.5) 0 sin0 -COS0J Новые оси показаны на рис. 4.7. *х = Изменение направления оси х На рис. 4.7 оси у и z имеют правильную ориентацию, а ось х должна быть направлена в противоположную сторону. Поэтому необходима матрица для выполнения преобразования х' = -jc, то есть "-1 0 0] 0 10 (4.6) 0 0 1J После этого завершающего преобразования получим систему ви- довых координат, уже показанную на рис. 4.4. "yz =
4.2. ВИДОВОЕ ПРЕОБРАЗОВАНИЕ 89 Рис. 4.7. Новые оси после поворота вокруг оси х Вычислим матрицу отображения V как матричное произведе- ние К= TR*R*My* (4.7) где обозначение R* использовано для матрицы 4x4, полученной путем расширения матрицы Л, имеющей размерность 3x3, до- бавлением четвертой строки и четвертого столбца, содержащих числа 0, 0, 0, 1 именно в этом порядке. Матричное произведение не коммутативно (то есть в общем случае АВ * В А), но ассоциа- тивно, поэтому уравнение (4.7) можно переписать в виде V-T(RzRxMy/ Таким образом можно работать с матрицами 3 х 3 до тех пор, пока это возможно. Дальнейшая проработка задачи умножения матриц связана с использованием обозначений cos^ = a cos в - с /Л 0ч sin <p = b sin 0 = а откуда следует, что а2+ Ь2 = 1 и с2+ d2 = 1. На основе уравнения (4.1), перепишем уравнение (4.3) в виде Г 1 0 0 01 т I 0 1 0 0 0 0 10 \_-pbc -pbd -pa 1 J
90 Глава 4. ПЕРСПЕКТИВНЫЕ ИЗОБРАЖЕНИЯ а уравнения (4.4) и (4.5) как Следовательно, *г = *х = Г d \-с 1 ° Г1 0 Lo RzRx = d -с 0 с 0" J 0 0 1 0 01 -а -Ь\ Ь -а\ -ас -be -ad -bd b - a Умножив эту матрицу справа на матрицу Л/ из уравнения (4.6), получим \-d -ас -be с -ad -bd 0 Ъ -а Затем искомую матрицу отображения V найдем как произве- дение двух матриц W^ откуда где 71- Г • 1 0 0 0 10 0 0 1 l~pbc -pbd -pa Wy/' V = V41 V42 -d -ac с -ad 0 b 0 0 -d -ac -be с -ad -bd 0 b -a v41 v42 v43 =pbca *"pab< =p{al =p(al = 0 l-pbcd = i :2+pabd2- >(c2 + d2)- b-ab) 0 0 0 1 -be -bd -a 0 01 0 0 lj > pal ab) : 0" 0 0 1 t
4.3. ПЕРСПЕКТИВНЫЕ ПРЕОБРАЗОВАНИЯ 91 =p{b2(c2 + d2)+a2} =р{Ь2 + а2} ЖР Таким образом, мы нашли P-sin0 -cosy>cos# -sin <p cos в 0] v cos в -cos <p sin в -sin у sin 0 0 I . 0 0 sinp -cosy> 0 [ 0 0 p lj Здесь мы получили важный результат. Если были заданы сферические координаты/э, 0, <р для точки наблюдения Е, то по- ложение точки в системе видовых координат можно вычислить по значениям ее мировых координат, используя только уравне- ния (4.2) и (4.9). После только что выполненного преобразования отображе- ния необходимо определить перспективные преобразования, и это будет сделано в следующем параграфе. Но уже сейчас можно использовать видовые координаты хе и уе, просто игнорируя ко- ординату ze. В этом случае будет получена так называемая орто- гональная проекция. Каждая точка Р объекта проецируется в точку Р' проведением прямой линии из точки Р перпендикуляр- но плоскости, определяемой осями х и у. Эту проекцию можно также считать перспективной картинкой, которая была бы пол- учена при удалении точки наблюдения в бесконечность. Приме- ром такой картинки может служить изображение куба на рис. 4.2(6). Параллельные линии остаются параллельными и на картинке, полученной при ортогональном проецировании. На практике такие проекции применяются очень часто. С другой стороны, внесение некоторого эффекта перспективы делает картинку более реалистичной. Поэтому после преобразо- вания отображения необходимо выполнить перспективные пре- образования, которые, к удивлению, потребуют очень незначи- тельных дополнительных вычислений. 4.3. ПЕРСПЕКТИВНЫЕ ПРЕОБРАЗОВАНИЯ Читатель может вообразить, что мы находимся всего на пол- пути и что в этом параграфе будет не меньше математических выкладок, чем в параграфе 4.2. Однако большая часть работы
92 Глава 4. ПЕРСПЕКТИВНЫЕ ИЗОБРАЖЕНИЯ уже выполнена. Здесь мировые координаты уже не будут затра- гиваться. Поэтому видовые координаты будут обозначаться просто Ос, у, z) вместо Ue, уе, ze). На рис. 4.8 была выбрана точка Q, видовые координаты кото- рой равны (0, 0, (1) для некоторого положительного числа d. Плоскость z = d определяет экран, который будем использовать. Таким образом, экран — это плоскость, проходящая через точку Q и перпендикулярная оси z. Экранные координаты определяют- ся привязкой начала к точке Q, а оси X и Y имеют такие же на- правления, как оси х и у соответственно. Для каждой точки объ- екта Р точка изображения Р' определяется как точка пересече- ния прямой линии РЕ и экрана. Чтобы упростить рис. 4.8, будем считать, что точка Р имеет нулевую у-координату. Но все после- дующие уравнения для вычисления ее у-координаты также при- годны и для любых других значений координаты X. На рис. 4.8 треугольники EPR и EP'Q подобны. Следовательно, P'Q_PR EQ. ER Отсюда будем иметь а: d Z (4.10) Рис. 4.8. Экран и видовые координаты
4.3. ПЕРСПЕКТИВНЫЕ ПРЕОБРАЗОВАНИЯ 93 Аналогично можем получить Y=d-2- (4.11) z В параграфе 4.2 было введено предположение, что точка О начала системы мировых координат примерно совпадает с цент- ром объекта. Поскольку ось z видовой системы координат совпа- дает с прямой линией ЕО, которая пересекает экран в точке Q, то начало Q системы экранных координат будет находиться в цент- ре изображения. Если бы мы потребовали, чтобы это начало ко- ординат располагалось в нижнем левом углу экрана, а размеры экрана составляли 2сх по горизонтали и 2с2 по вертикали, то можно заменить уравнения (4.10) и (4.11) на X^d- — +c, (4.12) z 1 Y^d-^+c? (4.13) z 1 Нам еще требуется определить расстояние между точкой на- блюдения Е и экраном. Грубо говоря, мы имеем соотношение: размер картинки _ размер объекта а - р что следует из подобия треугольников ЕР'Q' и EPQ на рис. 4.9. Рис. 4.9. Размеры картинки и объекта
94 Глава 4. ПЕРСПЕКТИВНЫЕ ИЗОБРАЖЕНИЯ Отсюда получим размер картинки (4 И) н размер объекта Это выражение равно применимо для горизонтального и вер- тикального размеров. Его следует интерпретировать скорее как средство для оценки подходящего значения с/, чем точное пред- писание, поскольку трехмерный объект может иметь очень сложную форму и не всегда ясно, какие его размеры следует включать в это уравнение. Можно ввести грубую оценку разме- ров объекта по максимуму его длины, ширины и высоты. Исходя из оценки уравнения (4.14) можно сделать вывод, что размеры картинки должны быть несколько меньше размеров экрана. Бо- лее сложные способы оценки желаемых размеров картинки бу- дут описаны в параграфе 4.5. Остальная часть этого параграфа посвящена более подробно- му анализу общей концепции, особенно относительно точек схо- да и горизонта. На рис. 4.10 изображены точки наблюдения Е и экрана ABCD. Как и прежде, взгляд направлен из точки Е в точ- ку Q. Прямые линии AF, BG, ЕН параллельны, и будем считать их горизонтальными. Предположим, что имеется плоскость, проходящая через параллельные линии ЕН и BG. Эта плоскость пересекает плоскость экрана по прямой линии ВН. Таким обра- зом, каждая точка Р на прямой линии BG будет иметь свою цент- ральную проекцию Р', лежащую на прямой ВН, при условии, что точка Е является центром проекции. Пусть точка Р удаляет- ся от точки В в бесконечность, тогда ее проекция Р' будет при- Рис. 4.10. Точка схода Н
4.3. ПЕРСПЕКТИВНЫЕ ПРЕОБРАЗОВАНИЯ 95 ближаться к точке Н. Это означает, что Н — точка схода для пря- мой линии, проходящей через точки В и G. В терминах проек- тивной геометрии точка Н представляет собой проекцию беско- нечно удаленной точки, лежащей на прямой BG. Как уже упоми- налось в параграфе 3.6, параллельные линии пересекаются в бесконечно удаленной точке, так что точка Н будет также проек- цией бесконечно удаленной точки, лежащей на прямой линии AF. Если возьмем прямую линию с другим направлением, но также горизонтальную, например прямую линию BF, то такая Рис. 4.11. Тонка схода F для вертикальных линий
96 Глава 4. ПЕРСПЕКТИВНЫЕ ИЗОБРАЖЕНИЯ линия также будет иметь точку схода, лежащую на прямой ли- нии CD. Она находится как точка пересечения прямой линии CD и прямой линии, проходящей через точку Е и параллельной этой выбранной горизонтальной прямой линии. Прямая линия CD — это линия горизонта. Каждая точка J на линии горизонта являет- ся точкой схода всех прямых линий, параллельных прямой ли- нии EJ. Точка схода есть не только у горизонтальных линий. На рис. 4.11 проведены вертикальные прямые линии СА и DB. Точкой схода для них будет F. Это та точка, в которой вертикальная пря- мая линия, проходящая через точку Е, пересекает экран. Отрез- ки прямых линий СА и DB имеют проекции СА' и DB', которые не параллельны. Строгий эффект перспективы для вертикальных прямых ли- ний не всегда приемлем; часто предпочитают картинки*, в кото- рых вертикальные линии представляются почти вертикальными. Это происходит потому, что мы больше привыкли к горизонталь- ному, или почти горизонтальному, направлению взгляда. Неко- торые чувствуют даже головокружение, если смотрят далеко вниз! Художники в своих картинах применяют "псевдоперспек- тиву ", когда вертикальные линии рисуются точно вертикальны- ми, даже если направление взгляда не горизонтально. В таком случае полученная картинка отличается от действительно види- мой, но, несмотря на некоторую курьезность, выглядит доста- точно правдоподобной. Примером может служить рис. 4.12(6). В параграфе 4.6 к этому явлению еще вернемся и покажем, что на- ша программа может формировать такие псевдоперспективные ад (а) (б) Рис. 4.12. (а) - перспектива; (б) - псевдоперспектива
4.3. ПЕРСПЕКТИВНЫЕ ПРЕОБРАЗОВАНИЯ 97 пзпп пап пою шел ABCDEFGH1J Рис. 4.13. Десять кубиков, параллельных экрану картинки (см. рис. 4.22(a)). В общем, однако, рекомендуется вы- бирать точку наблюдения не очень близко к объекту, особенно в тех случаях, когда угол <р> показанный на рис. 4.3, примерно ра- вен 90°. В этом заключается практический способ исключения сильного эффекта перспективы для вертикальных линий. Другая несколько дискуссионная тема — представление ли- ний, параллельных экрану. Они будут изображаться параллель- ными линиями на картинке. Рассмотрим, например, рис. 4.13 с изображениями десяти кубиков, расположенными в одну линию. Направление наблюдения — горизонтальное, то есть^> = 90°. На этой картинке можно видеть парадоксальный эффект, относя- щийся к размерам кубиков. Кубик А удален значительно даль- ше, чем кубик Е, но на картинке оба этих кубика имеют одина- ковые размеры. Кажется, что это неправильно, поскольку уда- ленные объекты должны иметь меньшие видимые размеры, чем более близкие. Однако это является результатом центрального проецирования, полученного по точным геометрическим прави- лам, выведенным на основе наших представлений. Изображение на рис. 4.13 практически нереально. Особенность нашего глаза такова, что мы можем видеть только точки, расположенные внутри определенного конуса, ось которого совпадает с направ- лением взгляда ЕО. Очень важный параметр этого конуса — угол атах, отмеченный на рис. 4.14. Глаз, как и камера, допускает только такие значения угла а, которые не превышают некоторого максимального значения атах* ^Ри вычислениях лучше ограничивать не фактическое значение угла а, показанное на рис. 4.14, а примерно выдержи- вать отношение 0.5 • размер объекта откуда опять следует, что выбор слишком малого значения рас- стояния/о, может быть источником затруднений. Если же значе- ние р будет выбрано сравнительно большим, то угол а может 4-271
98 Глава 4. ПЕРСПЕКТИВНЫЕ ИЗОБРАЖЕНИЯ Рис. 4.14. Конус наблюдения оказаться малым, чтобы избежать обсуждаемых затруднений. Если все-таки это не годится, то может быть принято решение о замене плоского экрана частью сферической поверхности с цент- ром в точке Е. Таким образом, можно допустить большие значе- ния угла а, но при этом картинка не получится плоской. Сфери- ческая картинка, в свою очередь, может быть спроецирована на плоскость. Читатели, интересующиеся такими необычными спо- собами проецирования, могут обратиться к работе известного ху- дожника М. Эшера (1972). В данной работе ограничимся только плоскими картинками, которые получаются при сравнительно малых углах а. 4.4. ПРОГРАММА ДЛЯ ВЫЧЕРЧИВАНИЯ КУБА Для многих программистских задач мы должны заранее ре- шить, насколько общими должны быть эти программы. Сущест- вуют две крайние возможности. С одной стороны, можно напи- сать очень специфическую программу, которая не считывает ка- ких-либо входных данных и может выдать только один результат, скажем, некоторую картинку. С другой стороны, можно разработать очень общую программу, например подо- бную программе GENPLOT в параграфе 2.6, которая может вы- чертить любую картинку при условии, что задан файл с исход- ными данными. Между этими двумя крайними случаями суще- ствует множество промежуточных решений для программ, формирующих картинки определенного типа после считывания
4.4. ПРОГРАММА ДЛЯ ВЫЧЕРЧИВАНИЯ КУБА 99 ограниченного количества входных данных, иногда называемых параметрами. В этом параграфе разработаем программу для вычерчивания перспективного изображения куба с длиной ребер, равной 100. Этот совершенно произвольный размер не имеет никаких огра- ничений в отношении картинки, поскольку нижеследующие па- раметры могут выбираться совершенно свободно: 1) три сферические координаты (см. рис. 4.3): р — расстояние до точки наблюдения ЕО, в — угол в горизонтальном направлении от оси х, (р — угол, измеренный по вертикали от оси z; 2) расстояние d между экраном и точкой наблюдения. Точка О начала системы мировых координат выбирается в центре куба, как показано на рис. 4.15. Длина каждого ребра JL. D Рис. 4.15. Куб и система мировых координат
100 Глава 4. ПЕРСПЕКТИВНЫЕ ИЗОБРАЖЕНИЯ обозначается через 2Л, следовательно, А в 50. Тогда восемь вер- шин куба будут иметь следующие координаты: А( А, -А, -Л) В( А, А, -А) С(-Л, Л, -А) D(-A, -А, -А) Е( А, -А, А) F( А, А, А) G(-A, А, А) Н(-А, -Л, А) Будем вычерчивать так называемую проволочную модель, что означает отсутствие различий между видимыми и скрытыми линиями. В данной программе это будет выглядеть так, как если бы перо перемещалось в трехмерном пространстве. Для этой це- ли будем использовать функции mvOc, у, z) и dw(x, у, z), совер- шенно аналогичные функциям moveix, у) и drawix, у) в двухмер- ном пространстве. В функциях mv и dw будет выполняться обра- щение к функции perspective у которая реализует как видовое, так и перспективное преобразование. Ради эффективности все коэффициенты, не зависящие от конкретных точек от А до Н, вычисляются заранее при помощи функции coeff. Ниже приво- дится текст всей программы: /* CUBE: Проволочная модель куба */ /* A wire frame model of a cube */ #lnclude "math.h" float v11, v12, v13, v21, v22, v23, v32, v33, v43, screen_dlst, с 1-4.5, c2-3.5; main() { float rho, theta, phi, h-50.0; prlntf /* "Viewing distance rho - EO" */ ("Расстояние до наблюдателя rho - EO: "); scanf("%r, &rho); prlntf /* "Give two angles.jn degrees." */ ("\n Задайте два угла в градусахЛп"); prlntf /* "Theta, measured horizontally from the x-axls:" */ ("Угол theta измеряется по горизонтали от оси х:"); scanf("%f", &theta); prlntf /* "Phi, measured vertically from the z-axls:" */ ("Угол phi измеряется по вертикали от оси z:"); scanf("%f", &phl);
4.4. ПРОГРАММА ДЛЯ ВЫЧЕРЧИВАНИЯ КУБА 101 printf /* "Distance from viewpoint to screen:" ("Расстояние от точки наблюдения до экрана:' scanf("%f", &screen_dist); coeff(rho. theta, phi); } InitgrO; mv(h, -h, -h); dw(h. h, -h); /* draw AB dw(-h, h, -h); /* draw ВС dw(-h, h, h); /* draw CG dw(-h,-h, h); /*drawGH dw(h,-h, h); /* draw HE dw(h, -h, -h); /* draw EA mv(h, h. -h); dw(h. h, h); /*drawBF dw(-h, h, h); /* draw FG mv(h, h. h); dw(h, -h. h); /* draw FE mv(h, -h, -h); dw(-h, -h, -h); /* draw AD dw(-h, h, -h); • /* draw DC mv(-h, -h, -h); dw(-h, -h, h); /* draw DH endgr(); */ i */ > */ i */ > V t */ t */ t */ t */ t */ i V i */ i f* f* f* f* 1* f* f* f* f* f* 1* f* отрезок АВ отрезок ВС отрезок CG отрезок GH отрезок НЕ отрезок ЕА отрезок BF отрезок FG отрезок FE отрезок AD отрезок DC отрезок DH */ V */ V */ */ V */ */ */ V V /* Углы в радианах */ coeff(rho, theta, phi) float rho, theta, phi; { float th, ph, costh, sinth, cosph, sinph, factor; factor-atan(1.0)/45.0; /* Angles in radians: */ th-theta*factor; ph-phi*factor; costh-cos(th); sinth-sin(th); cosph-cos(ph); sinph-sin(ph); /* Elements of matrix V. see Eq. (4.9): */ /* Элементы матрицы V, см. (4.9): */ v11— sinth; v12-^cosph*costh; v13—sinph*costh; v21-costh; v22-^cosph*sinth; v23—sinph*sinth; v32-sinph; v33^cosph; v43-rho; } mv(x, y, z) float x, y, z; { float X.Y; perspective(x, y, z. &X, &Y); move(X, Y); } dw(x, y, z) float x, y, z; { float X.Y; perspective^, y, z, &X, &Y); draw(X, Y); } perspective(x, y, z, pX, pY) float x, y, z, *pX, *pY; { float xe. ye, ze;
102 Глава 4. ПЕРСПЕКТИВНЫЕ ИЗОБРАЖЕНИЯ /* Eye coordinates, computed as in Eq. (4.2): */ /* Координаты глаза, вычисляемые по (4.2): */ xe-v11*x + v21*y; ye - v12*x + v22*y + v32*z; ze * v13*x + v23*y + v33*z + v43; /* Screen coordinates, computed as In Eqs. (4.12) and (4.13): */ /* Экранные координаты, вычисляемые по (4.12) и (4.13) */ *рХ - screen_dist*xe/ze + с1; *pY - screen_dist*ye/ze + с2; } Программа в результате своей работы выдала изображения кубов, показанные на рис. 4.16, Заметим, что на второй картинке взгляд направлен снизу куба, а на третьей картинке оказалось, что сторойы и диагонали лежат на одной прямой. Было бы гораз- до интереснее, если бы можно было вычертить изображение сплошного тела, а не его проволочную модель. Это задача главы 5, в которой описывается алгоритм, позволяющий определить видимость отрезков прямых линий из заданной точки наблюде- ния. Однако если применить некоторые меры предосторожности при задании точки наблюдения, то можно вычертить изображе- ния не очень сложных объектов с последующим ручным удале- нием невидимых линий. Для изображения на рис. 4Л5 выбрана такая точка наблюдения, что видны только грани ABFE, BCGF, EFGH, a AD, CD, DH должны быть удалены. Это сделано путем удаления из программы CUBE трех программных строк, предше- ствующих обращению к функции endgri ). Таким же образом модифицированная версия программы CUBE была фактически использована для получения рис. 4.2(a), где были заданы значе- ния/о «200,9 = 30, у>» 70, d= 3 и рис. 4.2(6) при значениях р = 200000, в * 30, <р = 70, d - 3000. Рис. 4.16. Кубы, видимые из различных точек
4.5. ВЫЧЕРЧИВАНИЕ ПРОВОЛОЧНЫХ МОДЕЛЕЙ 103 4.5. ВЫЧЕРЧИВАНИЕ ПРОВОЛОЧНЫХ МОДЕЛЕЙ Рассмотрим программу, которая может вычертить перспек- тивное изображение любой проволочной модели, образованной из конечного числа отрезков прямых линий. Опуская определен- ные отрезки прямых линий, можно выполнить картинки для простых сплошных тел, что уже было показано для куба в пара- графе 4.4. Как и ранее, будем использовать точку наблюдения Е и "объектную точку" О, которые определяют направление на- блюдения. Конечно, для пользователя не всегда удобно, чтобы точка О совпадала с началом системы мировых координат. Поэ- тому будем требовать только, чтобы пользовательская ось z рас- полагалась вертикально. Пользователь должен задать координа- ты jc0, yQ, z0 объектной точки О, выраженные в своей системе координат. После считывания пользовательских координат они преобразуются в систему "внутренних мировых координат" *, у, z с точкой О как начало по следующему правилу: х = пользовательская ^-координата - х0 у = пользовательская у-координата - у0 z = пользовательская z-координата - z0 Затем пользователь должен определить сферические коорди- наты/?, 0, <р точки наблюдения Е относительно нового начала ко- ординат О, как на рис. 4.3. Также должно быть задано расстояние между точкой наблюдения и экраном. Тем самым определяется положение экрана, поскольку он перпендикулярен направлению наблюдения ЕО. Для определения отрезков прямых линий нам опять придется представить перемещение в трехмерном про- странстве, которое ассоциируется с соответствующим перемеще- нием пера на картинке. Для каждого перемещения задаются ко- ды 0 = перо поднято 1 = перо опущено которые также уже применялись ранее в файле A.SCRATCH в параграфе 2.6. Для вычерчивания двух примыкающих друг к другу отрезков PQ и QR необходимо задать три входные строки следующей структуры: *р ^р ZP 0 (перенос пера в точку Р) хО yQ 20 * (вычерчивание PQ) *R yR *r 1 (вычерчивание QR)
104 Глава 4. ПЕРСПЕКТИВНЫЕ ИЗОБРАЖЕНИЯ Правая часть каждой строки является комментарием, ее наличие во входном файле не влияет на работу программы. Перед представлением программы рассмотрим пример пол- ного набора входных данных. Обратимся к рис. 4.10, который со- всем не тривиален. Если вычерчивать этот рисунок вручную, то возникает проблема определения положения точки М на прямой линии KL, такой, чтобы в трехмерном пространстве отрезок ЕМ был перпендикулярен плоскости BCDA. Выберем точку К в каче- стве начала пользовательской системы координат. Пусть поло- жительная полуось у в этой системе проходит через точку L, а положительная полуось х - через точку В. Вид с положительной оси х на плоскость yKz показан на рис. 4.17. В трехмерном про- странстве точка К находится в середине отрезка АВ, длина кото- рого равна 10. Теперь можно записать полный набор входных данных для картинки, которую собираемся начертить: (объектная точка О) (ро, тета, фи, d) (перенос в точку В) (вычерчивание отрезка ВС) (вычерчивание отрезка CD) (вычерчивание отрезка DA) 0 30 5 5 -5 -5 2 -15 0 2 2 0 2 75 0 4 4 0 15 0 1 1 1 г\ . 5 К 2 н 3 -* *- м 4 У Рис. 4.17. Вид с положительной полуоси х
4.5. ВЫЧЕРЧИВАНИЕ ПРОВОЛОЧНЫХ МОДЕЛЕЙ 105 5 5 -5 -5 0 0 0 0 0 0 0 9 9 0 -5 2 0 9 -5 3 0 0 0 0 4 4 0 0 4 0 1 1 1 1 0 1 1 1 0 1 (вычерчивание отрезка АВ) (вычерчивание отрезка BG) (вычерчивание отрезка GF) (вычерчивание отрезка FA) (перенос в точку Е) (вычерчивание отрезка ЕН) (вычерчивание отрезка НК) (вычерчивание отрезка KL) (перенос в точку Е) (вычерчивание отрезка ЕМ) Для параметра d было найдено значение 15 путем подстанов- ки в формулу (4.14) значений р = 30, размер _картинки = 7, размер_ объекта =14. Хотя до сих пор программа еще не пред- ставлена, покажем на рис. 4.18 результат ее работы на основании призеденных выше входных данных. Программа будет считывать данные из файла, имя которого задается в качестве аргумента программы. При имени програм- мы GPERS и имени файла PLANES это означает, что для запу- ска программы на выполнение необходимо набрать на клавиату- ре строку GPERS PLANES вместо ввода только имени GPERS в качестве команды для нача- ла выполнения программы. Программа с аргументами содержит строку mainiargc, argv) intargc; char*argv[ ]; в начале главной функции main. Имя программы, в данном слу- чае это GPERS, также рассматривается как аргумент. Параметр Рис. 4.18. Пример работы программы GPERS
106 Глава 4. ПЕРСПЕКТИВНЫЕ ИЗОБРАЖЕНИЯ argc принимает значение, равное числу аргументов (в данном примере argc - 2), а вектор аргументов argv содержит указатели на аргументы. В данном конкретном примере имеем argv[0] = "GPERS" argv[\] = " PLANES" Видовые и перспективные преобразования выполняются так же, как в программе CUBE в параграфе 4.4: /* GPERS: General program for PERSpective */ /* Общая программа для построения перспективных изображений */ #include "math.h" #include "stdio.h" float v11, v12,. v13, v21. v22, v23. v32, v33, v43. screendist, с 1-4.5, c2-3.5; main(argc, argv) int argc; char *argv[ ]; { float rho, theta. phi. xO, yO, zO, x, y, z; Int code; FILE *fp; If (argc !- 2) { prlntf /* "No file name given" */ ("He задано имя файла"); exit(1); } If (fp-fopen(argv[1],"r"), fp- -NULL) { prlntf /* "File .... does not exist" */ ("Файл с именем %s не существует", argv[1]); exlt(1); } fscanf(fp, "%f %f %f", &xO. &yO, &zO); sklpf(fp); fscanf(fp, "%f %f %f %f", &rho, &theta, &phl, &screen_dlst); coeff(rho, theta, phi); inltgr(); while (sklpf(fp), fscanf(fp, "%f %f %f %d", &x. &y. &z, &code) > 0) If (code) dw(x-xO, y-yO, z-zO); else mv(x-xO, y~yO, z-zO); endgr(); sklpf(fp) FILE *fp; { while (getc(fp)!- An');} coeff(rho, theta, phi) float rho, theta, phi; { float th, ph, costh, slnth, cosph, slnph, factor; factor-atan(1.0)/45.0; /* Angles In radians: */ /* Углы в радианах */ th-theta*factor; ph-phl*factor; costh-cos(th); si nth-si n(th); cosph-cos(ph); sinph-sln(ph); /* Elements of matrix V, see Eq. (4.9): */ /* Элементы матрицыЛЛ см. (4.9): */ v11-~sinth; v12—cosph*costh; v13—sinph*costh;
4.5. ВЫЧЕРЧИВАНИЕ ПРОВОЛОЧНЫХ МОДЕЛЕЙ 107 v21-costh; v22—cosph*slnth; v23—slnph*slnth; v32-slnph; v33—cosph; v43-rho; } mv(x, y, z) float x, y, z; { float X,Y; perspective^, y, z, &X, &Y); move(X, Y); } dw(x, y, z) float x, y, z; { float X,Y; perspective^, y, z, &X, &Y); draw(X, Y); } perspective^, y, z, pX, pY) float x, y, z, *pX, *pY; { float xe, ye, ze; /* Eye coordinates, computed as In Eq. (4.2): */ /* Видовые координаты, вычисляемые по (4.2): */ xe-v11*x + v21*y; ye - v12*x + v22*y + v32*z; ze - v 13*x + v23*y + v33*z + v43; /* Screen coordinates, computed as In Eqs. (4.12) and (4.13): */ /* Экранные координаты, вычисляемые по (4.12) и (4.13) */ *pX - screen_dlst*xe/ze + d; *pY - screen_dlst*ye/ze + c2; } Функция fscanf принимает неположительное значение, когда попытка чтения данных терпит неудачу, то есть когда достигает- ся конец файла. Поэтому цикл чтения продолжается до тех пор, пока функция fscanf возвращает положительное значение. Пользователь программы GPERS должен задать расстояние до экрана. По нескольким причинам такое решение неудовлет- ворительно: 1. Пользователю важен размер картинки, а не расстояние до экрана. 2. Параметр " размер ^объекта" в формуле (4.14) имеет очень неопределенный смысл, поэтому трудно вычислить точное значение для параметра d. 3. Иногда желательно определить прямоугольную область вы- вода, занимающую лишь часть экрана, в которую картинка должна быть вписана целиком. В этом случае было бы очень
108 Глава 4. ПЕРСПЕКТИВНЫЕ ИЗОБРАЖЕНИЯ неудобно преобразовывать размеры области вывода в рас- стояние до экрана. Есть несколько способов улучшения программ в этих направ- лениях, а именно: 1. Отсечение трехмерного объекта по пирамиде, вершина ко- торой совпадает с точкой наблюдения Е, а основанием явля- ется определенное окно, заданное в мировых координатах. Описание такого способа, который здесь применяться не бу- дет, можно найти в книге Ньюмена и Спрула (1979). 2. Отсечение картинки в двухмерном пространстве по задан- ной области вывода. Этот способ также не будем применять. 3. Автоматический подбор размера и позиции таким образом, чтобы картинка целиком входила в заданную область выво- да. Этот способ мы уже применяли для двухмерных объек- тов в параграфе 2.6, где программа GENPLOT действовала в качестве постпроцессора. Эту же программу можно исполь- зовать и в данном случае. Более того, мы сможем воспользо- ваться последними тринадцатью строками программы СURVGEN из параграфа 2.6 для вывода данных для черче- ния в файл A.SCRATCH. Будем применять третий способ, используя два файла и две программы, как показано на рис. 4.19. В программе GPERSF значение d не вводится, а просто зада- ется d = 1. Это значение практически не нужно, поскольку раз- меры картинки, вычисляемые программой GENPLOT, зависят только от заданных размеров области вывода. Напомним, что программа GENPLOT запрашивает у пользователя границы об- ласти вывода. На этом этапе читатель может высказать предпо- ложение: а нельзя ли избавиться от необходимости задавать объ- ектную точку О, поскольку точка О описывалась как централь- ная точка объекта и кажется, что положение такой точки может быть легко вычислено автоматически. Однако есть две причины, по которым следует оставить точку О как точку, определяемую пользователем. Эти причины обсудим ниже в параграфе 4.6. Следующая программа GPERSF воспринимает тот же самый входной файл, как и программа GPERS, но расстояние до экрана в конце второй строки исходных данных игнорируется и может быть опущено.
4.5. ВЫЧЕРЧИВАНИЕ ПРОВОЛОЧНЫХ МОДЕЛЕЙ 109 / Входной файл W Программа GPERSF Jr / Файл / A.SCRATCH V Программа GENPLOT 7 / Графический выход Рис. 4.19. Блок-схема для программ GPERSF и GENPLOT /* GPERSF: General program for PERSpective, producing the /* output File A.SCRATCH, to be read by GENPLOT /* Общая программа для построения перспективных изображений, /* формирующая выходной файл A.SCRATCH, предназначенный /* для считывания программой GENPLOT #include "math.h" #lnclude "stdio.h" float v11, v12, v13, v21, v22, v23, v32, v33, v43; maln(argc, argv) Int argc; char *argv[ ]; { float rho, theta, phi, xO, yO, zO, x, y, z; FILE *fpin. *fpout; struct { float X, Y; Int code; } s; If (argc J- 2) { prlntf /* "No Input file given" */ ("He указан входной файл"); exlt(1); } if (fpin-fopen(argv[1],"r"), fpin- -NULL) { printf /* "File ... does not exist" */ ("Файл с именем %s не существует", argv[1]); exit(1); }
по Глава 4. ПЕРСПЕКТИВНЫЕ ИЗОБРАЖЕНИЯ fscanf(fpin, "%f %f %Г, &хО, &уО, &zO); skipf(fpin); fscanf(fpin, "%f %f %f, &rho, &theta, &phl); coeff(rho, theta, phi); fpout-fopen("a.scratch", "wb"); while (sklpf(fpln), fscanf(fpln, "%f %f %f %d". &x, &y, &z, &s.code)>0) { perspect(x-xO, y-yO, z-zO, &s.X, &s.Y); fwrlte(&s, slzeof s, 1, fpout); } fclose(fpout); } sklpf(fpln) FILE *fpln; { while (getc(fpln)!- An'); } coeff(rho, theta, phi) float rho, theta, phi; { float th, ph, costh, slnth, cosph, slnph, factor; factor-atan(1.0)/45.0; /* Angles In radians: */ /* Углы б радианах */ th-theta*factor; ph-phi*factor; costh-cos(th); slnth-sln(th); cosph-cos(ph); slnph-sln(ph); /* Elements of matrix V, see Eq. (4.9): */ /* Элементы матрицы V, см. (4.9): */ v11— slnth; v12-~- cosph*costh; v 13—slnph *costh; v21-costh; v22—cosph*slnth; v23—slnph*slnth; v32-slnph; v33—cosph; v43-rho; } perspect(x, y, z, pX, pY) float x, y, z, *pX, *pY; { float xe, ye, ze; /* Eye coordinates, computed as In Eq. (4.2) */ /* Видовые координаты, вычисляемые по (4.2) */ xe-v11*x + v21*y; ye - v12*x + v22*y + v32*z; ze - v13*x + v23*y + v33*z + v43; /* Screen coordinates, tp be adjusted by GENPLOT */ . /* Экранные координаты, пересчитываемые в программе GENPLOT */ *рХ - xe/ze; *pY - ye/ze; } Функция perspect в этой программе была получена на осно.ве функции perspective из предыдущих программ CUBE и GPERS путем подстановки screenjiiste 1 и с\ - с2 » 0. (При выборе дру- гих значений содержимое файла A.SCRATCH будет иным, но картинка, сформированная программой GENPLOT, окажется точно такой же!)
4.6. НАПРАВЛЕНИЕ НАБЛЮДЕНИЯ Ш 4.6. НАПРАВЛЕНИЕ НАБЛЮДЕНИЯ, БЕСКОНЕЧНОСТЬ, ВЕРТИКАЛЬНЫЕ ЛИНИИ Используем программу GPERSF для обсуждения некоторых новых интересных аспектов. Предположим, что объект очень длинный в направлении оси х, как, например, балка на рис. 4.20, размеры которой 200 * 2 * 2. Интересным аспектом этого приме- ра является задание точки О, которую мы должны указать для определения направления наблюдения ЕО. До сих пор точка О выбиралась в центре объекта. Однако фактически нужна такая точка О, изображение которой будет располагаться в центре картинки. Часто это не существенно, но иногда очень сильно влияет на изображение. Например, на рис. 4.21 .точка О' оказы- вается по средине отрезка Q'U', хотя соответствующая ей точка О выбрана не на середине исходного отрезка QU. Если бы для нее была задана середина отрезка QU, то направление наблюдения получилось бы совершенно иным. Вернемся к балке, изображенной на рис. 4.20. Очевидно, что точку О следует выбирать не в середине балки, а значительно ближе к глазу. Это возможно осуществить потому, что в нашей программе объектная точка задается пользователем, а не вычис- ляется как центр объекта. Реальные бесконечные линии при изо- бражении ландшафта на картинке получаются в виде отрезков Рис. 4.20. Длинная балка
112 Глава 4. ПЕРСПЕКТИВНЫЕ ИЗОБРАЖЕНИЯ Рис. 4.21. Объектная точка не в центре объекта прямых конечной длины. Для бесконечных объектов нет никако- го смысла говорить о центре, хотя направление наблюдения ЕО существует. Поэтому и требуется определить положение "цент- ральной" точки О объекта. Балка на рис. 4.20, конечно, не явля- ется ландшафтом в точном смысле, но ее длина предполагает су- ществование бесконечности; если это не совсем ясно, то для оп- ределения ее длины можно задать число значительно больше 200. Для данной балки выберем точку 0(-15, 1, 1). За исключе- нием букв Р, Q,..., вся эта картинка была вычерчена с помощью программ GPERSF и GENPLOT. Пересечение балки с плоско- стью х = г 15 обозначено буквами ABC, а пересечение с плоско- стью дс=-100— буквами А'В'С. Следовательно, точка О лежит в плоскости ABC, а плоскость А'В'С расположена по центру балки. Полный набор входных данных для получения рис. 4.20 определяется списком: (объектная точка О) (ро, тета, фи) (перемещение в точку Q) (вычерчивание отрезка QR) (вычерчивание отрезка RS) (вычерчивание отрезка SP) (вычерчивание отрезка PQ) (вычерчивание отрезка QU) (вычерчивание отрезка UV) (вычерчивание отрезка VW) -15 30 0 0 0 0 0 -200 -200 -200 1 20 2 2 0 0 2 2 2 0 1 70 0 0 2 1 2 1 0 1 0 1 0 1 2 1 2 1
4.6. НАПРАВЛЕНИЕ НАБЛЮДЕНИЯ 113 0 0 -200 -15 -15 -15 -100 -100 -100 0 2 2 2 2 0 2 2 0 2 2 2 0 2 2 0 2 2 1 0 1 0 1 1 0 1 1 (вычерчивание отрезка WS) " (перемещение в точку R) (вычерчивание отрезка R V) (перемещение в точку А) (вычерчивание отрезка АВ) (вычерчивание отрезка ВС) (перемещение в точку А') (вычерчивание отрезка А'В') (вычерчивание отрезка В'О Другой аспект, требующий специального рассмотрения, — это представление вертикальных линий, кратко упомянутое в параграфе 4.3. На рис. 4.11 мы видели, что спроецированные вертикальные линии встречаются в точке схода F. Мы также сравнивали представления кубиков на рис. 4.12. Если точка О расположена в центре кубика, то наша программа не сможет сформировать изображение рис. 4.12(6). Но точку О можно по- местить над кубиком так, чтобы направление наблюдения ЕО было горизонтальным. Тогда экран будет занимать вертикальное положение, поскольку он перпендикулярен направлению на- блюдения. Это означает, что вертикальные ребра кубика будут параллельны экрану. Следовательно, их проекции на картинке также будут точно вертикальными. Курьезным здесь является то, что, хотя линия наблюдения горизонтальная, мы будем ви- деть верхнюю грань кубика как бы сверху. Получаемая картинка выглядит совершенно натурально, по- ка мы не преувеличим и не выберем точку наблюдения слишком высоко. На рис. 4.22 показаны оба таких случая. Линии на рис. 4.22(a) были вычерчены программами GPERSFu GENPLOT на основе следующего списка входных дан- ных: (точка О) (ро, тета, фи) (перемещение в точку Р) (вычерчивание отрезка PQ) (вычерчивание отрезка QU) (вычерчивание отрезка UT) (вычерчивание отрезка TR) (вычерчивание отрезка PR) (вычерчивание отрезка RW) 0 5 0 30 2 90 0
114 Глава 4. ПЕРСПЕКТИВНЫЕ ИЗОБРАЖЕНИЯ 1 1 -1 -1 2 1 0 0 0 0 1 -1 -1 1 0 0 2 1 0 0 1 1 1 1 0 0 0 0 2 1 1 0 1 1 0 1 0 1 0 1 (вычерчивание отрезка WT) (перемещение в точку W) (вычерчивание отрезка WV) (вычерчивание отрезка VU) (осьх) (ось у) (OCbZ) Рис. 4.22(6) получен путем выбора точки О значительно вы- ше, а именно — с координатами (0,0,6) вместо (0,0, 2). Это уже совсем нереалистичное изображение кубика. В то же время рис. 4.22(a) вполне приемлем. Некоторые даже предпочитают его по сравнению с кубиками на рис. 4.2(a) и рис. 4.12(a), где верти- кальные линии имеют точку схода. Если кубик вычерчивается в перспективе вручную, то обычно вертикальные линии остаются вертикальными, как на рис. 4.22(a). Другие представления вы- полнить вручную уже значительно труднее. С помощью компь- ютера их выполнить одинаково просто и пользователь может вы- брать такие, какие ему нравятся. (а) (б) Рис. 4.22. (а) — объектная тонка несколько выше кубика; (б) — объектная тонка очень высоко над кубиком
УПРАЖНЕНИЯ 115 Замечание Программы в этой главе предназначены скорее для объясне- ния принципов, чем для практического применения. В главах 5 и 6 будут обсуждены более практичные программы. УПРАЖНЕНИЯ В последующих упражнениях невидимые ребра не должны вычерчиваться, для этого можно применить способ, описанный в конце параграфа 4.4. 4.1. Напишите программу для получения чертежа ступенек, по- добных изображенным на рис. 4.23. Количество ступенек (п) должно быть переменным (здесь п в 3). 4.2. Напишите программу для вычерчивания пирамиды, образо- ванной из кубиков, подобной изображенной на рис. 4.24. Рис. 4.23. Ступеньки Рис. 4.24. Пирамида
116 Глава 4. ПЕРСПЕКТИВНЫЕ ИЗОБРАЖЕНИЯ Рис. 4.25. Кубик с вырезом Число л, определяющее высоту пирамиды в единицах куби- ков, должно считываться программой (здесь п ш 3). 4.3. Напишите программу для вычерчивания объекта, подобного показанному на рис. 4.25. Это кубик, из которого вырезано несколько меньших кубиков; их размеры составляют \1п -ю часть исходного кубика. Число п должно быть переменным (здесь п = 3).
Глава 5 УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ 5.1. АНАЛИЗ ЭФФЕКТИВНОСТИ Не всегда простые программы лучше сложных. В некоторых задачах простые алгоритмы работают слишком медленно и тре- буются более мощные алгоритмы, иногда реализуемые в виде чрезвычайно сложных программ. Примером может служить за- дача сортировки. Программы, описанные в главе 4, для формиро- вания перспективных чертежей были сравнительно простыми и быстрыми. Очевидно, что сложность таких программ будет зна- чительно возрастать, если потребуется автоматически удалять невидимые части отрезков. Наряду со сложностью самой задачи неизбежно возникают другие проблемы, когда программа долж- на работать как можно быстрее. Кроме обеспечения эффектив- ности необходимо удовлетворить требования общности, устой- чивости к ошибкам и удобства для пользователей, что в конеч- ном счете приводит к потенциальному усложнению программы. Этим четырем аспектам качества уделено столько внимания, что читатель может подумать, что теперь мы будем рассматривать только очень сложные программы. На самом деле более привле- кательно начать с анализа несколько упрощенной программы, чем с быстро работающей, но непонятной. Поэтому начнем с программы, обладающей сложностью порядка п . Это означает, что время вычислений будет примерно пропорционально п , где п — число отрезков прямых линий или полигонов, подлежащих вычерчиванию. Программа будет работать достаточно быстро при вычерчивании простых объектов, содержащих несколько со- тен граней. Данные, описывающие объект, будут храниться в массивах фиксированных размеров. Это также ограничит слож- ность объектов. Кроме этих ограничений, в остальном программа будет достаточно общей: в принципе могут быть вычерчены лю-
118 Глава 5. УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ бые конечные объекты, имеющие только конечное число пло- ских граней. Что же касается устойчивости к ошибкам и удобства пользователей, то не будем требовать, чтобы все ошибки при их появлении сопровождались четкими сообщениями и чтобы объ- екты определялись наиболее удобным способом. Позднее эта программа будет усовершенствована, особенно в отношении эф- фективности. Проблема удаления невидимых линий реализует- ся алгоритмами с чрезвычайно большими затратами вычисли- тельного времени и она всегда была хорошей задачей для про- граммирующих математиков. 5.2. ВХОДНЫЕ ДАННЫЕ И ВНУТРЕННЕЕ ПРЕДСТАВЛЕНИЕ Ребро объекта может закрываться полностью или частично одной или несколькими гранями этого же объекта. Каждое ребро — это конечный отрезок прямой линии. Объект может состоять из нескольких частей, не обязательно соединяющихся между со- бой. На рис. 5.1 изображены тетраэдр и куб с вершинами, обозна- ченными числами 0, 1, 2, ... вместо букв А, В, С, .... Точки с обозначениями 12, 13, 14 позволяют вычертить положительные координатные полуоси. Рис. 5.1. Тетраэдр и куб
5.2 ВХОДНЫЕ ДАННЫЕ И ВНУТРЕННЕЕ ПРЕДСТАВЛЕНИЕ 119 Составим такую программу, которая прочтет данные, описы- вающие точку наблюдения и объекты. Для любой (реальной) точки наблюдения графический вывод будет представлять собой изображение объекта. В противовес главе 4 будем считать объект непрозрачным. Как и в параграфе 4.5, пользователь должен за- дать две входные строки данных, определяющие точку О в пря- моугольной системе координат и сферические координаты/о, 0, <р для точки наблюдения. Для рис. 5.1 это будут следующие строки: 2.5 1 1 (центр объекта О) 8 20 70 (rho, theta, phi) Не будем здесь уделять много внимания удобству пользовате- ля, а лучше введем концепцию кодирования вершин: поскольку каждая вершина потребуется несколько раз, то было бы очень нецелесообразно многократно задавать ее координаты. В данном примере для каждой вершины будем задавать ее номер от 0 до 11 вместе со значениями координат по осям х, у, z. Конец этой час- ти вводимой информации будет обозначаться отдельной строкой с символом # в первой позиции: 0 5 0 0 13 2 0 2 3 0 0 3 3 0 2 4 2 0 0 5 2 2 0 6 0 2 0 7 0 0 0 (хО-5, у0 = 0, z0 = 0) (xl = 3,yl=2,zl=0) (и т. д.) 8 2 0 2 9 2 2 2 10 0 2 2 11 0 0 2 12 7 0 0 13 0 4 0 14 0 0 3 # Как и в параграфе 4.5, точка О объекта является началом сис- темы мировых координат, используемой в программе (обратите внимание на различие между буквой О и цифрой 0). Поскольку она имеет координаты (2.5, 1, 1), то внутри программы коорди- наты точек с 0 по 14 будут уменьшены на эти значения. Так, на- пример, точке 9 будут присвоены значения координат (-0.5, 1,1) вместо заданных (2, 2,2). Каждая грань объекта может быть описана в виде любой ко- нечной области плоскости с границами в виде отрезков прямых линий. В качестве примера такой области можно назвать поли-
120 Глава 5. УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ гон, в котором допустимы также и отверстия. Чтобы программа была проще, пользователь должен сам разбить такие области на треугольники. Эти треугольники затем будут использоваться для определения, не закрывают ли они отрезки прямых линий. По причинам, которые поясним ниже, номера вершин в каждом тре- угольнике перечисляются в порядке обхода против часовой стрелки при рассматривании их с внешней стороны объекта. Каждая грань куба разбивается на два треугольника. Эту часть входных данных опять завершает символ # (на новой строке). (треугольные грани тетраэдра) (передняя грань куба) (правая грань) (верхняя Ьрань) (задняя грань) (левая грань) (нижняя грань) Теперь необходимо задать каждое ребро объекта. Хотя эти ре- бра уже известны как стороны треугольников, их следует опи- сать снова. Во-первых, не все стороны треугольников являются ребрами объекта и, во-вторых, желательно иметь возможность вычерчивания дополнительных отрезков, не принадлежащих сплошному телу. Для иллюстрации вычертим части положи- тельных координат полуосей, как показано на рис. 5.1. Как это ни курьезно, но в данном примере количество входных строк не увеличится, поскольку ребра объекта, лежащие на координат- ных осях, не нужно задавать повторно. Следовательно, имеем 17 входных строк: .1 1 2 0 4 8 5 9 8 8 6 10 7 7 6 6 # 3 2 0 2 5 5 6 6 9 10 7 7 4 8 5 4 0 3 3 1 8 9 9 10 10 11 10 11 8 11 4 7
5.2. ВХОДНЫЕ ДАННЫЕ И ВНУТРЕННЕЕ ПРЕДСТАВЛЕНИЕ 121 7 7 7 0 1 3 1 2 4 12 13 14 1 3 0 2 3 5 (ось х) 1 (ось у) (ОСЬ Z) (тетраэдр) (куб) 5 8 8 9 10 8 4 6 6 9 9 10 11 11 8 10 Программа выполнит считывание всех входных данных из файла, имя которого передается в качестве аргументов програм- мы (argc и argv)y как описано в параграфе 4.5. Координаты каж- дой вершины сохраняются в массиве VERTEX, элементами кото- рого являются структуры, содержащие три поля х, у, z. Заданные пользовательские координаты сначала переводятся в координа- ты системы, начало которой расположено в объектной точке О. Внутренние мировые координаты, в свою очередь, преобразуют- ся в видовые координаты с помощью видовых преобразований (см. параграф 4.2). И именно эти видовые координаты записыва- ются в массив VERTEX: i VERTEX [i] I x у z 0 ! { { 1 ... Как мы уже знаем, точка Е — начало системы видовых коор- динат, и направление наблюдения совпадает с направлением оси z. Следовательно, все значения VERTEX [i ].z должны быть поло- жительными. Это условие проверяется в программе, и выполне- ние программы прекращается, если оно не выполнено. Список треугольников запоминается в массиве TRIANGLE: j TRIANGLE [/] i В С i i a b с h i i i i
122 Глава J. УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ Вместе с номерами вершин для каждого треугольника запи- сываются коэффициенты я, 6, с, Л уравнения плоскости ax + by + cz = h (5.1) в которой расположен треугольник. Конечно, их можно было бы рассчитывать каждый раз, когда они оказываются необходимы- ми, но это означало бы напрасную трату времени, поскольку они требуются довольно часто. (Здесь четвертый коэффициент обоз- начен через Л вместо d, так как эта буква уже использована для обозначения расстояния до экрана.) Коэффициенты а, А, с выби- раются такими, чтобы удовлетворялось условие а2 + А2 + с2=1 и А>0 Тоща уравнение (5.1) можно записать в виде скалярного произ- ведения п • х = А где п- [а Ъ с] х = [х у z ] Так называемый вектор нормали п представляет собой вектор единичной длины, перпендикулярный плоскости треугольника. Для любой точки X в этой плоскости вектор х - ЕХ имеет такое свойство, что его скалярное произведение А = п • х равно рассто- янию между точкой Е и плоскостью. Скалярное произведение было введено в параграфе 3.2. Заме- тим, что здесь мы используем систему видовых координат с точ- кой Е в качестве начала системы координат и отрезок ЕО опреде- ляет положительное направление оси z. Плоскость, параллель- ная экрану, описывается уравнением z = Л, которое является вырожденным случаем общего уравнения (5.1) при а = Ъ=0, с = 1. В общем случае коэффициенты а, А, с, А вычисляются по коорди- натам вершин треугольника А, В, С. В параграфе 3.3 мы видели, что плоскость, проходящая через эти точки, описывается урав- нением х у ХА УА Хв Уъ хс Ус г 2А ZB zc 1 1 1 1
5.2. ВХОДНЫЕ ДАННЫЕ И ВНУТРЕННЕЕ ПРЕДСТАВЛЕНИЕ 123 Это уравнение можно переписать в виде 1 >А ZA l Ув zb 1 \Ус*с 1 х- xkz\ ! *В ZB 1 *с zc 1 У + *a?a 1 *в Уъ 1 хс Ус 1 z = *А УA ZA *В ?В ZB хс Ус z6 В программе коэффициенты а, Ь, с, h вычисляются по форму- лам: a*>yA*(zB-zC)-yB*(zA-zC)+yC*(zA-zB); b--(xA*(zB-zC)-xB* izA-zC) +xC* (zA-zB))\ схА * (уВ-уС) -хВ*(уА-уС) + хС* (уА-уВ); h-xA*(yB*zC-yC*zB)- xB*(yA*zC-yC*zA)+ xC*(yA*zB-yB*zA); - if(h>0) { r=sqrt(a* a + b* b + c* с); a - a/r, b - btr, с - c/r\ h » h/r; } else { .., /* Треугольник ABC может быть проигнори- рован no указанным ниже.причинам */ } Однократное вычисление коэффициентов а, 6, с, Л, вместо их определения при каждой проверке отрезка на видимость, позво- ляет значительно сократить время вычислений. Если программа действительно должна быть по возможности проще, то необходимо было бы запоминать все треугольники по мере их ввода. Однако те треугольники, которые находятся сза- ди, сохранять не требуется и их можно проигнорировать. Рас- смотрим треугольник 123 на рис. 5.1. На картинке он закрыт тре- угольником 130. Поскольку последний треугольник закрывает часть отрезка 45, то можно проигнорировать тот факт, что пред- ыдущий треугольник делает то же самое. Задние грани закрыва- ются видимыми гранями. Хотя задние грани могут закрывать от глаза некоторые точки, эти же точки закрываются и видимыми гранями. Вот почему задние грани можно проигнорировать. Са- мый простой способ идентификации задних граней основан на ориентации вершин. Если мы посмотрим на грань 123 на рис. 5.1 в трехмерном пространстве извне (то есть со стороны отрицательной полуоси х), то порядок обхода вершин 123 при вводе будет против часовой стрелки, поскольку такая ориентация требовалась при определе-
124 I лава 5. УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ нии входной последовательности. Однако на рис. 5.1 порядок об- хода 123 соответствует движению часовой стрелки. Это означа- ет, что на картинке эта грань видна через тело объекта, а не из- вне. Таким образом, грань 123 — задняя. Применение этого спо- соба определения положения граней для других треугольников на рис. 5.1 может послужить хорошим упражнением. Обращаясь к концу параграфа 3.4, можно обнаружить, что в этом случае требуется найти значение детерминанта \ХА УА II °ш \хв ув Ч где Ад, Уд, ХБ, Ув, Хс, Yc — экранные координаты вершин А, В, С треугольника. Треугольник ABC будет задним, если D< 0. Но нет необходимости в действительном вычислении детерминанта, поскольку можно записать D = VZA VZB *c'zc V2 1 a 1 Vzc l Уа Ув Ус I (2A ZB ZC) -A/(zA zB zc) (5.2) Так как значения zA, zB, zc всегда положительны, то D имеет тот же знак, что и величина А, вычисленная в предыдущей програм- ме. Если h = 0, то плоскость ABC проходит через точку начала координат Е, совпадающей с точкой наблюдения. В этом случае треугольник ABC не будет закрывать никаких отрезков и его не надо запоминать в массиве TRIANGLE. При А < 0 детерминант D также имеет отрицательный знак и треугольник ABC является задним. Поэтому треугольник ABC будем записывать в массив только в том случае, если Л > 0 или, другими словами, если D > 0. С помощью векторного произведения можно убедиться,что треугольник ABC — задний, из условия А < 0 и без привлечения экранных координат. Но доказательство этого условия оставля- ем для упражнения читателям с хорошей математической подго- товкой. 5.3. АЛГОРИТМ ОПРЕДЕЛЕНИЯ НЕВИДИМЫХ ЛИНИЙ Разработаем теперь очень важный алгоритм, задача которого заключается в вычерчивании только видимых частей отрезка
5.3. АЛГОРИТМ ОПРЕДЕЛЕНИЯ НЕВИДИМЫХ ЛИНИЙ 125 PQ. Для краткости часто будем записывать "PQ" для обозначе- ния "отрезка прямой линии PQ", являющегося частью бесконеч- ной прямой, проходящей через точки Р и Q. Для каждого треу- гольника ABC из списка, записанного в массив TRIANGLE, необ- ходимо выполнить проверку — не будет ли он закрывать PQ (или его часть). Как и ранее, Е — точка наблюдения. Будем говорить, что треугольник ABC закрывает точку R, если отрезок ER пере- секает треугольник ABC в точке, внутренней как по отношению к отрезку, так и по отношению к треугольнику. Стороны тре- угольника не относятся к внутренним точкам, а концевые точки отрезка не являются внутренними для отрезка. Таким образом, точка R не закрывается треугольником ABC ни в том случае, ког- да точка R принадлежит внутренней части треугольника, ни в том случае, когда она принадлежит стороне треугольника, за- крытой другим треугольником. Если треугольник ABC не закры- вает точку R, то будем говорить, что точка R видима по отноше- нию к треугольнику ABC. Если только конечное число точек от- резка PQ видимо по отношению к треугольнику ABC, все равно будем говорить, что ABC закрывает PQ. Например, на рис. 5.1 треугольник 130 закрывает отрезок 12, хотя этот треугольник не закрывает точку 1. Если треугольник не закрывает PQ, нельзя сделать заключение, что все точки PQ видимы по отношению к этому треугольнику, поскольку треугольник может закрыть PQ частично. Треугольник частично закрывает PQ, если он закры- вает бесконечно много точек отрезка PQ и в то же время беско- нечно много точек этого отрезка остаются видимыми по отноше- нию к этому треугольнику. Если некоторый треугольник закрывает PQ, то нет необходи- мости учитывать оставшиеся треугольники в списке, но можно немедленно прийти к заключению, что PQ закрыт и не должен вычерчиваться. Обсудим видимость отрезка прямой линии PQ по отношению к треугольнику ABC в алгоритмическом смысле. Пусть заданы видовые координаты дср, уру zp, Xq,. .. и так далее для пяти точек Р, Q, А, В, С и коэффициенты а, Ь, с, h уравнения ах + by + cz = Л. Вся информация о треугольнике ABC сохраняется в элементе массива TRIANGLE'[/]. Ключевым фактором в нашем анализе будет бесконечная пирамида, вершина которой находится в точ- ке Е, а боковые грани проходят через стороны треугольника АВ,
126 Глава 5. УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ ВС, СА, Эту пирамиду для краткости назовем одним словом "пи- рамида ", а треугольник ABC — термином "треугольник". Все внутри пирамиды позади треугольника будет невидимым, а все точки впереди или вне пирамиды — видимыми (относи- тельно данного треугольника). На рис. 5.2 отрезок прямой линии PQ пересекает пирамиду в двух точках — I и J. Часть IJ отрезка PQ будет невидимой, а части PI и JQ — видимыми. (Ради крат- кости вместо фразы "видимый по отношению к треугольнику ABC" будем применять термин "видимый".) Сложность нашей задачи заключается в очень большом коли- честве случаев, которые предстоит рассмотреть. В ситуации, по- казанной на рис. 5.2, можем вычислить положение точки I сле- дующим образом. Векторное представление прямой линии PQ записывается в виде ЕР+Аг где г - [гх г2 г3 ] - PQ, откуда ri e xq "~ хр r2^Q-^P Г3 ~ ZQ ZP точка (ху у у z) принадлежит прямой линии PQ, если x-jCp+Arj у-Ур+А/^ (5.3) z = zp+Ar^ Для значений А между 0 и 1 точка лежит между точками Р и Q. Уравнение плоскости ЕАВ может быть записано как х у z ХА У A ZA ХЕ УВ ZB И
5.3. АЛГОРИТМ ОПРЕДЕЛЕНИЯ НЕВИДИМЫХ ЛИНИЙ 127 поскольку эта плоскость проходит через точки Е(0, 0, 0), А, В. Уравнение можно переписать в виде Clx + C2y + C3z = 0 (5.4) где ci-3>AzB-yBzA С2 = *BZA " *AZB C3 = Vb"¥a Подставляя правые части уравнений (5.3) в уравнение (5.4), находим _ _ <ClXp + C2yp + C3zp) (Cxrx+C2r2 + C3r3) Используя значение А в уравнении (5.3), получаем координа- ты точки I. Изображенная на рис. 5.2 ситуация возникает только тогда, когда значение Я находится в пределах от 0 до 1. Для дру- гих значений А точка I лежит не на отрезке PQ, а на одном из его продолжений. Нельзя утверждать, что при значении А от 0 до 1 отрезок PQ пересекает пирамиду, верно лишь обратное утверж- дение. На рис. 5.3 показана ситуация, видимая из точки Е, когда значение Л лежит в пределах от 0 до 1, хотя отрезок PQ не пересе- кает пирамиду. Возвращаясь к трехмерному пространству (рис. 5.2), можно представить плоскость, проходящую через прямую линию PQ и точку наблюдения Е. Теперь мы хотим узнать, не будет ли эта плоскость проходить через точки, принадлежащие отрезку АВ. Рис. 5.3. Отрезок PQ вне пирамиды
128 Глава 5. УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ Здесь необходимо выполнить вычисления, аналогичные прово- димым при определении значения Я. Запишем векторное пред- ставление прямой линии АВ ЕА+/г АВ Тогда точка Ос, у, z) принадлежит прямой линии АВ, если х = хА+/г(дсв-хА) y = yA+V%-yA> (5.6) 2 = ZA+//(ZB"ZA) Для значений /и от 0 до 1 точка лежит между А и В. Уравнение плоскости EPQ х у z I *Р Ур ZP =0 \*q yq zqI Перепишем это уравнение в виде A^jc + ity + A^O (5.7) где Ki=ypzQ-yqzp К2 = *QZP "" *PZQ К3 = XPyQ" ХоУр Совмещая уравнения (5.6) и (5.7), получаем *1*A + *2?A+-*3ZA (*п М Kl<*B-*A^K2%-yJ+K3<ZB-ZJ Отрезок прямой линии PQ и плоскость пирамиды ЕАВ имеют общую точку, если, и только если 0<А < 1 и 0</г < 1 До сих пор мы рассматривали пересечение прямой PQ только с одной из плоскостей пирамиды, а именно с ЕАВ. Необходимо также рассмотреть плоскости ЕВС и ЕСА. Отрезок PQ может не иметь либо иметь одно или два пересечения с пирамидой. Соот- ветственно, точки Р и Q морут лежать внутри, вне или на пира- миде, то есть располагаться впереди, позади или на плоскости ABC. Для проверки каждой пары из одного отрезка и одного тре- угольника может потребоваться большой объем работы. Обычно имеется очень много отрезков прямых, и каждый из них необхо- димо проверять относительно большого количества треугольни- ков. Такие проверки нужно выполнять с большой эффективно-
5.3. АЛГОРИТМ ОПРЕДЕЛЕНИЯ НЕВИДИМЫХ ЛИНИЙ 129 стью. (В параграфе 5.4 описана эффективна» методика по умень- шению числа треугольников, анализируемых совместно с дан- ным отрезком, что позволяет снизить количество проверок. Но каждая проверка для данного отрезка и данного треугольника должна выполняться по описанному алгоритму). Желательно как можно раньше начинать с таких случаев, которые встреча- ются наиболее часто, особенно если не требуются большие затра- ты машинного времени. При успешном выполнении теста ос- тальные тесты игнорируются. Тест 1 (рис. 5.4) Если точки Р и Q лежат перед или на плоскости ABC (но не позади нее), то отрезок PQ — видимый. Это происходит когда п • ЕР < А и n EQ < Л где n = [a be]. Напомним, что коэффициенты а> Ь> с, Л появились в уравнении (5.1) и они записаны в элементе массива TRIANGLE УI Тест 1 охватывает и тот важный случай, когда PQ представ- ляет собой одну из сторон треугольника. Рис. 5.4. Точки Р uQ расположены не сзади плоскости Тест 2 (рис. 5.5) Если бесконечная прямая линия PQ лежит вне пирамиды, то линия PQ видима. Для выполнения теста можно подставить зна- чения координат точек А, В, С в левую часть уравнения (5.7), определяющего плоскость EPQ. Если все три вычисленных зна- 5—271
130 Глава 5. УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ Q Р^^"С С /ч & л Д. ^ д- .g (а) р (б) Рис. 5.5. (а) — модуль суммы знаков - J; (б) — модуль суммы знаков - 2 чения (для точек А, В и С) имеют одинаковые знаки (все поло- жительные или все отрицательные), то все точки А, В, С лежат по одну сторону от плоскости EPQ. Следовательно, линия PQ расположена вне пирамиды и является видимой. Несколько ос- лабим это знаковое условие в том смысле, что один из знаков мо- жет быть равен нулю. Предположим, что каждый из знаков обоз- начается одним из чисел -1,0, 1, и проверим, не будет ли сумма знаков для точек А, В, С равна одному из чисел -3, -2, 2,3. Заме- тим, что на рис. 5.5(6) может произойти и такой случай, когда точка Р совпадает с точкой А. Это тоже очень важный специаль- ный случай. ТестЗ Теперь найдем точку пересечения прямой линии PQ с пло- скостями ЕАВ, ЕВС и ЕАС. Вычислим значения параметров А для точки пересечения прямой линии PQ с плоскостью ЕАВ по уравнению (5.5) и /г для точки пересечения прямой АВ с пло- скостью EPQ по уравнению (5.8). (В обоих случаях прямая ли- ния может оказаться параллельной плоскости, поэтому парамет- рамЛи/i будут присвоены очень большие числа.) Уравнение (5.4) описывает плоскость ЕАВ. Если левая часть этого уравне- ния при подстановке координат точек Р и С принимает разные знаки, то это означает, что точки Р и С лежат по разные стороны от прямой АВ. Тогда можно сказать, что точка Р лежит за прямой АВ. Если точка лежит за одной из сторон треугольника, то она расположена вне этого треугольника. Запомним эту информа-
J.J. АЛГОРИТМ ОПРЕДЕЛЕНИЯ НЕВИДИМЫХ ЛИНИЙ 131 р (а) Р (б) Рис. 56. (а) — точкиPuQmАВ; (б) — точкаРзаАВ, точкаQnaAB цию в логической переменной Poutside ("точка Р вне"). Анало- гично для точки Q введена переменная Qputside ("точка Q вне"). Эти переменные будут проверяться в тестах 4 и 6. Если точки Р и Q лежат за одной и той же стороной треугольника или одна точка находится за стороной, а другая — точно на этой стороне, то бу- дем говорить, что отрезок PQ лежит вне этого треугольника. Тог- да отрезок PQ — видимый. Обе ситуации показаны на рис. 5.6. Важный специальный случай возникает при небольшой модифи- кации рис. 5.6(6), когда точка Q совпадает с точкой А. Заметим, что в отличие от рис. 5.5(6) бесконечная прямая линия PQ на рис. 5.6(6) может пересекать отрезок ВС между точками В и С так, что тест 3 будет выполнен в том случае, когда тест 2 оказал- ся невыполненным. Большая часть работы должна быть выполнена трижды, а именно: для каждой из плоскостей ЕАВ, ЕВС и ЕСА, следова- тельно, будем иметь три пары значений (А^,/^) (/=1,2,3). За- тем найдем минимальное и максимальное из значенийА^, удов- летворяющие условиям О < А < 1 и 0< /* < 1 и эти значения обозначим MIN и МАХ, соответственно. Они яв- ляются побочным продуктом этого теста и определяются только тогда, когда отрезок PQ пересекает треугольник, то есть если тесты 3 и 4 (см. ниже) не выполнены. Тест 4 (рис. 5.7) Если и точка Р, и точка Q находятся внутри пирамиды, а предыдущий тест не удовлетворен, то отрезок PQ лежит позади треугольника и, следовательно, невидим.
132 Глава 5. УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ При выполнении этого теста также может встретиться весьма хитрая ситуация. Предположим, например, что отрезок PQ ле- жит на пирамиде позади отрезка АВ. Поскольку отрезок PQ не находится ни снаружи, ни внутри пирамиды, то кажется сомни- тельным, можем ли мы сказать, что треугольник закрывает отре- зок PQ. Однако отрезок АВ может быть не ребром, а диагональю грани. Тогда эта грань определенно закрывает отрезок и он не должен вычерчиваться. С другой стороны, если бы отрезок АВ был ребром объекта, то и тогда отрезок PQ вычерчивать было бы не нужно, поскольку изображение отрезка PQ совпадало бы с ре- бром АВ, а чертить совпадающие отрезки линий нет никакого смысла. Рис. 5.7. Отрезок PQ позади треугольника ABC Тест 5 (рис. 5.8) Если точка I, в которой прямая линия PQ пересекает пирами- ду, лежит впереди треугольника, то прямая PQ видима. Этот тест основан на том факте, что объект является сплошным телом, поэтому прямая PQ не может проходить через внутренние точки треугольника. Для нахождения такой точки I используем зна- чения MIN и МАХ параметра Я, вычисленные при выполнении теста 3. Затем можно просто проверить, не будет ли скалярное произведение векторов EI и n = [a be] меньше, чем А. Тест 6 Этот тест выполняется только в том случае, если все предыду- щие тесты не дали положительного результата, то есть когда прямая PQ пересекает пирамиду позади треугольника.
5.3. АЛГОРИТМ ОПРЕДЕЛЕНИЯ НЕВИДИМЫХ ЛИНИЙ 133 Рис. 5.8. Точка пересечения перед треугольником ABC Если значения MINnMAX параметра А указывают на точки пересечения I и J, как на рис. 5.2, тогда отрезок IJ будет невиди- мым, и: — если точка Р находится вне пирамиды или перед плоскостью ABC, то отрезок PI видимый; — если точка Q находится вне пирамиды или перед плоскостью ABC, то отрезок JQ видимый. Если точки Р и Q, обе одновременно, не находятся вне пира- миды, то точки I и J совпадают. Это показано на рис. 5.9. Во всех предыдущих тестах при определении видимости от- резка (по отношению к j-му треугольнику) значение параметра / увеличивается на единицу. Затем проверяется отношение отрез- ка прямой линии PQ с остальными треугольниками, если они Рис. 5.9. Только одна точка пересечения
134 Глава 5. УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ еще есть, или отрезок вычерчивается, если треугольников боль- ше нет. Но в этом тесте могут появиться два новых отрезка PI и JQ, которые должны быть проверены на отношение с оставшими- ся треугольниками. Здесь можно будет рекурсивно дважды обра- титься к этому же алгоритму. Для каждого рекурсивного обра- щения будем задавать концевые точки отрезка прямой линии и номер/И треугольника, с которого должна начаться проверка. Замечания о точности представления чисел Для сложных вычислений целесообразно использовать тип double вместо float. Поскольку тип double означает "двойная точ- ность с плавающей точкой", то неформально можно говорить о вычислениях с "плавающей точкой", хотя в действительности при этом имеется в виду двойная точность double. Но даже при использовании типа double вещественные числа представляются с конечной точностью. Если в результате некоторых сложных вычислений были получены два числа, например 5.843216 и 5.843217, отличающихся только последней цифрой, то их вполне можно считать равными. Если хотим, чтобы так же действовал и компьютер, то это правило необходимо сформулировать более определенно. Для чисел с плавающей точкой оно может оказать- ся совсем не простым. Обычно задается небольшая положитель- ная величина (например, е- 1(Г ) и вместо условного оператора х = = а (для числа с плавающей точкой х и константы а) записы- вают один из условных операторов: fabs (x-a) <= epsilon или fabs (x-a) <= epsilon*fabs(а) или fabsix - a) <= epsilon + epsilon * fabs (a) В первом условии параметр epsilon равен абсолютной ошиб- ке, которая допускается при вычислениях. При такой проверке могут возникнуть проблемы для больших значений х и а, если в этих случаях не задавать несколько большие значения epsilon. Поскольку абсолютная величина х и а обычно заранее не извест- на, то предпочтительнее иметь фиксированное значение для epsilon, но заменить первую проверку на вторую, где через epsilon обозначена максимальная относительная ошибка. Одна- ко если величина а будет равна (почти) нулю, то правая часть неравенства будет также (почти) нуль, что совсем не то, чего бы
5.3. АЛГОРИТМ ОПРЕДЕЛЕНИЯ НЕВИДИМЫХ ЛИНИЙ 135 нам хотелось. Поэтому будем использовать третью проверку, где абсолютная и относительная ошибки совмещены. Очевидно, что она сводится к первой при а = 0. На основании сказанного, проверка значений вещественных переменных х и а выполняется в соответствии со следующей таб- лицей, где введено обозначение epsl = epsilon + epsilon * fabs(a): Btoecro x = = a x-l-a x<a jc<-a x> a x>**a * = = 0 x\=0 x<0 x<=0 x>0 x>=0 Будем писать fabs(x-a) <=eps 1 fabs(x-a) > epsl x<a-epsl x<=a + ^psl x > a + epsl x>=a-epsl fabs(x) <e epsilon fabs(x) > epsilon x < -epsilon x <= epsilon x > epsilon x >"= -epsilon Заметим, что вторая часть этой таблицы может быть выведе- на из первой. Эти тесты будут использованы в программе HIDLIN, приведенной ниже. /* HIDLIN: Простая программа для удаления невидимых линий. */ /* Результат работы этой программы записывается в файл */ /* A.SCRATCH, который предназначен для считывания */ /* программой GENPLOT */ /* A simple program for hidden-line elimination. V /* The output of this program Is the file A.SCRATCH, */ /* which Is to be read by GENPLOT. */ #lnclude "stdlo.h" #lnclude "math.h" #deflne NVERTEX 50 #deflne NTRIANGLE 50 unsigned Int _STACK-15000; /* Lattice С */ Int ntr-0; double v11, v12, v13. v21, v22, v23. v32, v33, v43, eps- 1e-5, meps--1e-5, oneminus- 1-1.e-5, oneplus- 1+1.e-5; FILE *fpout; struct { float X, Y; Int code; } s;
136 Глава 5. УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ struct { double x, у, z; } VERTEX [NVERTEX]; struct { int A, B, C; double a, b, c, h; } TRIANGLE [NTRIANGLE]; maln(argc, argv) int argc; char *argv[ ]; { Inti, A. B. C, P.Q; double xO, yO, zO, rho, theta, phi, x, y, z, xA. yA, zA, xB, yB. zB, xC, yC, zC, a, b, c, h, r; FILE *fpin; int ch; If (argc!-2 I I (fpin-fopen(argv[1], "r"))-- NULL) errmess /* "Input file not correctly specified" */ ("Входной файл задан некорректно"); fscanf(fpin, "%lf %lf %lf", &xO, &yO, &zO); skipf(fpin); fscanf(fpin, "%lf %lf %lf", &rho, &theta, &phi); coeff(rho, theta, phi); while (skipf(fpin), ch-getc(fpin), ch!-'#' && ch!-EOF) { ungetc(ch, fpin); fscanf(fpin, "%d %lf %lf %lf", &i. &x, &y, &z); if (i<0 I I i>-NVERTEX) errmess /* "illegal vertex number"*/ ("Неправильный номер вершины"); viewing(x-xO, y-yO, z-zO, &VERTEX[i].x. &VERTEX[i].y, &VERTEX[l].z); if(VERTEX[i].z<-eps) { printf /* "Object point О and vertex ... lie on "*/ ("Точка объекта О и вершина %d лежат по", i); errmess /* "different sides of viewpoint E." */ ("разные стороны от точки наблюдения Е."); } } while (skipf(fpin), ch-getc(fpin), ch!-#' && ch!-EOF) { ungetc(ch, fpin); fscanf(fpin, "%d %d %d", &A, &B, &C); xA-VERTEX[A].x; yA-VERTEX[A].y; zA-VERTEX[A].z; xB-VERTEX[B].x; yB-VERTEX[B].y; zB-VERTEX[B].z; xC-VERTEX[C].x; yC-VERTEX[C].y; zC-VERTEX[C].z; a - yA * (zB-zC) - yB * (zA-zC) + yC * (zA-zB); b - -(xA * (zB-zC) - xB * (zA-zC) + xC * (zA-zB)); с - xA * (yB-yC) - xB * (yA-yC) + xC * (yA-yB); h-xA*(yB*zC-yC*zB)- xB*(yA*zC-yC*zA) + xC * (yA*zB - yB*zA); if (h>0) { if (ntr-т NTRIANGLE) errmess /* "Too many triangles" */ ("Слишком много треугольников"); г - sqrt(a*a+b*b+c*c); а - a/r; b - b/r; с - c/r; h - h/r; TRIANGLE[ntr].A-A; TRIANGLE[ntr].B-B; TRIANGLE[ntr].C-C;
5.3. АЛГОРИТМ ОПРЕДЕЛЕНИЯ НЕВИДИМЫХ ЛИНИЙ 137 TRIANGLE[ntr].a-a; TRIANGLE[ntr].b-b; TRIANGLE[ntr].c-c; TRIANGLE[ntr++].h«h; } /* If h-0, plane ABC passes through E and hides nothing. */ /* If h<0. triangle ABC Is a backface. */ /* In both cases ntr is not incremented and the triangle is not stored. */ /* Если h4), то плоскость ABC проходит через точку Е и ничего не */ /* закрывает. Если h<0, то треугольник будет задним. В обоих случаях*/ /* параметр ntr не увеличивается и треугольник не запоминается. */ } fpout-fopen("a.scratch", "wb"); /* required in Lattice С for binary mode */ /* параметр "b" для бинарного режима */ if(fpout--NULL) errmess /* "file a.scratch cannot be opened" */ ("Файл a.scratch нельзя открыть"); while (skipf(fpin), fscanf(fpin, "%d %d", &P, &Q)X)) llnesegment(VERTEX[P].x, VERTEX[P].y. VERTEX[P].z. VERTEX[Q].x. VERTEX[Q].y, VERTEX[Q].z. 0); fclose(fpout); } skipf(fpin) FILE *fpin; { intch; while (ch-getc(fpin), ch !- An' && ch !- EOF); ■[ errmess(str) char *str; { printf("%s\n", str); exlt(1); } coeff(rho, theta. phi) double rho. theta, phi; { double th, ph, costh, sinth, cosph, sinph, factor; factor-atan(1.0)/45.0; /* Angles In radians: */ /* Углы в радианах */ th-theta*factor; ph-phi*factor; costh-cos(th); sinth-sm(th); cosph-cos(ph); sinph-sin(ph); /* Elements of matrix V, see Eq. (4.9): */ /* Элементы матрицы V, см. (4.9): */ v11— sinth; v12-^cosph*costh; v13—sinph*costh; v21-costh; v22-^cosph*sinth; v23—sinph*sinth; v32-sinph; v33—cosph; v43-rho; } viewing(x, y, z, pxe, pye, pze) double x. y, z, *pxe, *pye, *pze; { /* Eye coordinates, computed as in Eq. (4.2): ■*/ /* Видовые координаты, вычисляемые по (4.2): */
138 Глава J, УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ } *pxe-v11*x + v21*y; *руе - v12*x + v22*y + v32*z; *pze - v13*x + v23*y + v33*z + v43; llnesegment(xP, yP, zP, xQ, yQ, zQ, JO) double xP, yP, zP, xQ, yQ, zQ; int JO; { /* Line segment PQ is to be drawn, as far as it is */ /* not hidden by the triangles JO, jO+1 ntr. */ /* Отрезок прямой PQ должен быть вычерчен, поскольку */ /* он не закрывается треугольниками J0J0+1 ntr. V Int J—JO, worktodo-1, A, B, C, I, Pbeyond, Qbeyond, outside, Poutslde, Qoutslde, eA, eB, eC, sum; double a, b, c, h, hP, hQ, П, r2, r3, xA, yA, zA, xB, yB, zB, xC, yC, zC, dA, dB, dC, MIN, MAX, lab, mu, xmin, ymin, zmin, xmax, ymax, zmax, C1, C2, C3, K1, K2, КЗ, denoml, denom2, Cpos, Ppos, Qpos, aux, epsl; while 0<ntr) { a-TRIANGLE[j].a; b-TRIANGLE[J].b; c-TRIANGLED]c; h-TRIANGLE[j].h; eps1-eps+eps*h; /* Test 1 */ /* Тест 1 */ hP-a*xP+b*yP+c*zP; hQ-a*xQ+b*yQ+c*zQ; if (hP<-h+eps1 && hCX-h+eps1){J++; continue;} /* PQ not behind ABC */ /* отрезок PQ не позади треугольника ABC */ /*Test2V /*Твст2*/ K2-zP*xQ-zQ*xP; B-TRIANGLED].B; yA-VERTEX[A].y; yB-VERTEX[B].y; yC-VERTEX[C].y; K3-xP*yQ-xQ*yP; C-TRIANGLED1-C; zA-VERTEX[A].z; zB-VERTEXtB].z; zOVERTEX[C].z; K1-yP*zQ-yQ*zP; A-TRIANGLED].A; xA-VERTEX[A].x; xB-VERTEX[B].x; xC-VERTEX[C].x; d A-K1 *xA+K2*yA+K3*zA; dB-K1*x8+K2*yB+K3*zB; dC-K1*xC+K2*yC+K3*zC; /* If dA, dB, dC have the same sign, the vertices А, В, С */ /* lie at the same side of plane EPQ. */ /* Если dA, dB, dC имеют одинаковые знаки, то вершины */ /* А, В, С расположены по одну сторону от плоскости EPQ */ eA- dA>eps ? 1 : dA<meps ? -1 : 0; eB- dB>eps ? 1 : dB<meps ? -1 : 0; eC- dOeps ? 1 : dC<meps ? -1 : 0; sum - еА+еВ+еС; if (abs(sum)>-2) {j++; continue; }
5.3. АЛГОРИТМ ОПРЕДЕЛЕНИЯ НЕВИДИМЫХ ЛИНИЙ 139 /* If this test succeeds, the (infinite) line PQ lies */ /* outside pyramid EABC (at most one common point). */ /* If the test falls, there Is a point of intersection. */ /* Если этот тест завершен успешно, то (бесконечная) */ /* прямая линия PQ лежит вне пирамиды ЕАВС */ /* (или имеет, по крайней мере, одну общую точку). */ /* Если тест не выполнен, то имеется точка пересечения. */ /*Test3V ЛТестЗ*/ Poutslde-Qoutslde-O; MIN-1.; МАХ-0.; for (i-0; КЗ; I++) { C1-yA*zB-yB*zA; C2-zA*xB-zB*xA; СЗ-хА*уВ-хВ*уА; /*C1x + C2y + C3z-0 IsplaneEAB */ /* уравнение описывает плоскость ЕАВ */ Cpos-C1*xC+C2*yC+C3*zC; Ppos-C1*xP+C2*yP+C3*zP; Qpos-C 1 *xQ+C2*yQ+C3*zQ; denom 1-Qpos-Ppos; If (Cpos>eps) { Pbeyond- Ppos<meps; Qbeyond- Qpos<meps; outside- Pbeyond && Qpos<-eps 11 Qbeyond && Ppos<-eps; } else if (Cpos<meps) { Pbeyond- Ppos>eps; Qbeyond- Qpos>eps; outside- Pbeyond && Qpos>-meps 11 Qbeyond && Ppos>-meps; } else outslde-1; If (outside) break; lab-fabs(denoml)<-eps ? 1.e7:-Ppos/denom1; /* lab Indicates where PQ meets plane EAB */ /* Параметр lab указывает на точку пересечения */ /* отрезка PQ с плоскостью ЕАВ */ Poutside I-Pbeyond; Qoutslde I-Qbeyond; denom2-dB-dA; mu- fabs(denom2)<-eps ? 1.e7 : -dA/denom2; /* mu telJs where AB meets plane EPQ */ /* Параметр mu указывает на точку пересечения */ /* отрезка АВ с плоскостью EPQ */ if (mu>-meps && mu<-oneplus && lab>-meps && Jab<-oneplus) \ { if(lab<fwWN)MINHab; Л lf()ab>MAX)MAX-lab; } aux-xA; xA-xB; xB-xC; xC-aux; aux-yA; yA-yB; уВ-уС; yC-aux; aux-zA; zA-zB; zB-zC; zC^aux; aux-dA; dA-dB; dB-dC; dC'-aux;
140 Глава 5. УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ if (outside) {j++; continue;} /*Test4V /*Тест4*/ lf(!(Poutside I I Qoutside)) { worktodo-0; break; /* PQ invisible */ /* Отрезок PQ невидимый */ } /*Test5V /*Тест5*/ r1-xQ-xP; r2-yQ-yP; r3-zQ-zP; xmin-xP+MIN*r1; ymin-yP+MIN*r2; zmln-zP+MIN*r3; if (a*xmin+b*ymin+c*zmin<h-eps1){ j++; continue; } xmax-xP+MAX*r1; ymax-yP+MAX*r2; zmax-zP+MAX*r3; if (a*xmax+b*ymax+c*zmax<h-eps1) {j++; continue; } /* If this test succeeds, an intersection of PQ */ /* and the pyramid lies in front of plane ABC. */ /* Если этот тест выполнен успешно, то точка пересечения */ /* отрезка PQ и пирамиды находится перед плоскостью ABC. */ /*Test.6V ЛТестб*/ if(Poutside) Hnesegment(xP, yP, zP, xmln, ymin, zmin, j+1); if (Qoutside) linesegment(xQ, yQ, zQ, xmax, ymax, zmax, J+1); worktodoK); break; } if (worktodo) { s.X-xP/zP; s.Y-yP/zP; s.code-0; fwrite(&s, sizeof s, 1, fpout); s.X-xQ/zQ; s.Y-yQ/zQ; s.code-1; fwrite(&s, sizeof s, 1, fpout); } } Этой программой были считаны входные данные, приведен- ные в параграфе 5.2; результат ее работы записан в файл A.SCRATCH и считан программой GENPLOT, которая выдала изображение, показанное на рис. 5.10. 5.4. ПОЛИГОНЫ И ПИКСЕЛЫ Улучшим теперь программу HIDLIN из параграфа 5.3 с целью повышения удобства для пользователя и увеличения эф- фективности. Дадим возможность пользователю определить гра- ни объекта в виде полигонов , а программа сама будет преобразо- 1 См. сноску в параграфе 3.5.
5.4. ПОЛИГОНЫ И ПИКСЕЛЫ 141 Рис. 5.10. Результат работы программы HI DUN вывать эти полигоны в треугольники. Тогда список отрезков бу- дет не длиннее, чем если бы они задавались отдельно как ребра полигонов, что обычно имеет место. В качестве примера рассмотрим объемную букву А, изобра- женную на рис. 5.11. Передняя и задняя грани не являются в полном смысле полигонами, поскольку содержат треугольные отверстия. Все остальные грани представляются в виде прямо- угольников. Вершины на передней грани имеют номера от 0 до 9. Увеличение каждого номера на 10 приводит к получению номе- ров соответствующих вершин задней грани. Во входном файле для нашей новой программы переднюю грань можно описать сле- дующим образом: 0123456-9 879-6 Знак минус между обозначениями двух вершин 6 -9 и 9 -6 оз- начает, что эта последовательность определяет невычерчивае- мый отрезок. Такие последовательности служат только в качест- ве средства описания области на передней грани. Штриховая ли- ния между точками 6 и 9 на рис. 5.11 может рассматриваться как два совпадающих ребра полигона. Передняя грань на рис. 5.11 будет получена, если расстояние d в полигоне на рис. 5.12 будет исчезающе малым. Вместо отрезка 6 -9 может быть взята любая
142 Глава J. УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ Рис. 5.11. Объемная буква А другая пара точек для соединения внешней и внутренней границ (только следует иметь в виду, что нельзя записать -0, поскольку такая запись не представляет отрицательного числа). Вершины должны быть перечислены в порядке обхода против часовой стрелки, за исключением внутренних частей, которые обходятся по часовой стрелке. Общее правило заключается в следующем. Если обходим все ребра от вершины к вершине в перечисленном порядке, каждый раз глядя в направлении следующей вершины, то определяемая область должна находиться с левой стороны. Вот почему за обозначением -9 в вышеприведенной последова- тельности следует номер 8. Если на рис. 5.11 будем двигаться от вершины 9 к вершине 8, глядя на вершину 8, то "тело" буквы А будет находиться слева от нас. Заметим, что входная последова- тельность интерпретируется циклически и конечная вершина (6) соединяется с первой вершиной (0).
5.4. ПОЛИГОНЫ И ПИКСЕЛЫ 143 Рис. 5.12. Полигон Потребуем, чтобы первые три вершины образовывали выпук- лый угол. Например, из рис. 5.11 видно, что перечень 12 3 в начале последовательности будет неподходящим, а перечень 6 0 1 может быть приемлемым. За последней вершиной каждой последовательности должен немедленно следовать символ #. Такое соглашение позволяет ис- пользовать несколько входных строк для описания одной после- довательности, что дает возможность задавать полигоны с очень большим числом вершин. Если последовательность содержит только две вершины, то она интерпретируется как отдельный от- резок прямой линии. Это свойство будет использовано, напри- мер, для вычерчивания координатных осей, как на рис. 5.10.
144 Глава 5. УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ Как и ранее, файл начинается с координат х у z центральной точки О, которая будет использована в качестве но- вого начала системы координат. Также потребуются сфериче- ские координаты/), 0, <р точки наблюдения Е относительно точки О (рис. 4.3), но вместо считывания их из файла программа запро- сит пользователя ввести эти три числа. Таким образом, програм- му можно будет запускать на выполнение несколько раз с раз- личными точками наблюдения без изменения файла. Прямая линия ЕО определяет направление наблюдения. Номер каждой вершины и ее координаты дс, у, z опять считываются из файла. Затем перечисляются номера вершин, определяющих грани, как было описано ранее. Помните, что для определения направления обхода по часовой стрелке на грань следует смотреть снаружи объекта. Примем соглашение, что комментарии могут появлять- ся в любом месте вне перечня чисел и все комментарии заключа- ются в скобки, они не должны быть вложенными. Для определе- ния начала последней, части входных данных указывается слово Faces ("грани'1). Фактически проверяется только первая буква (прописная F), а остальные пропускаются, поэтому подойдет и слово Facets, Полный список входных данных для вычерчивания буквы А (рис. 5.11) приведен ниже: 0 0 30 0 0 -30 10 -10 -30 1 0 -20 11 -10 -20 2 0 -16 12 -10 -16 3 0 16 13 -10 16 4 0 20 14 -10 20 5 0 30 15 -10 30 60 0 16 -10 0 7 0 -12 0 0 0 0 8 8 8 8 -О 0 0 0 60 60 16 (координаты точки О: направление наблюдения - ЕО) (вершина 0) (вершина 10) (вершина 1) (вершина 11) (вершина 2) (вершина 12) (вершина 3) (вершина 13) (вершина 4) (вершина 14) (вершина 5) (вершина 15) (вершина 6) (вершина 16) (вершина 7)
5.4. ПОЛИГОНЫ И ПИКСЕЛЫ 145 17 8 18 9 19 Fac 0 10 1 2 14 7 7 18 5 10 10 14 -10 0 -10 0 -10 es—1 1 16 11 12 4 8 17 8 15 0 11 15 -12 12 12 .0 0 16 16 16 40 40 Грани: 2 -19 12 13 3 18 19 9 16 6 1 5 3 17 2# 3# 13# 17# 9# 19# 6# 16# 0# 4# (вершина 17) (вершина 8) (вершина 18) (вершина 9) (вершина 19) 4 5 6-9 18 19 -16 15 Программа, которую предстоит разработать, будет гораздо удобнее программы HIDLIN из параграфа 5.3, поскольку вместо треугольника и отрезков прямых теперь можно будет задавать грани объекта со сложной формой. Однако это не единственное усовершенствование. В программе HIDLIN мы имели набор тре- угольников. Видимость каждого отрезка проверялась относи- тельно каждого треугольника. Поэтому при повышении сложно- сти объекта, например, оба списка удлиняются в два раза, затра- ты вычислительного времени возрастают в четыре раза. Будем говорить, что программа HIDLIN имеет квадратичную слож- но сть по затратам времени. Для сложных объектов наша новая программа будет работать значительно быстрее программы HIDLIN, Как и ранее, будем иметь набор отрезков и набор треугольников, но видимость каж- дого данного отрезка теперь будет проверяться только относи- тельно части набора треугольников. Например, отрезок PQ, ле- жащий в левой верхней части экрана, не будет проверяться отно- сительно треугольника ABC в правой нижней части экрана. Для реализации этого алгоритма разделим экран на Nscreen * N screen равных прямоугольников, где Nscreen — некоторое по- ложительное целое число. Для рис. 5.13 выбрано Nscreen = 8.
146 Г лава 5. УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ гтах row numbers < jpix Номера строк 7 6 0 ч Улгйп J > НОШ 111 I и tllll \ ^А= |=Cg = в| 0 1 2 3 4 5 6 7 А Ч_ . • I col umn numb< ?rs *тт ipix Номера колонок жтах Рис. 5.13. Приборно-независимые пикселы Такой элементарный прямоугольник назовем pixel (пиксел), что в переводе с английского языка означает "элемент картин- ки". Пикселы обычно ассоциируются с растровыми дисплеями. Для получения приемлемой разрешающей способности требует- ся обеспечение вывода нескольких сотен пикселов по обоим на- правлениям, по горизонтали и по вертикали, поскольку каждый пикселы целиком заполняется одним цветом. До сих пор наш подход был приборно-независимым. Вероятно вопреки тому, что ожидает читатель после введения пикселов, мы по-прежнему бу- дем придерживаться этого принципа! Фактически будем исполь- зовать приборно-независимые элементы картинки, так что для них можно было бы изобрести какое-нибудь новое имя, напри- мер dipels. Но вместо этого будем придерживаться обычной тер- минологии и использовать термин пиксел. Заметим, однако, что наши пикселы не имеют ничего общего с разрешающей способ- ностью отображения и вообще с техническими средствами. Вы- бор значения параметра N screen (на рис. 5.13 N screen « 8) влия- ет на затраты вычислительного времени, но в результате получа- ется то же самое изображение. Хорошие показатели были
5.4. ПОЛИГОНЫ И ПИКСЕЛЫ 147 получены при Nscreen = 30, то есть при использовании 30 * 30 - 900 пикселов. Повышение эффективности основано на идее формирования списков треугольников для каждого пиксела. Такой список пик- селов содержит только те треугольники, которые частично или полностью покрывают пиксел. Треугольник ABC на рис. 5.13 бу- дет занесен в списки только для заштрихованных пикселов из ко- лонок 5, 6, 7. Как и в программе HIDLIN, будет существовать один общий массив TRIANGLE, элементами которого являются структуры, содержащие номера вершин А, В, С и коэффициенты a, by с, А из уравнения плоскости ABC. Напомним, что видовые координаты вершины А записаны в элемент массива VERTEXIA ] и так далее. В зависимости от контекста термин "треугольник ABC" означает либо исходный треугольник в трех- мерном пространстве, либо его проекцию на экран. Будем, на- пример, говорить, что сторона треугольника АВ на рис. 5.13 ле- жит в пикселных колонках 5, 6, 7. Это несколько упростит изло- жение, поскольку в противном случае пришлось бы говорить о центральной проекции А'В' исходной стороны треугольника АВ. В списках пикселов будут указываться только значения индек- сов у, обозначающих треугольники, необходимые параметры ко- торых могут быть вызваны из TRIANGLE \j]. Будем говорить, что треугольник, частично или полностью покрывающий пик- сел, ассоциирован с этим пикселом. После того, как будут сформированы все пикселные списки, можно начать вычерчивание отрезков прямых линий. Для каж- дого отрезка PQ начнем с построения наборов всех треугольни- ков, ассоциированных с пикселами, содержащими точки отрезка PQ. На рис. 5.13 такие пикселы заштрихованы в колонках 0, 1, 2. Затем отрезок PQ проверяется на видимость только относитель- но треугольников из этого набора. На первый взгляд кажется, что реализация такой простой идеи потребует очень большой до- полнительной работы, так как, во-первых, все треугольники должны быть записаны в пикселных списках и, во-вторых, про- верке на видимость каждого отрезка прямой линии должно пред- шествовать формирование набора треугольников, которые могут его закрыть. Но дополнительная работа для этих действий зави- сит только линейно от числа треугольников и отрезков. Поэтому для этого способа ожидается линейная зависимость сложности.
148 Глава 5. УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ В параграфе 5.3 координаты области вывода вычислялись не программой HIDLIN, а постпроцессором GENPLOT. Последняя программа сначала определяет минимальные и максимальные значения координат (считываемых из файла A.SCRATCH) и за- тем использует их для вычисления коэффициента масштабиро- вания/и параметров переноса с\ и с2, как было описано в пара- графе 2.6. Сейчас предпочтительнее перенести эти действия в нашу новую программу HIDLINPX. В любом случае нам потре- буются максимальные и минимальные значения координат, по- скольку иначе нельзя будет ассоциировать точки на экране с пикселами. И будет напрасная трата времени, если эту работу выполнять дважды. Внутри программы будут использованы эк- ранные координаты X и У, вычисленные путем "чистых" перс- пективных преобразований в соответствии с уравнениями (4.10) и (4.11), что означает размещение плоскости экрана на расстоя- нии 1 от точки наблюдения Е. Экранные координаты всех точек объектов должны быть вы- числены как можно раньше, чтобы обеспечить возможность вы- числения их минимальных и максимальных значений Xmin, Хтах, Ymin, Ymax. Полученные таким образом размеры проек- ции на экран не следует путать с областью вывода, в которую впоследствии будет отображаться картинка. Соответствие между размером проекции и областью вывода устанавливается по коор- динатам границ области вывода Xvpjnin, Xvpjnax, Yvpjnin, Yvpjnax, которые запрашиваются у пользователя во время вы- полнения программы. Приводимый ниже фрагмент программы тесно связан с материалом, изложенным в параграфе 2.6, и пока- зывает, что фактически вычисляется: Xrange = Хтах - Xmin; Yrange = Ymax- Ymin\ Xvpjrange = Xvpjnax - Xvpjnin\ Yvp_jange= Yvpjnax- Yvpjnin; fx = XvpjrangelXrange\ fy= Yvpjrangel Yrange\ f=(fx<fy?fx:fy); Xcentre = 0.5 * (Xmin + Xmax); Ycentre = 0.5 * (Ymin + Ymax); Xvpjoentre = 0.5 * (Xvpjnin + Xvpjnax); Yvpjcentre = 0.5 * (Yvpjnin + Yvpjnax);
5.4. ПОЛИГОНЫ И ПИКСЕЛЫ 149 с\ = Xvpjoentre -/* X_centre\ с2 = Yvpjcentre-f* Yjcentre; Затем можно вычислить видовые координаты Xvp и Yvp на осно- вании значений экранных координат X и У следующим образом: Xvp=f*X + cl; Уур=/*У+с2; Интересно сравнить эти две формулы с параграфом 4.3, где утверждалось, что коэффициент масштабирования есть просто расстояние d между точкой наблюдения Е и плоскостью, в кото- рой можем представить область вывода. Экран и область вывода располагаются в двух параллельных плоскостях (обе перпенди- кулярны к направлению линии наблюдения ЕО), причем первая плоскость находится на расстоянии 1, а вторая — на расстоянии d =/от точки наблюдения Е. Координаты точек в области вывода будут использоваться только при вызовах действительных "команд черчения", то есть в обращениях к функциям move и draw. Внутри программы будут применяться только экранные координаты, которые лежат в пре- делах вычисленных границ Xmin, Хтах, Ymin, Ymax. И именно эта область будет разделена на Nscreen * Nscreen пикселов. Об- судим теперь взаимосвязь между X и ipix — номером столбца пикселов. Взаимосвязь между У и номером строки jpix будет аналогичной. Как видно из рис. 5.13, мы имеем Xmin<= X <=Xmax О <= ipix <= Nscreen - 1 Теоретически горизонтальный размер пиксела равен deltaX = {Xmax - Xmin) /Nscreen (Скоро мы увидим, что практически полезно несколько скоррек- тировать это уравнение.) Тогда число ipix получается путем усе- чения значения частного (X-Xmin)/deltaX до целого числа. Результат вычисления по этой формуле имеет неприятное следствие, которое заключается в том, что значение X = Хтах соответствует ipix = Nscreen, а это превышает макси- мально допустимое значение Nscreen - 1. Одним из решений мог ло бы быть расширение экрана на один столбец (и на одну строку), который использовался бы для точек столбца X = Хтах (и строки У т Ymax). По причине экономии такое решение не
150 Глава 5. УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ рационально, и поэтому будем настаивать на применении только N screen столбцов пикселов с номерами от 0 до N screen - 1. Это легко получить путем небольшого увеличения значения пара- метра deltaX. Умножим его на величину 1 + epsilon, где констан- те epsilon приписано небольшое положительное значение, ска- жем 1(Г . Теперь, поскольку значения Xmin, Хтах, Ymin, Ymax уже определены, вычислим deltaX = (1 + epsilon) * Xrange/Nscreen; deltaY= (1 + epsilon) * Yrange IN screen; Каждый раз при необходимости определить пиксел, ассоции- рованный с точкой (X, У), его позиция вычисляется операторами ipix =(X- Xmin) /deltaX; jpix = (Y-Ymin)/ delta Y\ Заметим, что отсечение дробной части в языке Си выполняет- ся неявно при любом присвоении значения с плавающей точкой целочисленной переменной. Поэтому вполне достаточно объя- вить тип int для переменных ipix и jpix. Может также потребоваться и обратная операция X = Xmin + ipix * deltaX; Так будет найдено значение X, соответствующее левой гра- нице колонки пикселов с номером ipix. Экранные координаты X и Уточки в центре пиксела (ipix, jpix) вычисляются операторами X = Xmin + (ipix + 0.5) * deltaX; Y= Ymin + (jpix + 0.5) * deltaY; Посмотрим теперь, как можно использовать пикселы для хра- нения информации о треугольниках, с которыми они ассоцииро- ваны. Будем использовать линейный список узлов, а каждый узел содержит номер треугольника #г и указатель на следующей узел, если он существует. Если нет, то поле указателя будет иметь значение NULL. Начальный указатель каждого списка по- мещен в двухмерный массив SCREEN. Пиксел (ipix, jpix) соот- ветствует элементу SCREEN [ipix ] \jpix ]. Ради эффективности некоторым из треугольников, ассоции- рованных с пикселом, будет дана специальная трактовка. К этим специальным треугольникам отнесем те, которые полностью покрывают пиксел. (Треугольник полностью покрывает пиксел, если он весь лежит внутри треугольника.) Для данного пиксела (ipix, jpix) все эти специальные треугольники могут быть проиг-
5.4. ПОЛИГОНЫ И ПИКСЕЛЫ 151 норированы, за исключением только одного, который располо- жен ближе всех к точке наблюдения! Здесь термины "близкий" и "расстояние до треугольника" относятся к точке в трехмерном пространстве, в которой прямая линия, проходящая через точку Е и центр пиксела, пересекает плоскость треугольника. Если есть треугольники, полностью покрывающие пиксел, то номер, идентифицирующий ближний из них, также будет сохранен в массиве SCREEN [ipix ] [jpix ] вместе с расстоянием до этого тре- угольника. Поля для этих двух новых элементов называются trjoov и trjiist. На рис. 5.14 показан типичный набор треуголь- ников, ассоциированных с пикселом. В процессе запоминания треугольников пиксел может иметь структуру данных, показан- ную на рис. 5.15. Только треугольники 18 и 23 покрывают пиксел полностью. Поскольку треугольник 18 является ближайшим к глазу, то его номер и расстояние до него записываются в элемен- ты массива SCREEN. Треугольники 3 и 5 не полностью закрыва- ют пиксел, следовательно, расстояние до них не существенно на данном этапе. В процессе запоминания треугольников ближай- ший треугольник, с номером 18 в данном примере, записывается в линейный список не сразу, поскольку впоследствии может об- наружиться другой закрывающий треугольник, более близкий. Ближайший закрывающий треугольник добавляется в список в Рис. 5.14. Пиксел и ассоциированные треугольники
152 Глава 5. УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ Линейный список SCREEN[iplx][jpix] trxov tr.dist start jtr next jtr next ПиаГ 257 5 3 NULL Рис. 5.15. Структура данных для пиксела самом конце процесса. После окончания этого процесса список для каждого пиксела будет содержать все треугольники, ассоци- ированные с пикселом. Теперь нужно разобраться с другой интересной проблемой программирования. Для данного треугольника ABC с экранными координатами вершин ХА, YA, XB, YB, ХС и YC необходимо найти, какой пиксел с ним ассоциирован и какие из них, если есть таковые, полностью покрываются этим треугольником. Эта проблема совсем не тривиальная, поскольку необходимо рас- смотреть очень много ситуаций. Первая из подзадач заключает- ся в поиске одной стороны треугольника, являющейся его грани- цей сверху или снизу. Припишем номера 0, 1, 2 сторонам треу- гольника АВ, АС, ВС, соответственно, как показано на рис. 5.16. Здесь получилось так, что сторона 0 расположена сверху, а сто- роны 1 и 2 — снизу. Нам нужно закодировать эту ситуацию и за- нести соответствующие коды в массив topcode, содержащий три элемента topcode [0 ] = 1 topcode [1 ] = 0 topcode [2] = О то есть 1 и 0 являются кодами для положения сверху и снизу, со- ответственно. Для аналитического определения этих значений используем точку, лежащую далеко внизу. Обозначим экранные координаты точки V как (О, М), где, например, М = -1(Г. Тогда topcode [0 ] = 1, если, и только если, обе точки С и V лежат по од- ну сторону от прямой линии А. Последнее утверждение легко проверить путем подстановки координат точек С и V в левую часть уравнения, описывающего прямую АВ: |*А УК Ц хв Ув Ц =0 •* У Ч
5.4. ПОЛИГОНЫ И ПИКСЕЛЫ 153 2 С Рис. 5.16. Нумерация сторон Если точки С и V дают одинаковые знаки при такой подста- новке, то они находятся по одну и ту же сторону от прямой А, а это означает, что данная сторона находится сверху. Запись на языке Си получается очень краткая: topcode[0]=(D *DAB >0); где через D'n DAB обозначены детерминанты £> = "ав = 1 ИГЛ Я ТОТ dac° Явс = *А *в хс ХА ХВ ТЯ IT *А 0 ХС 0 *в хс Уа Ув Ус УА Ув м етер1 Ук м Ус м Ув Ус 1 1 1 1 1 1 МИ 1 1 1 1 1 1 после чего получаем остальные элементы массива topcode[l]=(D *DAC>0); topcode[2]= (D +DBO0); Следующим шагом будет определение координат концевых точек слева (Xleft[l ], Yleft[\ ]) и справа (Xright[\ ], Yright[\ ]) для каждой из сторон / (/=0, 1, 2). Так, для рис. 5.16 будем иметь Xleft [0 ] = ХВ, Yleft [0 ] - YB и так далее.
154 Глава 5. УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ Найдем номера ipixmin и ipixmax колонок пикселов, в кото- рых расположены самые крайние левая и правая вершины треу- гольника ABC. После этого для треугольника ABC можно скор- ректировать список пикселов, ограничив его только пикселами в диапазоне номеров столбцов от ipixmin до ipixmax. Для каждого столбца ipix в этом диапазоне введем границы по строкам LOWER[ipix] и UPPER[ipix]. Все пикселы в диапазоне LOWER [ipix], ..., UPPER[ipix] ассоциируются с треугольни- ком. Этот диапазон включает (возможно пустой) поддиапазон LQW[ipix]+ 1,..., UP[ipix}-l. Все пикселы в этом поддиапазоне полностью покрываются треугольником. На рис. 5.17 показана сторона с номером 1 треугольника ABC. Предположим, что эта сторона — нижняя граница, то есть для нее topcode[\ ]в 0, тогда эта сторона треугольника позволит определить значения LOWER [ipix ] и LOW[ipix ] для значений ipix в диапазоне ipixleft, ..., ipixright y где целые числа ipixleft и ipixright получаются в ре- зультате усечения частного при вычислении ipixleft = (Xleft [I ] - Xmin) /deltaX; ipixrigte (Xright [I ] - Xmin) /deltaX; Вычислим наклон сторон треугольника slope = {Yright[l]- Yleft[l])/(Xright[l]-Xleft[l]); ' (В отличие от двух предыдущих вычислений здесь отсечения не будет, поскольку переменная slope имеет тип переменной с плавающей точкой.) Предположим, что при последовательных вычислениях от столбца ipixleft до столбца ipixright рассматрива- ется столбец ipix. Определим номер строки // для точки I на рис. Пиксел полностью закрыт (XlefUILYIefHU) (Xright[t],Yright[l]) ipixleft ipix ipixright Рис. 5.17. Сторона треугольника и столбцы пикселов
5.4. ПОЛИГОНЫ И ПИКСЕЛЫ 155 5.1 7, в которой сторона треугольника пересекает границу между столбцами пикселов ipix и ipix + 1. Этот номер строки можно най- ти по значениям экранных координат (XI', YD точки пересече- ния!: XI = Xmin + (ipix+l) * deltaX; YI = Yleft[l] +slope* (XI-Xleft[l]); Д= (У7- Ymin) I deltaY; I* implicitly truncated */ /* неявное отсечение */ Пусть jjold будет старое значение //, вычисленное для столб- ца с номером ipix - 1. Тогда имеем LOWER [ipix ] = min(j_old, //); LOW[ipix ] - max(jjold, jl); где функция min определяет минимальное значение одного из двух аргументов, а функция max — максимальное значение. На рис. 5.17 имеем LOWER[ipix] = 3, LOW[ipix] - 4. Для столбца пикселов ipixleft припишем усеченное частное от деления (Yleft[l] - Ymin)/ deltaY, а если это номер строки для крайней левой конечной точки, то jjold. Аналогично усеченный номер строки (Yright[l] ~ Ymin)IdeltaY самой правой конечной точки берется в столбце на самом краю справа, ipixright Описанный способ не всегда дает правильный результат. Так, если на рис. 5.18 стороны 0 и 1 рассматривать именно в этом порядке, то получим неверный результат: LOWER [ipix] - 6, поскольку точ- ное значение 5, полученное при анализе стороны 0, будет затер- 1\ 7 6 5 у 'О ipix Рис. 5.18. Значение LOWER[ipix] зависит от двух сторон треугольника
156 Глава 5. УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ то результатом анализа стороны 1. Поэтому вначале присвоим всем элементам массива LOWER очень большие значения и раз- решим только уменьшать их. Также всем элементам массива LOW должны быть присвоены очень маленькие значения, кото- рые затем можно только увеличивать. Зйачения массивов UPPER и UP определяются аналогично. Теперь для треугольника ABC (данные для которого зафикси- рованы в массиве TRIANGLE'[/]) определены диапазоны столб- цов ipixmin, ..., ipixmax и элементы массивов LOWER [ipix], UPPER [ipix ], LOW[ipix ], UP[ipix ] (ipixmin < ipix < ipixmax), по- этому можно скорректировать матрицу SCREEN и линейный список. Если диапазон номеров пикселов в столбце ipix LOW[ipix]+ly...,UP[ipix]-l не пуст и jpix определяет номер строки в этом диапазоне, то пик- сел (ipix, jpix) полностью закрывается треугольником ABC. За- тем исследуем точку R*, в которой прямая линия, проходящая через точку наблюдения Е и центр R пиксела, пересекает треу- гольник ABC (см. рис. 5.19). Напомним, что плоскость ABC рас- положена на расстоянии h от точки Е и имеет вектор нормали п - [а Ъ с]. Числа а, Ъ, с, А хранятся в массиве TRIANGLElj]. Ком- понента вектора ER в направлении вектора нормали п равна EQ. Следовательно, длина вектора EQ может быть вычислена из ска- лярного произведения EQ = n • ER = [а Ъ с] • [xR yR l] = axR + byR + с Рис. 5.19. Расстояния в треугольнике
5.5. УЛУЧШЕННАЯ ПРОГРАММА 157 Поскольку треугольники ERQ и ER*Q* подобны, то имеем ER*^ ER EQ? EQ Следовательно, _Б£_£Л_ EQ. axR + byR + c В поля tr_cov всех элементов матрицы SCREEN [ipix] [jpix] первоначально записаны значения -1, а всем полям tr dist при- своены начальные значения 10 . Для каждого пиксела (ipix, jpix), который полностью покрывается треугольником /, эти два поля изменяются, если значение ER* будет меньше записанного в trjlist. В этом случае новое расстояние до треугольника ER* за- писывается в поле trjlist, а значение/— в поле trjcov. 5.5. УЛУЧШЕННАЯ ПРОГРАММА В нашей программе HIDLINPX большинство новых аспектов реализовано в функции counterjclock. Это очень сложная функ- ция. Здесь уместно напомнить, что главное различие между на- шей предыдущей программой HIDLIN и этой новой программой заключается в концепции полигонов и пикселов. Декомпозиция полигона на треугольники выполняется в основном так же, как в параграфе 3.5. Каждый раз, когда заканчивается считывание описания полигона, выполняется проверка, не является ли он за- дней гранью. Этот тест основывается на проверке треугольника, образуемого первыми тремя вершинами полигона. Тест подобен аналогичному тесту в программе HIDLIN и выполняется в функ- ции counterjzlock (отсюда и ее название — "против_часо- вой_стрелки"). Если первый треугольник ориентирован как за- дняя грань, то весь полигон игнорируется. В противном случае полигон не смотрит назад и придется определять ориентацию трех соседних вершин многократно. Таким же образом, как и в параграфе 3.5, находим треугольник, который должен быть отре- зан от полигона. Попутно определяется длина диагонали, кото- рая возвращается в программу через четвертый аргумент функ- ции. Наконец, пятый аргумент является кодом, который сообща-
158 Глава 5. УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ ет функции, что от нее хотят иметь. В тексте программы содер- жатся все необходимые подробности и нет смысла их здесь повто- рять. Если полигон не задний, то в память должны быть записаны не только все треугольники, но и все ребра полигона, поскольку они являются отрезками прямых линий, которые, возможно, должны быть вычерчены, если они окажутся видимыми. Теперь предположим, что входными данными определены два полигона PQRS и ABCQP и ни один из них не задний. Поскольку эти пол- игоны имеют одно общее ребро PQ, то этот отрезок будет записан в память дважды, если не предусматривать никаких мер против этого. С целью повышения эффективности необходимо предус- мотреть условия, чтобы каждый такой отрезок PQ записывался только однажды. Это означает, что, прежде чем записывать от- резок PQ в память, следует проверить список записанных отрез- ков и, в случае если он уже имеется в этом списке, второй раз его не записывать. Все это должно быть выполнено эффективно с точки зрения затрат времени и занимаемой памяти, поскольку обычно вводится довольно большое количество отрезков. Есть несколько способов реализации такой операции. Вместо поиска желательно использовать один из номеров вершин Р и Q, допу- стим, меньший из них, в качестве индекса массива. Как и в про- грамме HI DUN, у нас есть массив VERTEX, в который записы- ваются прямоугольные координаты каждой вершины. Теперь до- полним каждый элемент этого массива указателем на целое число. Фактически он будет указывать на первое из последова- тельности целых чисел. В языке Си имеются функции malloc и reallocy которые позволяют динамически отводить пространство памяти очень гибко и только на то время, когда оно действитель- но нужно. Поэтому здесь особенно полезной может оказаться функция realloc. Предположим, например, что заданы следую- щие пары номеров вершин именно в таком порядке, причем каж- дая пара обозначает отрезок, который нужно запомнить, если это еще не сделано 0 2 1 3 1 О О 1 3 О
5.5. УЛУЧШЕННАЯ ПРОГРАММА 159 Эта информация запоминается в следующем виде: i VERTEX[i] х у z connect 0 ).. .!. J. — 3 2 13 1 —^13 2 — О 3 — О Поле connect С соединение'') элемента массива VERTEX [0 ], например, указывает на последовательность 3, 2, 1, 3. Первое число 3 говорит о том, что далее следуют три номера вершин, а именно для отрезков (0, 2), (0, 1) и (0, 3). На языке Си можно записать ptr = VERTEX [О Iconnect; n = *ptr; В этом примере имеем п = 3 и *(р/г+1)-2 *(р*г + 2) = 1 ф(р*г + 3)=3 Заметим, что при необходимости записи в память отрезка (3 0) ради единообразия такая последовательность номеров заменя- ется на (0 3). Если такой последовательности еще нет, то она за- писывается в массив, начиная с элемента VERTEX[0], вместо элемента VERTEX[3]. Поиск каждого отрезка, который нужно записать в память, теперь обычно ограничен очень малой после- довательностью, поэтому алгоритм работает очень быстро. По- скольку память отводится только в том объеме, который необхо- дим, то этот способ экономичен и в отношении памяти. В про- грамме эти операции выполняются в функции addjinesegment. Во многих отношениях программа HIDUNPX аналогична программе HIDUN из параграфа 5.3. Это особенно касается ос- новной части программы, а именно функции linesegment. В про- грамме HIDLIN она должна была проверять взаимосвязь каждо- го заданного отрезка со всеми треугольниками. В программе HIDLINPX набор треугольников, подлежащих анализу, обычно значительно меньше. Перед обращением к функции linesegment основная программа формирует этот набор на основе данного от- резка PQ и линейного списка, начинающегося в матрице SCREEN, Это выполняется в два этапа. На первом опять исполь- зуются массивы LOWER и UPPER, но теперь для обозначения пикселов, через которые проходит отрезок PQ. Это реализуется
160 Глава 5. УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ аналогичным способом, как и для треугольников, так что нет не- обходимости описывать подробно. На втором этапе выбранные таким образом пикселы используются для нахождения ассоции- рованных треугольников. Если отрезок PQ проходит через пик- сел (ipix, jpix), то треугольники из линейного списка, начинаю- щегося в SCREEN[ipix] [jpix], добавляются к создаваемому на- бору треугольников. Здесь используется термин "набор", чтобы подчеркнуть, что массив trset будет содержать номер каждого треугольника не более одного раза. Предположим, что список trset[0]у..., trset[ntrset- 1 ] является последовательностью треу- гольников, которая уже составлена (вначале ntrset = 0). Новый номер треугольника trnr должен быть добавлен в эту последова- тельность только в том случае, если его там еще нет. Это делает- ся путем предварительного занесения его в самый конец списка trset [ntrset ] = trnr; как своего рода стража. Затем проверяется ^ся последователь- ность на наличие в ней номера trnr, начиная с элемента trset [0 ]. Поскольку этот номер обязательно будет найден, то необходимо проверять только на равенство и этим заканчивать цикл. После выхода из цикла нужно проверить выполнение условия jtr - ntrset. Если условие выполнено, то это означает, что jtr до этого не встречался в последовательности, поэтому нужно увеличить число ntrset. Если jtr < ntrset, то номер треугольника уже присут- ствует в последовательности и число ntrset не увеличивается, то есть фактически "страж" не добавляется в набор. Ниже приводится полный текст программы HIDLINPX: /* HIDLINPX: Программа удаления невидимых линий */ /* на основе приборно-независимых пикселов. */ /* A program for hidden-line elimination, */ /* using device-independent pixels. */ #include<stdio.h> #include<math.h> #include<ctype.h> #define max2(x,y) ((xMy)?(x):(y)) #define min2(x,y) ((x)<(y)?(x):(y)) #define max3(x,y,z) ((x) > (y) ? max2(x,z): max2(y,z)) #define min3(x,y,z) ((x) < (y) ? mln2(x,z): min2(y,z)) #define xwhole(x) ((lntX((x]hXmin)/deltaX)) #define ywhole(y) ((intX((y)-Ymin)/deltaY)) #define xreal(i) (Xmln+((l))*deltaX)
5.5. УЛУЧШЕННАЯ ПРОГРАММА 161 #define M -1000000.0 #define nvertex 300 /* maximum number of vertices */ /* максимальное число вершин */ #define ntriangle 200 /* maximum number of (no backface) triangles to be stored */ /* максимальное количество треугольников, подлежащих */ /* запоминанию (без невидимых) */ #define Nscreen 15 #define big 1.e30; #defineNPOLY400 /* maximum number of vertices of a single polygon */ /* максимальное количество вершин в одном полигоне */ #define nntrset 200 /* maximum size of set of triangles associated */ /* with a single (long) line segment */ /* максимальная величина набора треугольников, */ /* ассоциируемых с одним (длинным) отрезком */ int ntr-O, iaux, ipixmin, ipixmax, ipixleft, ipixright, ipix, jpix, Jtop. Jbot. j_old. I. jl, topcode[3], POLY[NPOLY], - npoly.isize-sizeof(int), LOWER[Nscreen]. UPPER[Nscreen], LOW[Nscreen], UP[Nscreen], trset[nntrset], ntrset; double v11, v12, v13, v21, v22, v23, v32. v33, v43, d, d, c2. eps-1e-5, meps—1e-5, oneminus-1-1.e-5, oneplus-1+1.e-5, Xrange, Yrange, Xvprange, Yvprange, Xmin, Xmax, Ymin, Ymax, deltaX, deltaY, denom, slope, Xleft[3], Xright[3], Yleft[3], Yright[3]; char *malloc(), *realloc(); struct { double x, y, z; int ^connect; } VERTEX [nvertex], *p vertex; struct { int А, В, С; double a, b, c, h; } TRIANGLE [ntriangle], *ptriangle; struct node { int jtr; struct node *next; } *pnode; struct { int trcov; double tr_dist; struct node *start; } SCREEN[Nscreen] [Nscreen], *pointer; FILE *fpin; /* V main(argc, argv) int argc; char *argv[ ]; { int i, P, Q, ii, imin, verte*nr, *ptr, iconnect, Ю, i1, i2, code, count, trnr, jtr; double xO, yO, zO, rho, theta, phi, x, y, z, X, Y, xe, ye, ze, diag. min_diag, Xvpmin, Xvpmax, Yvp_min, Yvp_max, fx, fy, Xcentre, Ycentre, Xvp_centre, Yvp_centre, xP, yP, zP, xQ, yQ, zQ, XP, YP, XQ, YQ, Xlft. Xrght, Ylft, Yrght; 6 — 271
162 Глава 5. УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ char ch; If (argc!-2 I I (fpin-fopen(argv[1], "r"))--NULL) { prlntf /* "Input file not correctly specified" */ ("Входной файл указан некорректно\п"); exlt(1); } /* Initialize screen matrix */ /* Инициирование экранной матрицы */ for(lplx-0; lplx<Nscreen; lplx-н-) for (jpix-O; Jplx<Nscreen; Jplx-нк) { polnter-&(SCREEN[lplx] [Jplx]); po!nter->tr_cov—1; polnter->tr_dlst-big; polnter->start-NULL; } reflo(&xO); reflo(&yO); reflo(&zO); prlntf /* Give spherical coordinates rho, theta, phi of */ /* viewpoint E (phi - angle between z-axis and OE) */ ("Задайте сферические координаты rho, theta, phi"); prlntf("^fl точки наблюдения E\n(phl-угол между"); printf(" осью z и направлением отрезка ОЕ):\п"); scanf("%lf %lf %|Г. &rho, &theta, &phl); coeff(rho, theta, phi); init_viewport(&Xvp_mln, &Xvp_max, &Yvp_mln, &Yvp_max); /* Initialize vertex array */ /* Инициирование массива вершин */ for(l-0; Knvertex; i++) VERTEX[l].connect-NULL; /* Read vertices */ /* Считывание координат вершин */ Xmln-Ymln-blg; Xmax-Ymax—big; while (skipbl(), ch-getc(fpln), chKF' && chKf) { ungetc(ch, fpln); relnt(&l); reflo(&x); reflo(&y); reflo(&z); If (КО 11 l>-nvertex) errmes /* illegal vertex number */ ("Номер вершины отрицательный или больше максимального (300)"); vlewlng(x-xO, y-yO, z-zO, &xe, &ye, &ze); If (ze <- eps) errmes /* "Object point О and a vertex on different sides of viewpoint E */ ("Точка объекта О и вершина с разных сторон от точки наблюдения Е"] X-xe/ze; Y-ye/ze; if (X<Xmin) Xmin-X; if (X>Xmax) Xmax-X; if (Y<Ymin) Ymln-Y; if (Y>Ymax) Ymax-Y; VERTEX[l].x-xe; VERTEX[i].y-ye; VERTEX[i].z-ze; VERTEX[i].connect - ptr- (int*)malloc(isize); if (ptr--NULL) errmes /* "Memory allocation error 1" */ ("Ошибка 1 распределения памяти"); *ptr-0; } /* Compute screen constants */ /* Вычисление экранных констант */ Xrange-Xmax-Xmin; Yrange-Ymax-Ymin; Xvprange-Xvpmax-Xvpmin; Yvp_range-Yvp_max-Yvp_min; fx-Xvp_range/Xrange; fy-Yvp_range/Yrange;
5.5. УЛУЧШЕННАЯ ПРОГРАММА 163 d-<fx<fy ? fx : fy); Xcentre-0.5*(Xmin+Xmax); Ycentre-0.5*(Ymin+Ymax); Xvp_centre-0.5*(Xvp_min+Xvp_max); Yvp_centre-0.5*(Yvp_min+Yvp_max); с 1-Xvp_centre-d*Xcentr/e; c2-Yvp_centre-d* Ycentre; deltaX-oneplus*Xrange/Nscreen; delta Y-oneplus*Yrange/Nscreen; /* Now we have: Xrange/deltpX < Nscreen */ /* Теперь имеем: Xrange/deltaX < Nscreen */ /* Read object faces and store triangles */ /* Считывание описаний граней и запись треугольников */ while (! isspace(getc(fpin))); /* The string "Faces:" has now been skipped */ /* Строка со словом "Faces:" будет пропущена */ while (reint(&l)X)) { POLY[0>i; npoly-1; skipbl(); while (ch-getc(fpin), ch !- '#') { ungetc(ch, fpin); reint(&POLY[npoly-H-]); if(npoly--NPOLY) errmes /* "Too many vertices in one polygon" */ ("Слишком много вершин в одном полигоне"); } if (npoly- -1) errmes /* "Only one vertex of polygon" */ ("Полигон имеет только одну вершину"); if (npoly--2) { addJinesegment(POLY[0], POLY[1]); continue; } if (!counter_clock(0, 1, 2, &diag, 0)) continue; /* Задняя грань */ /* backface */ fot(i-1;i<-npoly; I++) { il-i%npoly; code-POLY[il]; vertexnr-ebs(code); if (VERTEX[vertexnr].connect- -NULL) errmes /* "Undefined vertex number used" */ ("Использован номер неопределенной вершины"); if (code<0) POLY[ii]-vertexnr; else add_linesegment(POLY[i-1], vertexnr); } /* Division of a polygon into triangles (see Section 3.5): */ /* Рёзбиение полигона на треугольники (см. параграф 3.5): */ count-»!; while (npoly>2) { mindiag-big; for(i1-0;IKnpoly;i1-H-) { Ю-(И--0? npoly-1: M-1); i2-(M--npoly-1?0:H+1); 6**
164 Глава 5. УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ if (counter_clock(IO, И, i2, &diag.O) && diag<min_diag) { min_diag-diag: imin-И; } > i1-jmin; iO-(M--0? npoly-1 :M-1); i2-(i1--npoly-1?0:M+1); /* store triangle in array TRIANGLE and in screen lists: */ /* Запоминание треугольника в массиве TRIANGLE */ /* и в экранных списках: */ counter_clock(iO, II. i2. &diag, count-н-); npoly—; for(iH1; ii<-npoly; ii++) POLY[ii}-POLY[ii+1]; } } fclose(fpin); /* Add nearest triangles to screen lists: */ /* Добавление ближайшего треугольника в экранные списки*/ for(ipixK); ipix<Nscreen; ipix++) for(|pix-0; jpix<Nscreen; Jpix++) { pointer-&(SCREEN[ipix] [jpix]); if ((*pointer).tr_cov >- 0) { pnode-^struct node *)malloc(sizeof(struct node)): if (pnode- -NULL) errmes /* "Memory allocation error 2" */ ("Ошибка 2 распределения памяти"): pnode->jtr-pointer->tr_cov; pnode->next-pointer->start: pointer->start-pnode: } } /* Draw all line segments as far as they are visible */ /* Вычерчивание отрезков прямых линий при их видимости */ for (P-0; P<nvertex: Р++) { pvertex-VERTEX+P; /* - &VERTEX[P] */ ptr - pvertex->connect: if (ptr - - NULL) continue: xP - pvertex->x: yP - pvertex->y: zP - pvertex->z: XP-xP/zP:YP-yP/zP: for(iconnect-1: lconnect<-*ptr, iconnect++) { Q - *(ptr+iconnect): pvertex-VERTEX+Q: /* - &VERTEX[Q] */ xQ - pvertex->x: yQ - pvertex->y: zQ - pvertex->z: XQ-xQ/zQ: YOyQ/zQ: /* Using the screen lists, we shall build the */ /* set of triangles that may hide points of PQ: */ /* На основе экранных списков определим набор треугольников, */ /* которые могут закрывать точки отрезка прямой линии PQ: */
5.5. УЛУЧШЕННАЯ ПРОГРАММА 165 if (XP<XQ I I (XP--XQ && YP<YQ)) { Xlft-XP; Ylft-YP; Xrght-XQ; Yrght-YQ; } else { Xlft-XQ; Ylft-YQ; Xrght-XP; Yrght-YP; } ipixleft-xwhole(Xlft); jpixright-xwhole(Xrght); denom-Xrght-XIft; if (fabs(denom)<-eps) denom-eps; slope-(Yrght-Ylft)/denom; Jbot-jtop-ywhole(Ylft); for(ipix-ipixleft; ipix<-ipixright; iplx++) { if (ipix- Hpixright) JI-ywhole(Yrght); else JI-ywhole(Ylft-Kxreal(iplx+1>-Xlft)*slope); LOWER[lplx}-mln2Cbot,JI); Jbot-JI; UPPER[ipixj-max2(jtop,JI); jtop-jl; } ntrset-0; for(lpix-iplxleft; ipix<-lpixright; iplx++) for (Jpix-LOWER[lpix]; Jpix<-UPPER[ipix]; jpix++) { pointer-&(SCREEN[lplx] [Jpix]); pnode- polnter->start; while (pnodel-NULL) { trnr- pnode->Jtr; /* trnr will be stored only if it is not yet */ /* present In array trset (the triangle set) */ /* Номер trnr будет запомнен только в том случае, если */ /* его еще нет в массиве trset (набор треугольников) */ trset[ntrsetHrnr. /* sentinel */ /* "страж" */ Jtr-0; while (trset[Jtr]!-trnr)Jtr-H-; if (jtr- -ntrset) { ntrset++; /* this means that trnr is stored */ /* это означает, что trnr записано в массив */ if (ntrset- -n ntrset) errmes /* "Triangle set overflow" */ ("Набор треугольников переполнен\пм); pnode-pnode->next; } } /* Now trset[0] trset[ntrset-1] Is the set of */ /* triangles that may hide points of PQ. V /* Теперь список trset[0] trset[ntrset-1] определяет */ /* набор треугольников, которые могут закрывать точки PQV llnesegmentfrP. yP, zP, xQ, yQ, zQ, 0); } } endgr(); }
166 Глава 5. УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ sklpbl() { charch; do ch-getc(fpin); whlle(isspace(ch)l lcomment(ch)); ungetc(ch,fpin); } int comment(ch) char ch; { intk; If(ch--T) { do k-getc(fpln); while (k !- ')' && к !- EOF); return k- -')", } else return 0; /* Int reflo(px) double *px; { sklpbl(); return fscanf(fpin, "%1Г, рх); } /* Int relnt(pl) Int *pi; { sklpbl(); return fscanf(fpln, "%d", pl); } -add_Hnesegment(P, Q) Int P, Q; { Int laux, *ptr, II, n; If (РХЭ) { laux-P; P-Q; Q-laux; } /* Now: P < Q */ ptr-VERTEX[P].connect; n-*ptr; for(ll-1;IK-n;ll++) If (*(ptr+ll>- -Q) return; /* Q already In list */ /* Q уже в списке */ n++; VERTEX[P].connect-ptr-(lnt *)realloc(ptr, (n+1)*lslze); If (ptr- -NULL) errmes /* "Memory allocation error 3" */ ("Ошибка З распределения памяти"); *(ptr*n)-Q; *ptr-n; } /* Int counter_clock(IO, 11,12, pdlst, code) Int 10,11, i2, code; double *pdlst; /* code - 0: compute orientation; If counter-clockwise, /* compute length of projected diagonal AC /* code - 1: compute a, b, c, h; store the first triangle /* code > 1: check If next triangle is coplanar; store it /* /* code - 0: ориентация : если против часовой стрелки — /* вычисление длины проекции диагонали АС
5.5. УЛУЧШЕННАЯ ПРОГРАММА 167 pvertex->y; zB - pvertex->z; /* code - 1: вычисление a, b, c, h; запоминание первого V /* треугольника */ /* code > 1: проверка на компланарность следующего V /* треугольника; запись его в память */ { Int A-abs(POLY[IO]), B-abs(POLY[i1]), C-abs(POLY[l2]); double xA, yA, zA, xB, yB, zB, xC, yC, zC, r, xdist, ydist, zdlst, XA, YA, XB, YB, XC, YC, hO, DA, DB, DC, D, DAB, DAC, DBC, aux, dlst, xR, yR; static double a, b, c, h; pvertex-VERTEX+A; xA - pvertex->x; yA - pvertex->y; zA - pvertex->z; pvertex-VERTEX+B; xB - pvertex->x; у В - pvertex-VERTEX+C; xC - pvertex->x; yC - pvertex->y; zC - pvertex->z; hO-xA*(yB*zC-yC*zB)- xB*(yA*zC-yC*zA) + xC*(yA*zB-yB*zA); If (code--0) If (hO>eps) { xdlst-xC-xA; ydlst-yC-yA; zdlst-zC-zA; *pdist-xdist*xdist+ydist*ydist+zdlst*zdist; return 1; } else return 0; /* If hO-0. plane ABC passes through E and hides nothing. */ /* If h(X0, triangle ABC is a backface. */ /* In both cases ntr Is not incremented and the triangles */ /* of the polygon are not stored. */ /* */. /* Если hO-0, то плоскость ABC проходит через точку Ей */ /* ничего не закрывает. */ /* Если h(X0, то треугольник ABC — задний. */ /* В обоих случаях переменная ntr не получает приращения */ /* и треугольники в полигоне не запоминаются. */ If (code--1) { а - yA * (zB-zC) - yB * (zA-zC) + yC * (zA-zB); Ь - -(хА * (zB-zC) - xB * (zA-zC) + xC * (zA-zB)); с - xA * (уВ-уС) - хВ * (уА-уС) + хС * (уА-уВ); г - sqrt(a*a+b*b+c*c); If (г- -О.0) r-eps; а - а/г, b - b/r; с - c/r; h - hO/r; } else if (fabs(a*xC+b*yC+c*zC~h)>0.001*fabs(h)) errmes /* Incorrectly specified polygon */ ("Полигон задан некорректно"); If (ntr - - ntriangle) errmes /* "Too many triangles" */ ("Слишком много треугольников"); ptrlangle-TRIANGLE+ntr; /* - &TRIANGLE[ntr] */ ptrlangle->A - A; ptriangle->B - В ; ptriangle->C - C;
168 Глава 5. УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ ptriangle->a - a; ptriangle->b - b ; ptriangle->c - с; ptriangle->h - h; /* The triangle will now be stored in the screen lists */ /* of the associated pixels; first the arrays LOWER, */ /* UPPER, LOW, UP are defined: */ /* Теперь треугольник нужно записать в экранные списки */ /* ассоциированных пикселов, но сначала необходимо */ /* определить массивы LOWER, UPPER, LOW. UP: V XA-xA/zA; YA-yA/zA; XB-xB/zB: YB-yB/zB: XC-xC/zC; YC-yC/zC: DA-XB*YC-XC*YB; DB-XC*YA-XA*YC; DOXA*YB-XB*YA; D-DA+DB+DC; DAB-DC-M*(XA-XB); DAODB-M*(XC-XA); DBC-DA-M*(XB-XC); topcode[0]-(D*DAB>0);topcode[lHD*DAC>0); topcode[2HD*DBC>0); Xleft[0]-XA; Yleft[0}-YA: Xright[0]-XB; Yrlght[0]-YB; Xleft[1]-XA; Yleft[1]-YA; Xright[1}-XC; Yrlght[1J-YC; Xleft[2]-XB; Yleft[2]-YB; Xright[2)-XC; Yright(2}-YC; for (l-O; КЗ; I++) /* I - triangle-side number */ /* переменная I означает номер стороны треугольника*/ if (Xleft[l]>Xright[l] I I (Xleft[l}--XNght[l] && Yleft[l]>Yright[l])) { aux-Xleft[l]; Xleft[lhXhght[l]; Xright[l]-aux; aux-Yleft[l]; Yleft[l]-Yhght[l]; Yright[l}-aux; } ipixmin-xwhole(min3(XA,XB,XC)); ipixmax-xwholeCmax^XA.XB.XC)); for(ipix-ipixmin; ipix<-ipixmax; ipix-н-) { LOWER[ipix}-UP[ipix]-10000; UPPER[ipix]-LOW[ipix]—10000; } for(l-0;K3;l++) { ipixleft-xwhole(Xleft[l]);ipixright-xwhole(Xright[l]); denom-Xhght[lhXleft[l]; if (Ipixleft!- ipixright) slope-(Yright[l]-Yleft[l])/denom; J_old-ywhole(Yleft[l]); for (ipix-ipixleft; ipix<—ipixright; ipix-и-) { if (ipix- -ipixright) jl-ywhole(Yright[l]); else jl-ywhole(Yleft[lHxreal(ipix+1)-Xleft[l])*slope); if (topcode[l]) { UPPER[ipix]-max3G_old(jl,UPPER[ipix]); UP[ipix}-min30_old,jl,UP[ipix]); }else { LOWERtipixhmlnSC.oldJI.LOWERnpixD; LOW[ipix]-max30_oldJI,LOW[ipix]); } " j_old-jl; } }
5.5. УЛУЧШЕННАЯ ПРОГРАММА 169 /* For screen column ipix, the triangle Is associated only /* with pixels In the rows LOWER[lplx] UPPER[lplx]. /* The subrange LOW[iplx}H UP[lplx]-1 of these rows /* denote pixels that lie completely whithin the triangle. /* /* Для экранного столбца ipix треугольник ассоциируется /* только с пикселами в строках LOWER[ipix],..,UPPER[iplx] /* Часть LOW[lpix]+1 UP[lplx]-1 этих строк обозначает /* пикселы, которые полностью лежат внутри треугольника. for(lplxHpixmin; ipix<-ipixmax; ipix-n-) for (jpix-LOWER[ipix]; jpix<-UPPER[ipix]; jplx++) { pointer-&(SCREEN[ipix] [jpix]); if 0pix>LOW[ipix] && Jpix<UP[lpix]) { xR-Xmln-Kipix-K).5)*deltaX; yR-Ymin-K]pix-H3.5)*deltaY; denom-a*xR+b*yR+c*d; dist-fabs(denom)>eps ? h*sqrt(xR*xR+yR*yR+1.)/denom : big; /* The line from viewpoint E to pixel point (xR, yR, 1) */ /* intersects plane ABC at a distance dlst from E. */ /* Прямая из точки глаза Е в точку пиксела (xR, yR, 1) */ /* пересекает плоскость ABC на расстоянии dlst от Е. */ if (dist < pointer->tr_dlst) { pointer->tr_cov-ntr; pointer->tr_dist-dlst; } } else /* Add triangle to screen list: */ /* Треугольник добавляется к экранному списку */ { pnode-^struct node *) malloc(sizeof(struct node)); if (pnode- -NULL) errmes /* "Memory allocation error 4й */ ("Ошибка 4 распределения памяти"); pnode->jtr- ntr; pnode->next - polnter->start; pointer->start - pnode; } } ntr++; } /* */ errmes(str) char *str; { fatal(); printf("%sV\ str); exit(1); /* fatal is defined in the same file as endgr etc. */ /* fatal определена в том же файле, что и endgr и др. */ } /*— V coeff(rho, theta, phi) double rho, theta, phi; { double th, ph, costh, sinth, cosph, sinph, factor; factor-atan(1.0)/45.0; /* Angles In radians */ /* Углы в радианах */ th-th eta *factor; ph-phi*factor; costh-cos(th); sinth-sin(th);
170 Глава 5. УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ cosph-cos(ph); slnph-sin(ph); /* Elements of matrix V, see Eq. (4.9): */ /* Элементы матрицы V, см. (4.9): */ v11—slnth; v12—cosph*costh; v13—sinph*costh; v21-costh; v22-*-cosph*sihth; v2^-sinph*slnth; v32-slnph; v33—cosph; v4CH*ho; /* */ vlew1ng(x, y, z, pxe, pye, pze) double x, y, z, *pxe, *pye, *pze; { /* Eye coordinates, computed as In Eq. (4.2): */ /* Видовые координаты, вычисляемые по (4.2): */ *pxe-v11*x + v21*y; *pye - v12*x + v22*y + v32*z; *pze - v13*x + v23*y + v33*z + v43; } Л V llnesegment(xP, yP, zP, xQ, yQ, zQ. kO) double xP, yP, zP, xQ, yQ, zQ; Int kO; { /* Line segment PQ is to be drawn, as far as It is not */ /* hidden by the triangles trset[k0] to trset[ntrset-1]. */ /* Отрезок прямой линии PQ вычерчивается, поскольку он */ /* не закрыт треугольниками от trsetfkO] до trset[ntrset-1]. */ Int j, k-kO, worktodo-1, А, В, С i, Pbeyond, Qbeyond, outside, Poutside, Qoutside, eA, eB, eC, sum; double a, b, c, h, hP, hQ, rl, r2, r3, xA, yA, zA, xB, yB, zB, xC, yC, zC, dA, dB, dC, labmln, labmax, lab, mu, xmin, ymin, zmin, xmax, ymax, zmax, C1. C2, C3, K1. K2, КЗ, denoml, denom2, Cpos. Ppos, Qpos, aux, epsl; while (k<ntrset) { j-trset[k]; ptrlangle-TRIANGLE+j: /* - &TRIANGLED] */ a-ptriangle->a; b-ptriangle->b; c-ptriangle->c; h-ptriangle->h; /* Test 1 */ /* Тест 1 */ hP-a*xP+b*yP+c*zP; hQ-a*xQ+b*yQ+c*zQ; eps1*eps+eps*h; if (hP-h<-eps1 && hG-h<-eps1){k++; continue;} /* PQ not behind ABC */ /* PQ не позади ABC */ /*Test2*/ /*Тест2*/ K1-yP*zQ-yQ*zP; K2-zP*xQ-zQ*xP; K3-xP*yQ-xQ*yP; A-ptriangle->A; B-ptriangle->B; C-ptrlangle->C; pvertex-VERTEX+A;
5.5. УЛУЧШЕННАЯ ПРОГРАММА 171 хА - pvertex->x; уА - pvertex->y; zA - pvertex->z; pvertex-VERTEX+B; xB - pvertex->x; yB - pvertex->y; zB - pvertex->z; pvertex-VERTEX+C; xC - pvertex->x; yC - pvertex->y; zC - pvertex->z; dA-K1*xA+K2*yA+K3*zA; dB-K1*xB+K2*yB+K3*zB; dC-K1*xC+K2*yC+K3*zC; /* If dA, dB, dC have the same sign, the vertices */ /* А, В, С lie at the same side of plane EPQ. */ /* Если dA, dB, dC имеют одинаковые знаки, то вершины */ /* А, В, С находятся с одной стороны от плоскости EPQ. */ еА- dA>eps ? 1 : dA<meps ? -1 : 0; еВ- dB>eps ? 1 : dB<meps ? -1 : 0; еС- dOeps ? 1 : dC<meps ? -1 : 0; sum - еА+еВ+еС; if (abs(sum)>-2) { k++; continue; } /* If this test succeeds, the (Infinite) line PQ lies /* outside pyrtamid EABC (or the line and the pyramid /* have at most one point in common). /* If the test fails, there is a point of intersection. /* Если этот тест завершен успешно, то (бесконечная) /* прямая линия PQ лежит вне пирамиды ЕАВС /* (или имеет, по крайней мере, одну общую точку). /* Если тест не выполнен, то имеется точка пересечения. /*Test3*/ Л Тест 3*/ Poutside-Qoutslde-O; labmin-1.; labmax-O.; for (i-0; КЗ; I++) { C1«yA*zB-yB*zA; C2-zA*xB-zB*xA; C3-xA*yB-xB*yA; /* C1 x + C2 у + C3 z - 0 is plane EAB */ /* уравнение описывает плоскость ЕАВ */ Cpos-C1*xC+C2*yC+C3*zC; Ppos-C1*xP+C2*yP+C3*zP; Qpos-C1*xQ+C2*yQ+C3*zQ; denom 1-Qpos-Ppos; if (Cpos>eps) { Pbeyond- Ppos<meps; Qbeyond- Qpos<meps; outside- Pbeyond && Qpos<-eps 11 Qbeyond && Ppos<-eps; } else if (Cpos<meps) { Pbeyond- Ppos>eps; Qbeyond- Qpos>eps; outside- Pbeyond && Qpos>-meps [ I Qbeyond && Ppos>»meps; }else outside-1; if (outside) break; lab-fabs(denom1)<-eps ? 1.e7 :-Ppos/denom1; /* lab indicates where PQ meets plane EAB */ /* Параметр lab указывает на точку пересечения */ /* отрезка PQ с плоскостью ЕАВ */ Poutside I-Pbeyond; Qoutside I-Qbeyond;
172 Глава 5. УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ denom2-dB-dA; mu- fabs(denom2)<-eps ? 1.е7 : -dA/denom2; /* mu tells where AB meets plane EPQ */ /* Параметр mu указывает на точку пересечения */ /* отрезка АВ с плоскостью EPQ */ If (mu>-meps && mu<-oneplus && lab>-meps && lab<-oneplus) { If (lab<labmin) labmin-lab; if (lab>labmax) labmax-lab; } aux-xA; xA-xB; xB-xC; xC-aux; aux-yA; yA-yB; yB-yC; yC-aux; aux-zA; zA-zB; zB-zC; zC-aux; aux-dA; dA-dB; dB-dC; dC-aux; } If (outside) {к-н-; continue;} /*Test4*/ /*Тест4*/ if (!(Poutside || Qoutside)) { worktodo-O; break; /* PQ Invisible */ /* Отрезок PQ невидим */ } /*Test5*/ /*Тест5*/ r1-xQ-xP; r2-yQ-yP; r3-zQ-zP; xmln-xP+labmln*r1; ymln-yP+labmln*r2; zmln-zP+labmln*r3; If (a*xmln+b*ymln+c*zmln-h<-eps1){ k++; continue; } xmax-xP+labmax*r1; ymax-yP+labmax*r2; zmax-zP+labmax*r3; If (a*xmax+b*ymax+c*zmax-h<-eps1){ k++; continue; } /* If this test succeeds, an Intersection of PQ */ /* and the pyramid lies In front of plane ABC. */ /* Если этот тест выполнен успешно, то точка пересечения */ /* отрезка PQ и пирамиды находится перед плоскостью ABC */ /*Test6V ЛТестб*/ ' if (Poutside II hP<h-eps1) llnesegment(xP, yP, zP, xmin, ymln, zmin, k+1); If (Qoutside I I hQ<h-eps1) Hnesegment(xQ, yQ, zQ, xmax, ymax, zmax, k+1); worktodo-O; break; > if (worktodo) { move(d*xP/zP+d,d*yP/zP+c2); draw(d*xQ/zQ+d, d*yQ/zQ+c2); } } lnit_vlewport(pXMIN,pXMAX.pYMIN.pYMAX) double *pXMIN, *pXMAX, *pYMIN, *pYMAX; { double XMIN, XMAX, YMIN, YMAX, len-0.2;
5.5. УЛУЧШЕННАЯ ПРОГРАММА 173 printf /* "Give viewport boundaries XMIN, XMAX, YMIN, YMAX" */ ("Задайте границы области вывода XMIN, XMAX, YMIN, YMAXAn"); printf /* "for a full screen: 0 10 0 7"*/ ("для полного экрана : 0 10 0 7\n"); scanf("%lf %lf %lf %lf", &XMIN, &XMAX, &YMIN, &YMAX); /* Show the four viewport corners */ /* Маркировка четырех углов области вывода */ initgr(); move(XMIN,YMIN+len); draw(XMIN.YMIN); draw(XMIN+len,YMIN); move(XMAX-len.YMIN); draw(XMAX,YMIN); draw(XMAX,YMIN+len); move(XMAX.YMAX-len); draw(XMAX.YMAX); draw(XMAX-len,YMAX); move(XMIN+len,YMAX); draw(XMIN,YMAX); draw(XMIN,YMAXHen); move((XMIN+XMAX)/2,YMIN);draw((XMIN+XMAX)/2,YMIN); /* Dot in the middle of bottom viewport boundary for orientation. */ /* Точка в середине нижней границы области вывода */ /* включена для определенности ориентации. */ *pXMIN-XMIN; *pXMAX-XMAX; *pYMIN-YMIN; *pYMAX-YMAX; Эта программа использовала входные данные, приведенные в начале параграфа 5.4, и в результате получено изображение на рис. 5.20. Рис. 5.20. Буква А в перспективе
174 Глава 5. УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ УПРАЖНЕНИЯ 5.1. Составьте входной файл для программы HIDLINPX для вы- черчивания простейшего домика в перспективе. 5.2. Используйте программу HIDLINPX для одного из упражне- ний главы 4, но возьмите фиксированное значение для я, например п - 3. В отличие от главы 4 теперь можно выби- рать любое положение для точки наблюдения, не заботясь о невидимых ребрах. 5.3. Какую придется сделать модификацию программы HIDLIN (или HIDLINPX), если потребуется задавать координаты точки наблюдения в ортогональных координатах? 5.4. В этой главе невидимые отрезки полностью исключались. Однако инженеры часто изображают их в виде штриховых линий. Исследуйте, как можно модифицировать программу HIDLIN или HIDLINPX для достижения такого эффекта.
Глава 6 ПРАКТИЧЕСКИЕ ПРИМЕРЫ 6.1. ВВЕДЕНИЕ Программа HIDUNPX — это программа общего назначения для вычерчивания объектов в перспективе. Однако, несмотря на те хорошие возможности, которые она обеспечивает по сравне- нию со своей предшественницей HIDUN, еще требуется прове- сти довольно большую работу по подготовке входного файла. Эта задача может быть упрощена двумя путями. Во-первых, сущест- вуют различные устройства для графического ввода (см. книгу Ньюмена и Спрула, 1979). Эту сторону здесь обсуждать не бу- дем, а ограничимся рассмотрением приборно-независимого про- граммного обеспечения. Во-вторых, можно обнаружить, что многие объекты обладают некоторой степенью регулярности, что позволяет автоматически подготавливать требуемый файл. Про- граммы для этой цели должны выполняться перед запуском на выполнение основной программы, поэтому иногда они называ- ются препроцессорами. В этой главе обсудим несколько приме- ров таких программ. Все они генерируют файл, который должен читаться программой HIDLINPX. Как и в параграфе 5.4, этот файл имеет следующую структуру: хО уО zO (Координаты центральной точки объекта) номер_вершины х у z номер_вершины х у z Faces — Грани: (Обязательное ключевое слово Faces) номер^вершины . . . номер_вершины# ... . (вершины полигона) номер_вершины . . . номер_вершины#
176 Глава 6. ПРАКТИЧЕСКИЕ ПРИМЕРЫ Замечания 1. В любом месте файла можно вставлять комментарии между круглыми скобками (...). 2. После ключевого слова Faces номера вершин каждого поли- гона задаются в порядке обхода против часовой стрелки. Не- посредственно за последним номером вершины каждого полигона следует символ #. 3. Если после слова Faces последовательность содержит только два номера вершин, то это означает необходимость вычер- чивания отдельного отрезка прямой линии (если только он виден), что дает возможность вычерчивать отрезки прямых линий, не входящие в состав ребер объекта. 4. Если грань содержит отверстие, то она трансформируется в полигон введением искусственного фиктивного ребра. Это ребро не вычерчивается, если номер его второго конца задан отрицательным. Например, на Рис. 6.1 внутренний прямо- угольник обозначает отверстие, тогда мы можем ввести фиктивное ребро 2 -6 и описать полигон как 12-65876-234* Заметим, что если обходить эти ребра именно в этом порядке и смотреть на следующую вершину, то описываемая область всегда находится слева по ходу. Это означает, что вершины отверстия должны обходиться по часовой стрелке, а не против. Начертим картинку, которая является логическим продолже- нием примера параграфа 5.5. Вместо одной объемной буквы Л на- 8 5 ^ 7 6 Рис. 61. Отверстие в прямоугольнике
61. ВВЕДЕНИЕ 111 чертим п ее копий в ряд, где п может принимать любое положи- тельное целое значение. В самой картинке нет какого-либо опре- деленного смысла, но идея часто используется на практике: срав- нительно нерегулярная часть картинки может быть продублиро- вана много раз (здесь это одиночная буква А). В этом случае координаты и номера вершин для копий могут быть вычислены по данным оригинала. Следующая программа является препро- цессором. Она генерирует файл A,DAT, который должен быть прочитан программой HIDLINPX. Программа запрашивает чис- ло, определяющее количество вычерчиваемых букв А. /* LETTERSA: A Preprocessor for HIDLINPX */ /* ' Препроцессор для программы HIDLINPX */ #include "stdlo.h" #deflne thickness 10 int base; FILE *fp; maln() { int n, I, j, nr, x; static int X[20], /* By default initialized to 0 */ /* По умолчанию инициируется нулями */ Y[20]-{-30.-20.-16. 16,20,30, 0,-12, 12, 0}, Z[20]-{ 0, 0, 8, 8, 0, 0,60, 16, 16,40}; printf /* "How many letters?" */ ("Сколько букв вычертить?\п"); scanf("%d",&n); fp-fopen("a.dat", "w"); fprintf(fp,"%f %f %f\n", -(n-0.5)*thickness, 0.0, 30.0); for (j-10; j<20; j++) { X[j] - -thickness; Y[j] - Y[j-10]; Z[j]- Z[j-10]; } for(i=0; Kn; I++) for (|-0; j<20; j++) { nr - 20*i+j; x - -2*i*thickness+X[j]; fprintf(fp, "%d %d %d %d\n", nr, x, Y[j]. Z[jJ); } fprintf(fp,"Faces « ГраниЛп"); for(K); Kn; I++) { base - 20*i; w12(0, 1, 2, 3, 4, 5, 6,-9, 8, 7, 9.-6); w12(10, 16, -19, 17, 18, 19, -16, 15, 14, 13, 12, 11); w4(1, 11, 12. 2);
178 Глава 6. ПРАКТИЧЕСКИЕ ПРИМЕРЫ w4(2, 12, 13, 3); w4(14, 4, 3, 13); w4(7, 8, 18,17); w4(7, 17, 19, 9); w4(18, 8, 9, 19); w4(5, 15, 16, 6); w4(10, 0, 6, 16); w4(10, 11, 1, 0); w4(14, 15, 5, 4); } fclose(fp); } w12(a, b, c, d, e, f, g, h, i, j, k, I) inta, b, c, d, e, f, g, h, i, j, k, I; { fprlntf(fp, "%4d %4d %4d %4d %4d %4d %4d %4d %4d %4d %4d %4d#\n'\ s(a), s(b), s(c), s(d), s(e), s(f), s(g), s(h), s(i), sO), s(k), s(l)); } w4(a, b, c, d) Int a, b, c, d; { fprintf(fp, "%4d %4d %4d %4d#\n", s(a), s(b), s(c), s(d)); } int s(p) Int p; { return p>-0 ? p+base : p-base; } Для понимания этой программы можно также обратиться к параграфу 5.4, где описан входной файл для вычерчивания оди- ночной буквы Л. Теперь вместо него задаются строки вида номер^вершины х у z в возрастающем порядке номеров вершин, хотя порядок их пере- числения никак не влияет на вычисления. Чтобы избежать мно- гократных обращений к функции fprint/, введены функции wl2 и w4. Они, в свою очередь, обращаются к функции s, которая обес- печивает приращение исходных номеров вершин (< 20) на 20/, при определении j'-й буквы. Очевидно, что сложение заменяется вычитанием, если номер вершины задан отрицательным. Если пустим эту программу на выполнение и зададим число 20 для определения количества букв, а также 700, 80,80 для сфе- рических координат точки наблюдения Е, то получим довольно объемистый файл Л.DAT. Выполнение программы HIDLINPX с этим файлом дает результат, показанный на рис. 6.2. Время вы- числений на компьютере типа PRIME 750 составило около 17 с.
6.2. ПОЛЫЙ ЦИЛИНДР 179 Рис. 62. Результат совместной работы программ LETTERS А и HIDUNPX 6.2. ПОЛЫЙ ЦИЛИНДР Большое число объектов имеет кривые поверхности. Такие поверхности можно аппроксимировать полигонами. Для приме- ра рассмотрим полый цилиндр, показанный на рис. 6.3(6). Для некоторого достаточно большого целого числа п выберем п рав- ноудаленных точек на внешней окружности (с радиусом R) на верхней грани и столько же аналогичных точек на нижней грани. Затем аппроксимируем внешний цилиндр с помощью призмы, вершинами которой являются эти 2п точек. Есть также и внут- ренний цилиндр с радиусом г (г < R). Оба цилиндра имеют высо- ту А и ось, совпадающую с осью z нашей системы координат. Внутренний цилиндр также аппроксимируется призмой анало- гично внешнему цилиндру. Нижняя грань лежит в плоскости z e О, а верхняя грань — в плоскости z = А. Подлежащий вычерчива- нию объект и его положение полностью определяются значения- ми /г, R, г и А. Сначала рассмотрим случай п = 6, а затем позднее обобщим его для произвольного п. Пронумеруем вершины, как показано на рис. 6.4. Для каж- дой вершины i на верхней грани (1 < i < 12) существует верти- кальное ребро, соединяющее ее с вершиной /+12. Верхняя грань может быть описана последовательностью 1 2 3 4 5 6 -12 11 10 9 8 7 12-6* (а) (б) Рис. 6.3. (а) — п - 6; (б) - п - 100
180 Глава 6. ПРАКТИЧЕСКИЕ ПРИМЕРЫ Верхняя грань 18 Нижняя грань Рис. 6.4. Нумерация точек Здесь пары чисел (6, -12) и (12, -6) обозначают фиктивные ребра. На рис. 6.4 нижняя грань наблюдается со стороны положи- тельной полуоси z, но в реальности может быть видна только од- на из граней. Следовательно, для нижней грани обход вершин должен производиться в противоположном направлении и его можно записать в виде 18 -24 19 20 21 22 23 24 -18 17 16 15 14 13# Поскольку п = 6, имеем 12 = 2/г, 18 = Ъп и 24 = 4л, поэтому приведенные выше последовательности являются частным слу- чаем последовательностей вида 1 ... п -2п 2м-1... л+1 2п -п# и Ъп -4/1 Ъп+1 ... 4п -Ъп Ъп-1 ... 2я+1# Обозначим п Тогда ортогональные координаты вершин на верхней грани (с номерами вершин / = 1,..., 2п) определятся как х. = R cos id yt = R sin id (i = 1,..., n ; внешняя окружность) z,-h xt = r cos (i-n)d yt = r sin (i-n)d (/ = n+l,..., 2n ; внутренняя окружность) zt-h
6.2. ПОЛЫЙ ЦИЛИНДР 181 Для нижней грани будем иметь xi xh2n УГУ(г2п «в.2л+1,...,4л) z,-0 Ниже приведена программа для вычерчивания полой приз- мы. Выбрав достаточно большое целое число л, получим хоро- шую аппроксимацию цилиндра, что демонстрирует рис. 6.3(6). /* HOLLOW_CYLINDER preprocessor for HIDLINPX */ /* препроцессор для программы HIDLINPX */ #include "stdio.h" #lnclude "math.h" main() { FILE *fp; int n, j, k, I, i, m; float r, R, pi, alpha, cosa, sina, delta, h, radius, hite; printf /* "Give number n (n points on a circle):" */ ("Задайте число n — количество точек на окружности: "); scanf("%d", &n): printf /* "Give cylinder height:" */ ("Задайте высоту цилиндра: "); scanf("%f", &h); printf /* "Give large radius R and small radius r:" */ ("Задайте большой (R) и малый (г) радиусы цилиндра: "); scanf("%f %f", &R, &r); fp-fopen("cyl.dat", "w"); pi-4.0*atan(1.0); delta-2.0*pi/n; fprintf(fp, "0.0 0.0 %7.2f\n", .5*h); for (i-1; i<-n; i++) { alpha-i*delta; cosa-cos(alpha); sina-sin(alpha); for(K); l<2; I++) /* IK): outer, 1-1: inner circle */ /* К): внешняя, 1-1: внутренняя окружность */ { radius-(l--0?R:r); for (m-0; m<2; m-н-) /* m-O: top, m-1: bottom boundary */ /* т-Ю: верхняя, т-1: нижняя грань */ { k-i+l*n+m*2*n; hite- (m- -0 ? h : 0); fphntf(fp, "%d %9.5f %9.5f %9.5f\n", k, radlus*cosa, radlus*sina, hite); } } } fprintf(fp, "Faces - ГраниЛп"); /* Top boundary face */ /* Верхняя граничная грань */ for (i-1; i<-n; i-H-)fprintf(fp, "%d\n", I); fprintf(fp, "%d\n",-2*n); for(i-2*n-1; i>-n+1; i—) fprintf(fp, "%d\n", i);
182 Глава 6. ПРАКТИЧЕСКИЕ ПРИМЕРЫ fprlntf(fp, "%d %d#\n", 2*n,-n); /* Bottom boundary face */ /* Нижняя граничная грань */ fprlntf(fp, "%d %d\n", 3*n,-4*n); for(i-3*n+1; i<-4*n; I++) fprintf(fp, "%d\n", i); fprlntf(fp, "%d\n", 3*n); for(l-3*n-1; l>-2*n+2; i- -) fprlntf(fp, "%d\n", I); fprlntf(fp, "%d#\n",2*n+1); /* Vertical lines */ /* Вертикальные линии */ for(M;K-n;l++) { H%n+1; fprlnttXfp. "%d %d %d %d#\n", J, i, l+2*n, j+2*n); fprintf(fp, "%d %d %d %d#\nH, i+n, j+n, J+3*n, l+3*n); } fclose(fp); } 6.3. СТЕРЖНИ ПО СПИРАЛИ На рис. 6.5 показан следующий пример — спираль, состав- ленная из горизонтальных стержней длиной /, шириной и высо- той iv. Нижний стержень лежит на плоскости ху, как показано на рис. 6.6. Начиная снизу, позицию каждого следующего стержня получают поворотом предыдущего вокруг оси z на угол 90° и од- новременным приращением его координаты z на величину w. Пронумеруем стержни 0, 1, ..., п - 1 снизу вверх. Стержень i имеет номера вершин 8/, 8г + 1,..., 8i + 7, назначаемые цикличе- ски, в соответствии с номерами вершин стержня 0, изображенно- го на рис. 6.6. Каждая точка Ос', /, z') стержня i + 1 может быть получена путем поворота соответствующей точки (х, у, z) стерж- ня i на 90° вокруг оси z (и заданием z' = z + w), то есть г , ,, г . Г cos 90° sin 90°] [х yl=[xylL-sin90° cos90*J или просто х' = -у и у1 = х. Это и применяется в следующей про- грамме. /* BEAMS preprocessor for HIDLINPX) */ /* препроцессор для программы HIDLINPX */ #include "stdio.h" main() { FILE *fp; int I, j, n, A; float I, w, xA, yA, xB, yB, xC, yC, xD, yD, a, b, aux, z; printf /* "How many beams?" */ ("Сколько стержней?\п"); scanf("%d", &n);
63. СТЕРЖНИ ПО СПИРАЛИ 183 Рис. 65. Спираль из стержней Рис. 66. Нумерация вершин стержня О
184 Глава 6. ПРАКТИЧЕСКИЕ ПРИМЕРЫ } printf /* "The beam measures I xwx wAnGive I and w:M */ ("Размеры стержня I x w x юЛпЗадайте значения I и w: n); scanf("%f %f", &l,&w); fp-fopen("beams.dat", "w"); fprlntf(fp, "0.0 0.0 % An", n*w/2.0); /* central object point */ /* центральная точка объекта a-0.5*l; b-a-w; xA-a; yA—a; xB-a; yB-a; xC-b; yC-a; xD-b; yD—a; for(i-0; Kn; i++) { for(p0;j<2;j++) { z-<i+j)*w; A-8*i+4*J; fprintf(fp, "%d %f %f %f\n", A, xA, yA, z); fprintf(fp, "%d %f %f %f\n", A+1, xB, yB, z); fphntf(fp, "%d %f %f %f\n", A+2, xC, yC, z); fprlntf(fp, "%d %f %f %f\n", A+3, xD, yD, z); } aux-xA; xA1—yA; yA-aux; aux-xB; xB—yB; yB-aux; aux-xC; xC—yC; yC-aux; aux-xD; xD—yD; yD-aux; } fprintf(fp, "FacesAn"); for (1-0; i<n;i++) { A-8*i; fprintf(fp,"%d %d %d %d#\nu fprintf(fp,"%d %d %d %d#\n" fprintf(fp,"%d %d %d %d#\n" fprintf(fp,"%d %d %d %d#\nn fprintf(fp,"%d %d %d %d#\nM fprintf(fp,"%d %d %d %d#\n" } fclose(fp); A, A+3, A+2, A+1); A+4, A+5, A+6, A+7); A, A+1, A+5, A+4); A+3, A+7, A+6, A+2); A, A+4, A+7, A+3); A+1, A+2, A+6, A+5); /* снизу */ /* сверху */■ /* спереди */ /* сзади */ /* слева */ /* справа */ 6.4, ВИНТОВАЯ ЛЕСТНИЦА Идея размещения стержней по спирали реализована в следу- ющем примере — винтовой лестнице, показанной на рис. 6.7. Ес- ли подниматься по лестнице, то на каждом шаге ступенька сдви- гается вверх на расстояние Л. При числе ступеней, равном /г, об- щая высота лестницы будет H = nh. Каждая ступенька описывается восемью вершинами, локально пронумерованными от 0 до 7. Добавим к ним точки 8 и 9 для вертикального стержня. Стержень служит для крепления перил к лестнице. Стержни и перила очень тонкие, поэтому они вычерчиваются в виде отрез- ков прямых линий. В центре лестницы расположен цилиндри - ческий столб диаметром 2г. Его ось совпадает с осью z нашей сие-
64. ВИНТОВАЯ ЛЕСТНИЦА 185 Рис. 6.7. Винтовая лестница . темы координат. Перила и вертикальные стержни находятся на расстоянии R от оси z (R> r). Ступеньки соединяют столб с вер- тикальными стержнями. Нижняя ступенька показана на рис. 6.8. Каждая ступенька представляет собой брусок длиной /?-г, шириной 1.5Л и высотой 0.2А. Вместе все ступеньки совершают полный оборот, поэтому угол поворота для каждой ступеньки составляет п
186 Глава 6. ПРАКТИЧЕСКИЕ ПРИМЕРЫ 5h 0.2h Рис. 6.8. Нижняя ступенька Ступенькам припишем номера 0, 1, ..., п - 1, нумеруя их снизу вверх. Ступенька / может быть получена путем поворота нуле- вой ступеньки вокруг оси z на угол а = йи одновременного подъ- ема на высоту ih. Таким образом, координаты вершин (дс, у, z) ступеньки i можно вычислить по координатам соответствующих вершин (X, У, Z) ступеньки 0: [xy)-[XY)\C0Sa SinCtl- ' [-sin a cos а J z = Z + ih В приведенной ниже программе координаты вершин нижней ступеньки записаны в массивах X, У, Z. Вершинам ступенек, включая вертикальные стержни и перила, приписываются номе- ра 0,..., Юл - 1. Считая М- Юл, равноудаленным точкам ниж- ней окружности центрального столба приписываем следующие л номеров Л/, ..., М + п - 1. Аналогично, целые числа М + л, ..., М + 2 - 1 приписываются верхней окружности столба.
6.4. ВИНТОВАЯ ЛЕСТНИЦА 187 /* WINDING_STAIRCASE (preprocessor for HIDLINPX) */ /* Препроцессор для программы HIDLINPX*/ ♦Include "stdio.h" ♦include "math.h" main() { FILE*fp; Intl. J. n, k,M; float r, R, pi, alpha, cosa, sina, x, y, z, delta, h, H,X[10],YtlO],Z[10]; printf /* "Give number n (to draw n stairs)" */ ("Задайте число n (для вычерчивания n ступенек):\п"); scanff%d", &n); printf /* "Give height of a single stair step " */ ("Задайте высоту отдельной ступеньки лестницы: "); scanf("%f", &h); printf /* "Give large radius R and small radius r\n" */ ("Задайте большой (R) и малый (г) радиусы лестницы\п"); scanf("%f %f", &R, &г); fp-fopen("winding.dat", "w"); pl«4.0*atan(1.0); delta-2.0*pi/n; fprlntf(fp, "0.0 0.0 %f\n", 0.5*n*h); forO-0;J<4;J4+)Z[]}-0; for(H;J<8;j++)Z[j]-h/5; X[0}-X[1}-X[4]-X[5}-R; X[2]-X[3)-X[6}-X[7]-r; Y[0}-Y[4]-Y[3]-Y[7}—.75*h; Y[1}-Y[5]-Y[2]-Y[6]-.75*h; X[8]-R; Y[8}-.0; Z[8)-h/10; X[9]-R; Y[9]-.0; Z[9]-5*h; M-10*n; H-n*h; for(H);l<n;l++) { alpha-i*delta; cosa-cos(alpha); sina-sln(alpha); for(J-0;j<10;J++) { k-10*i+J; x-X[j]*cosa-Y[j]*sina; y-X[j]*slna+Y[J]*cosa; z-Z[j}H*h; fprintf(fp, "%d %f %f %f\n", k, x, y, z); } x-r*cosa; y-r*sina; fprintf(fp, "%d %f %f %f\n", M+i, x, y, 0.0); fprlntf(fp, "%d %f %f %f\n", M+n+i, x, y, H+5*h); } fprlntf(fp, "FacesAn"); for(l-0;Kn;l-H-) { k-10*i; fprintf(fp, "%d %d %d %d#\n", k. k+1, k+5, k+4);
188 Глава 6. ПРАКТИЧЕСКИЕ ПРИМЕРЫ fprintf(fp, "%d %d %d %d#\n", k+2. k+3, k+7, k+6); fprintf(fp. "%d %d %d %d#\rT, k+1, k+2, k+6. k+5); fprintf(fp. "%d %d %d %d#\n", k+3, к-Ю, k+4, k+7); fprintf(fp. "%d %d %d %d#\n", k+2, k+1, k+5, k+6); fprintf(fp, "%d %d %d %d#\n", k+4, k+5, k+6, k+7); fprintf(fp, "%d %d %d %d#\n", k+1, k+0, k+3, k+2); fprintf(fp, "%d %d#\n", k+8, k+9); if (i<n-1)fprintf(fp, "%d %d#\n", k+9, k+19); } for(i-0; i<n; i++) fprintf(fp, "%d %d %d %d#\n", M+i, M+(i+1)%n, M+n+(j+1)%n, M+n+i); for(i-M+n-1; i>-M; i--) fprintf(fp, " %d". i); fprintf(fp. "#\n"); /* bottom of pole */ /* нижняя часть столба */ for(i-M+n; !<M+2*n; i++) fprintf(fp, " %d". i); fprlntf(fp, "#\n"); /* top of pole */ fclose(fp); /* верхняя часть столба */ } 6.5. TOP Некоторые размеры (вернее, их соотношения) винтовой лест- ницы выбирались произвольно. Теперь рассмотрим несколько примеров, в которых координаты всех вершин вычисляются по ограниченному количеству данных. В качестве первого такого примера рассмотрим тор, изображенный на рис. 6.9. Входные данные для этой программы состоят из трех чисел /г, R и г (R > г). На рис. 6.10 большая горизонтальная окружность определяет положение центров окружностей, образующих тор, радиус этой окружности равен R. Выберем п равноудаленных то- чек на этой окружности в качестве центров малых вертикально Рис. 6.9. Тор
6.5. TOP 189 Рис. 6.10. Основные окружности тора расположенных окружностей радиуса г. Параметрическое пред- ставление большой окружности описывается формулами x = Rcosa y = R since z = 0 Точка, соответствующая а = 0, является центром малой окруж- ности х = R + г cos /? у = 0 z = rsin/J которая также показана на рис. 6.10. Остальные п - 1 малые ок- ружности формируются поворотом этой исходной окружности вокруг оси z на угол а = й, nje/^l,..., п- 1 и<5 = Ъс/п. На малой окружности выберем п точек с номерами вершин 0, 1,..., п - 1. Размещение точек на первой малой окружности определяеюся параметром/? = /3, им приписываются номера вер- шину (/-О,1,..., п-1). Следующие п вершин, пронумерованные л, п + 1, ..., 2я - 1, лежат на соседней малой окружности, соот- ветствующей i = 1, и так далее. В общем, мы получим номера
190 Глава 6. ПРАКТИЧЕСКИЕ ПРИМЕРЫ вершин i-n + j (/- 0, 1,..., п- 1,у = 0, 1,..., п- 1). Поворот на угол а = id относительно оси z записывается как г , , 1 г , Г cos a sin a 1 [х у ]= [jc у] . j I i -^-sina Cos а J В нашей ситуации исходная малая окружность лежит в пло- скости xz, поэтому у = 0, что сокращает это матричное произве- дение до х' = х cos a у' = х sin a Этот же результат может быть получен непосредственно из рис. 6.10, Следующая программа генерирует файл для вычерчи- вания тора. /* TORUS (preprocessor for HIDUNPX) */ /* Препроцессор для программы HIDLINPX */ ♦include "stdio.h" ♦Include "math.h" maln() { FILE *fp; Intl. J, n; float r, R, pi, alpha, beta, cosa, sina, x, x1, y1, z1, delta; prlntf /* "Give number n (to draw an n x n torus)" */. ("Задайте число n (для вычерчивания тора из п х п сегментов):\п"); scanf("%d", &n); prlntf /* "Give large radius R and small radius r" */ ("Задайте большой (R) и малый (г) радиусы тора\п"); scanf("%f %f", &R,&r); fp-fopen("torus.dat", "w"); pl-4.0*atan(1.0); delta-2.0*pi/n; fprintf(fp, "0.0 0.0 0.0\n"); /* central object point */ /* центральная точка объекта */ for(l-0;i<n;l++) { alphaH*delta; cosa-cos(alpha); sina-sin(alpha); for(J-0;J<n;j++-) { beta-J*delta; x-R+r*cos(beta); /* у - 0 */ x1-cosa*x; y1-sina*x; z1-r*sln(beta); /* z1 - z */ fprintf(fp, "%d %e %e %e\n". i*n+j, x1. y1. z1); /* %f not correctly Implemented */ /* применение формата %f здесь не очень корректно */ } } fprintf(fp, "Faces- ГраниЛп");
6.6. ПОЛУСФЕРА 191 for (i-0; i<n; i++) for(j-0;j<n;J++) { fprlntf(fp, "%d %d %d %d#\rT, •*n+J, (i+1)%n*n+j, (l+1)%n*n-Kj+1)%n, i*n-Kj+1)%n); } fclose(fp); 6.6. ПОЛУСФЕРА На рис. 6Л1 показана нижняя половина сферы. Пусть начало системы координат совпадает с центром сферы. Поскольку вся картинка будет автоматически масштабироваться на всю область вывода, абсолютные размеры не имеют значения, поэтому мож- но выбрать радиус единичной длины. Тогда все точки этой полу- сферы удовлетворяют уравнениям *2+Az2=i -1 < z < О Точка (0, 0, -1) имеет номер вершины 0. Прямой угол делит- ся на л равных углов д: число п — единственное число, которое требуется ввести в программу при запуске. Все необходимые точки на дуге полусферы при х > 0 и у=0 пронумерованы 1,2, ... , я, считая снизу вверх. Точки на соседней полуокружности при у > 0 нумеруются п + 1, п + 2,..., 2п и так далее. Такой способ ну- мерации иллюстрируется следующей таблицей, где каждая (го- ризонтальная) строка соответствует точкам на горизонтальной Рис. 6.11. Полусфера
192 Глава 6. ПРАКТИЧЕСКИЕ ПРИМЕРЫ окружности и, аналогично, каждый столбец соответствует точ- кам на четверти вертикальной окружности: i —*0 1 2 4л-1 У п п 2п Ъп An 2 2 п + 2 2п + 2 (4п-1)п + 2 1 1 п + 1 2м+ 1 (4л-1)л + 1 Нижний слой полусферы образуется из треугольников, все они имеют общую вершину в точке с номером 0. Остальные п - 1 слоев состоят из четырехугольников ABCD, каждый из которых имеет два взаимно параллельных горизонтальных ребра АВ и DC. Заметим, что эта полусфера фундаментально отличается от всех других примеров, рассматривавшихся ранее. Это не сплош- ной объект, а поверхность, видимая с любой стороны, в зависи- мости от положения точки наблюдения. Поскольку каждый из четырехугольников имеет две стороны, то в программе прихо- дится описывать обе последовательности, ABCD и DCBA. /* SEMI_SPHERE (preprocessor for HIDLINPX) */ /* препроцессор для программы HIDLINPX */ #include "stdio.h" #lnclude "math.h" main() { FILE *fp; Inti, j, n, A, B, C, D, P,Q; float pi, alpha, beta, delta, cosa, sina, cosb, sinb; printf /* "Give number n" */ ("Задайте число n\n"); scanf("%d", &n); fp-fopen("semi.dat", "w"); pM.0*atan(1.0); delta-pi/(2*n); /* n * delta - pi/2 */ fprintf(fp, "0.0 0.0 -0.5V); /* central object point */ /* центральная точка объекта */ /* R- 1; sphere centre in О */ /* Радиус — единичный, центр сферы в точке О */ fprintf(fp,"0 0.0 0.0 -1.0\п"); /* first point */ /* первая точка */
6.7. ФУНКЦИЯ ДВУХ ПЕРЕМЕННЫХ 193 for(i-0;i<4*n;i++) { alpha—l*delta; cosa-cos(alpha); sina-sln(alpha); for (j-1; j<-n; j-H-) { beta-j*delta; cosb-cos(beta); slnb-sin(beta); fprintf(fp, "%d%f %f %f\n'\ n*i+j, sinb*cosa, sinb*sina, -cosb); } } fprintf(fp, "Faces - Грани:\пм); for(l-0;i<4*n;i-H-) { P-I*n+1; Q-(i+1)%(4*n)*n+1; fprintf(fp,"%d %d %d#\n", 0, P, Q); fprintf(fp,M%d %d %d#\n", 0, Q, P); forQ-1; j<n; j++) { A-P+J-1; B-Q+J-1; C-Q+J; D-P+J; fprintf(fp, "%d %d %d %d#\rT, А, В, С, D); fprintf(fp, "%d %d %d %d#\n", D, С, В, A); } } fclose(fp); 6.7. ФУНКЦИЯ ДВУХ ПЕРЕМЕННЫХ Программа HIDLINPX составлялась с целью получения чер- тежей сплошных объектов. Но мы уже видели на двух примерах, что она также применима и для других целей. Во-первых, она может вычерчивать "свободные" отрезки прямых линий, напри- мер координатные оси, если номера их конечных точек имеются во входном файле после ключевого слова "Faces". Во-вторых, в параграфе 6.6 мы рассматривали поверхность полусферы как не- которую математическую абстракцию. Теперь продвинемся еще на шаг дальше в применении программы HIDLINPX для целей, далеких от первоначально поставленных. Иногда требуется графически представить функцию от двух переменных z-/U,y) (6.1) Для этого необходимо определить прямоугольный домен *min - х - хтах V • < V ^ V ■^min J -'max В принципе функция/может быть любой функцией двух пе- ременных. В качестве примера воспользуемся квадратичной функцией fix, у) = ах2 + by2 + сху + б/л*+ еу + g (6.2) 7-271
194 Глава 6. ПРАКТИЧЕСКИЕ ПРИМЕРЫ (В качестве коэффициента мы не применяем букву/ потому, что этой буквой обозначено имя функции.) Программа будет запра- шивать значения коэффициентов а, Ъ, с, d, e, g, a также целые числа Nx и N . Эти два целых числа используются для вычисле- ния длины и ширины элементарных прямоугольников: д max min Углы этих прямоугольников образуют сетку точек Ос, у), для которых будут вычисляться значения ze/Ос, у). Полученные та- ким образом точки в трехмерном пространстве соединяются от- резками прямых линий, параллельными плоскостям либо xz, ли- бо yz. Именно эти отрезки и будут вычерчиваться. На рис. 6.12 показана функция /0c,y)=0.1jc2-0.4y2 для которой было задано -5< jc<5, -2<у<2, N -20, #=8, р = 20, 0 = 50, ^> = 80 С каждой точкой сетки (/ = 0,..., Nx; j -0,.;., N ) ассоцииру- ется пара чисел О, /). Для точки Ос, у, z) на поверхности, соответ- ствующей точке сетки (/, /), приписывается номер вершины Рис. 6.12. Квадратичная функция двух переменных
6.7. ФУНКЦИЯ ДВУХ ПЕРЕМЕННЫХ 195 У Углах Угтп 1 2 1 • 0 ( 9 5 1 ) 10 6 2 1 ; 11 7 3 12 8 4 \ 3^1 0 *min *mox x (а) (б) Рис. 6.13. (а) — точки на поверхности; (б) — два треугольника Тогда имеем z-/(JCfy) Нумерация вершин показана на рис. 6.13 (а), где #х.- 3, Nу = 2. На рис. 6.13(a) мы видим поверхность из конца положитель- ной полуоси z. Например, точки 1, 2, 5, 6 -'■точкиповерхности. К сожалению, в общем случае эти точки не лежат в одной плос- кости, поэтому их нельзя использовать в качестве вершин поли- гона. Если точку 1 соединить с точкой 6, то получатся два тре- угольника, которые решат эту проблему. Общий случай показан на рис, 6.13 (б), где / - k + Nx + 1. Хотя эти два треугольника и не лежат в одной плоскости, они могут быть использованы в качест- ве требуемого полигона при условии, что ребро (к, I + 1) вычер- чиваться не будет. В зависимости от положения точки наблюде- ния любая из сторон треугольников может быть видимой, поэто- му после слова Faces в файле данных каждый треугольник необходимо определить дважды: к -</+!) Л+1# * + 1 Z+1 -*# к -(/+1) /# / /+1 -*# (нижний правый по часовой стрелке) (нижний правый против часовой стрелки) (верхний левый против часовой стрелки) (верхний левый по часовой стрелке) Знак минус предотвратит вычерчивание отрезка (Л, Z+ 1).
196 Глава 6. ПРАКТИЧЕСКИЕ ПРИМЕРЫ Кроме поверхностей функциональных зависимостей можно также вычертить части положительных координатных полуосей там, где они видны. Их длину должен указать пользователь. На- конец, программа запрашивает некоторое значение координаты z, чтобы можно было вычислить положение "центральной точки объекта", необходимой для программы HIDUNPX. Эта величи- на не особенно критична, поэтому для нее годится грубая оценка. Для квадратичных функций двух переменных следующая программа носит довольно общий характер. Для других функций f(x,y) необходимо заменить функцию/в конце программы и иск- лючить элементы программы, относящиеся к определению коэф- фициентов я, й, с, с/, е, g. /* FUNC: Perspective plot of a quadratic function */ /* Перспективный чертеж квадратичной функции */ #include "stdlo.h" float a, b, c, d, e, g; main() { FILE *fp; Intl.j.Nx. Ny. k.l; float xmin, xmax, ymin, ymax, hx, hy, x, y, f(), zc, xaxis, yaxis, zaxis; fp-fopen("FUNC.DAT", "w"); printf /* "Function equation:\n\n" */ ("Функция описывается уравнением:\п\п"); printf("f(x, у) - a*x*x + b*y*y + c*x*y + d*x + e*y + g\n\n"); printf /*"Give six coefficients:" */ ("Задайте шесть коэффициентов уравнения "); printf("a, b, c, d, e, g:\n"); scanf("%f %f %f %f %f %f", &a, &b, &c. &d, &e, &g); printf /*"Give limits:" */ , ("Укажите предельные значения "); prlntf("xmin, xmax, ymin, ymax:\n"); scanf("%f %f %f %f", &xmin, &xmax, &ymin, &ymax); printf /*"Glve integers Nx and Ny:" */ ("Задайте количество вершин Nx и Ny:\n"); scanf("%d %d", &Nx, &Ny); hx- (xmax-xmin)/Nx; hy- (ymax-ymin)/Ny; printf /* "Give approximate z value:" */ ("Укажите примерно центральное значение по оси z:\n"); scanf("%f", &zc); printf /* "Length of positive axes to be drawn (x. y, z):" */ ("Длина вычерчиваемых положительных осей (х, у, z):\n"); scanf("%f %f %f", &xaxis, &yaxis, &zaxis);
УПРАЖНЕНИЯ 197 х- (xmin+xmax)/2; у- (ymin+ymax)/2; fprintf(fp, "%f %f %f\n", x, y, zc); for (Ю; K-Nx; i++) for (j-0; j<-Ny; j++) { x-xmln+i*hx; y-ymin+J*hy; fprintf(fp, "%d %f %f %f\rT, J*(Nx+1)H+1. x, y, f(x.y)); } k-{Nx+1)*(Ny+1); fprintf(fp, "%d %f %f %f\n", ++k. 0.. 0.. 0.); fprintf(fp, "%d %f %f %f\n", -н-к, xaxls. 0., 0.); fprintf(fp, "%d %f %f %f\n", ++k, 0., yaxis, 0.); fprintf(fp, "%d %f %f %f\n", ++k, 0.. 0.. zaxls); fprintf(fp,"Faces - Грани.Лп"); for(i-0;i<Nx;l++) for (j=0; j<Ny; j++) { k«j*(Nx+1}H+1;l-k+Nx+1; fprintf(fp, "%d %d %d#\n", k, -(1+1), k+1); fprintf(fp. "%d %d %d#\n", k+1,1+1, -k); fprintf(fp, "%d %d %d#\n", k, -(1+1), I); fprintf(fp, "%d %d %d#\n", 1,1+1, -k); } k-(Nx+1)*(Ny+1); fprintf(fp, "%d %d#\n", k+1,k+2); /* x-axis */ /* ось х */ fprintf(fp. "%d %d#\n", k+1. k+3); /* y-axls */ /* ось у */ fprintf(fp, "%d %d#\n", k+1,k+4l: /* z-axis */ /* ось z */ fclose(fp); } float f(x,y) float x,y; { return a*x*x+b*y*y+c*x*y+d*x+e*y+g; УПРАЖНЕНИЯ 6.1. Напишите препроцессор для вычерчивания нескольких пи- рамид; некоторые из них могут частично закрывать другие в зависимости от положения точки наблюдения. 6.2. Напишите препроцессор для вычерчивания полной сферы. 6.3. Напишите препроцессор для вычерчивания нескольких полусфер; некоторые из них могут частично закрывать дру- гие. 6.4. Выберите одно из упражнений главы 4 и напишите препро- цессор для него. Теперь мы в состоянии дать общее решение относительно двух аспектов: - невидимые линии могут быть удалены вычислительным способом (а не так, как это делалось в главе 4),
198 Глава 6. ПРАКТИЧЕСКИЕ ПРИМЕРЫ Рис 6.14. Куб из кубиков - число п может быть переменным (в упражнении 5.2 ис- пользовалось п - 3). 6.5. Напишите общую программу для поворота, аналогичную программе из упражнения 3.2, в которой пользователь мо- жет задавать вектор АВ в качестве произвольной оси и угол поворота а вокруг нее. Программа должна прочитать вход- ной файл, предназначенный для программы HIDLINPX, и записать другой вместо него. В новом файле координаты бу- дут отличаться от координат в исходном файле так, чтобы в результате получилось изображение повернутого объекта в соответствии с заданными параметрами поворота. 6.6. Напишите препроцессор для вычерчивания большого коли- чества кубиков, которые располагаются рядом, позади и сверху относительно друг друга (см. рис. 6.14).
Приложение КРАТКОЕ ВВЕДЕНИЕ В ЯЗЫК СИ Это приложение нельзя рассматривать как полное описание языка Си. Оно предназначено для читателей, уже знакомых с ка- ким-либо другим современным языком программирования, но нуждающихся в кратком обзоре элементов языка Си, применяе- мых в данной книге. Читателям, которые собираются активно работать с языком Си, автор настоятельно рекомендует обра- титься к специальному учебнику, например: Керниган Б., Ритчи Д. Язык программирования Си (Пер. с англ. — М.: финансы и статистика, 1985). АЛ. ОСНОВНЫЕ ТИПЫ ДАННЫХ Перед использованием любых переменных сначала необхо- димо определить их типы, или, говоря профессиональным язы- ком, объявить их float хЛ, уАу pyqyr = 5.0, epsilon = 1^-6; double dd ; int i; short int si; long int li; charch='A '; При объявлении переменных им можно присвоить начальные значения, как сделано в этом примере для переменных г, epsilon, ch. Говорят, что эти переменные инициализированы. Количест- во битов (или разрядов), которые используются для различных типов данных, зависит от применяемых технических средств. На многих компьютерах тип float (число с плавающей точкой оди- нарной точности) может быть эффективно заменен на тип double (число с плавающей точкой двойной точности) для получения более высокой точности. В последующем мы не всегда будем явно указывать тип double, но будем рассматривать его как специаль- ный случай типа float Преобразование типов из float к double и обратно никогда не приведет к эффекту, отличному от ожйдае-
200 Приложение. КРАТКОЕ ВВЕДЕНИЕ В ЯЗЫК СИ мого. Типы "Boolean" (" Булевский") и "logical" ("логический") в языке Си отсутствуют. Целочисленное значение 0 воспринима- ется как false ("ложь"), а значение 1 (или любое другое ненуле- вое значение) — как true ("истина"). А.2. НЕКОТОРЫЕ ОПЕРАТОРЫ Рассмотрим фрагмент программы ' */Ос>=0.5) {/=10;/ = 20;Mse{* = 30;/ = 40;} if (и < 3.0) {v=1.8;w = 3.4;} if (a<b) m = n = 100; Смысл этих трех условных операторов интуитивно ясен. Заметим, что после ключевого слова if в условном операторе всегда следует выражение в круглых скобках. Часть условного оператора else ("иначе") является необязательной. Операторы присваивания, например г «10; (а также любые другие операторы), могут группироваться вме- сте в один составной оператор с помощью фигурных скобок. Так, строка {/=10; / = 20;} является составным оператором. Фигурные скобки обязательно применяются в условном операторе if, если при удовлетворении условия необходимо выполнить более одного оператора. Напри- мер, если бы было записано if (и<3.0) v=1.8;w=3.4; то оператор и>=3.4; выполнялся бы всегда, поскольку этот оператор не имеет ника- кой связи с условием и < 3.0. Фигурные скобки не ставятся только тогда, когда от условия зависит выполнение только одного опера- тора, как в последнем из трех показанных выше операторов. Оператор множественного присваивания m = n= 10; присваивает значение 10 обеим переменным тип. Примитивный цикл можно образовать с помощью условного оператора if и оператора безусловного перехода goto, как в примере
A.2. НЕКОТОРЫЕ ОПЕРАТОРЫ 201 5 = 0; i-l; again: if (i <= n) {s += i; z++; goto again;} Здесь 5 += / и /++ — сокращенная форма записи операторов 5 = 5 + / и * = z + 1, соответственно. Последние две строки не надо воспринимать как образец хорошо структурированного програм- мирования. Они приведены здесь лишь для объяснения двух последующих новых концепций языка. Первая из них — оператор цикла while: 5 = 0; *=1; while (i <= п) {s 4*= i; /++;} Этот цикл дает тот же результат, что и предыдущий пример, — после приращения переменной i она снова сравнивается с числом п и так далее. Другой важной конструкцией является оператор цикла for: 5 = 0; for (i = 1; i <= n\ /++) s += i; Эта часть программы дает такой же результат, как и оба преды- дущих примера, так как в каждом из них вычисляется значение суммы 5=1+2 + . .. + Л Если п имеет значение 0 (или меньше, чем 0), сумма 5 также будет равна 0, поскольку во всех трех случаях проверка на завер- шение цикла выполняется в начале цикла. Но такого не произой- дет при использовании оператора цикла do-while, как в примере: /=1; 5 = 0; do {s += /; /++;} while (i <= n); Здесь проверка на завершение цикла выполняется в конце, как и предполагает запись. Поэтому значение 1 + 2 + ... + п присваи- вается переменной 5, только когда п положительно. Если п будет равно 0 (или меньше 0), то переменная 5 получит значение 1, так как внутренняя часть этого цикла выполняется, по крайней ме- ре, один раз. Оператор завершения break ; применяется при необходимости безусловного завершения вы- полнения ближайшего вложенного оператора while, оператора for или оператора do-while. Так, для фрагмента программы
202 Приложение. КРАТКОЕ ВВЕДЕНИЕ В ЯЗЫК СИ i-i; while (i <в п) {5 -н= i; if (s > MAX) goto ready; /++;} ready. тот же эффект может быть получен без применения оператора безусловного перехода goto, например i-1; while (i <= п) {s 4- г; г/ (5 > MAX) break; г++;} или for (i - 1; i <= п; *++) {5 «н-1; j/ (s > МАЮ break ;} Для немедленного перехода на проверку условия завершения цикла можно использовать оператор продолжения continue ; Например, оператор while (a < b) {а++; if (а < р) {а = 2; Ъ -и» 1;}} можно заменить на while (а < Ъ) {я++; if (a >=p) continue ; а += 2; ft +«= 1;} А.З. ОПЕРАТОРЫ И ВЫРАЖЕНИЯ Символы +, -, < используются для обозначения операций в выражениях, например a+b-c<d Запись x + y*z означает, что результат умножения у *z должен быть прибавлен к значению переменной х. Конечно, операция умножения * име- ет более высокий приоритет, чем операция сложения +. Опера- ции, имеющие одинаковый приоритет, обычно выполняются в порядке записи слева направо. Например, выражение a-b+c-d эквивалентно <te-ft)+c)-rf Однако есть операции, которые выполняются в порядке спра- ва налево. Примером может служить операция присваивания =, следовательно, оператор присваивания /—/—* —30; даст такой же эффект, как 1-</-<*-эо»;
A.3. ОПЕРАТОРЫ И ВЫРАЖЕНИЯ 203 Заметим, что символ ■» не должен использоваться для провер- ки равенства двух величин, для этой цели служит операция «■ «. Перечислим теперь все операции языка Си; смысл некоторых из них ясен немедленно, другие будут пояснены позже. Опера- ции перечислены в порядке уменьшения приоритета, но для группы операций, расположенных между двумя соседними гори- зонтальными линиями, приоритеты одинаковы; в таблице также указан порядок выполнения операций внутри группы (слева на- право и СПРАВА НАЛЕВО) при отсутствии скобок. () Вызов функции Слева направо [ ] Выделение элемента массива Выделение элемента структуры или объединения -> Выделение элемента структуры, адресуемой указателем (Следующие операции обычно называются унарными: они имеют только один операнд. Следует отметить, что симво- лы -,&, * также используются и в качестве бинарных опе- раций) ! Логическое отрицание СПРАВА НАЛЕВО ~ Побитовое отрицание - Изменение знака ++■ Увеличение на единицу — Уменьшение на единицу & Определение адреса * Обращение по адресу (тип) Преобразование типа sizeof Определение размера в байтах ф / % + « » Умножение Деление Деление по модулю Сложение Вычитание Сдвиг влево Сдвиг вправо (остаток) Слева направо Слева направо Слева направо < Меньше чем Слева направо , <= Меньше или равно > Больше чем >= Больше или равно
204 Приложение. КРАТКОЕ ВВЕДЕНИЕ В ЯЗЫК СИ т= & •\ i && м ?; з *= » Равно Не равно Побитовая операция И Операция исключающее ИЛИ Побитовая операция ИЛИ Логическая операция И Логическая операция ИЛИ Условная операция " /- %- +- -- «- »- &= ~= Присваивание Операция запятая Слева направо Слева направо Слева направо Слева направо Слева направо Слева направо СПРАВА НАЛЕВО 1 = СПРАВА НАЛЕВО Слева направо Выражение может не только изменять значения, но и выпол- нять действия, которые изменяют состояние. Следующая строка, например, является выражением i = i + 1 Это выражение с присваиванием, которое увеличивает значе- ние переменной /на 1. Менее очевидно, но это выражение также имеет и значение, а именно — новое значение переменной и Следовательно, имеет смысл запись /-5*<1-*И)+2; которая, в частности, может быть заменена на /-5*(++0+2; Оба выражения ++*' и /++ означают, что значение переменной увеличивается на единицу. Однако выражения вырабатывают различные значения: ++i означает приращение переменной i и использова- ние ее нового значения; /++ означает использование старого значения пере- менной i и затем приращение этой переменной. Так, после выполнения операций т = 5; пш (т++); будем иметь i=/ = 6, т = 6, п = 5.
А.З. ОПЕРАТОРЫ И ВЫРАЖЕНИЯ 205 Выражение присваивания и знак точки с запятой (;), именно в этом порядке, образуют оператор присваивания. Так, i-1 + l; обозначает не выражение, а оператор (аналогичный смысл при- обретает запись /++;). Если требуется выполнить несколько дей- ствий в контексте, в котором допускаются только выражения (но не операторы), можно применять операцию запятая. Рассмот- рим, например, цикл while 0* + = у,у --,* = / + 2 * у Д > 0) у *= 2; Он работает аналогично строке again : i + =у ; у - -; к = i: + 2 * у ; if (к > 0) {у *=2; goto again ;} (запись/*= 2 означаету=у * 2, у- = 3 означаету=у- 3 и так далее). Пара символов ? и : образует другой, не совсем обычный, но очень удобный оператор. В так называемом условном выражении cond ? exprl : exprl вычисляется первое выражение cond, означающее условие. За- тем вычисляется либо exprl, либо exprl в зависимости от того, будет ли значение cond не нулевое или нуль, соответственно. Напомним, что в языке Си значение true ("истина") обозначает- ся через 1, a false ("ложь") — через 0, так что вместо if (x > у) max = x\ else max = у; можно записать max = (х > у ? х : у); Очень важно различать логические и побитовые операции. Логические операции применяются очень часто, например в та- ком выражении (х<0 I I ;y>0&&;y<l)&&!(z<0) которое следует читать как (Ос < 0) или ((у >0)и(у< 1))) и не (z < 0) Для логических операций && и I I гарантируется, что второй операнд вычисляется только в том случае, если первого недоста- точно для принятия решения, какой будет конечный результат. Поэтому можно не беспокоиться, что при выполнении операции i>0&&j/i >k может произойти деление на нуль.
206 Приложение, КРАТКОЕ ВВЕДЕНИЕ В ЯЗЫК СИ Результатом вычисления логических выражений всегда будет 0 или 1. Но в побитовых операциях всегда имеется в виду вся по- следовательность битов операндов, даже если указываются цело- численные переменные, как, например / = 5; у = /«2; k = i I у; Здесь использованы операции "сдвиг влево" («) и "побитовая операция ИЛИ" (I). Значения переменных i, у, к можно предста- вить в двоичном формате 1 = 0... 000101 (= 5) у = 0...010100 (=20) * = 0... 010101 (=21) Операция преобразования типа может применяться для при- нудительного преобразования типа переменной. Так, значение выражения (int) 3.95 будет равно 3 типа integer ("целый"). Если обе переменные i и у имеют тип integer, то результат де- ления i /убудет иметь целое значение усеченного частного. Так, 8/3 имеет значение 2 типа integer, даже в том контексте, где ожи- дается тип float, как в выражении х = 8 / 3, где х имеет тип веще- ственной цеременной. Если, по крайней мере, один из операндов а и Ь имеег\ип float, то частное alb также имеет тип float и не усекается. Константы с плавающей точкой содержат десятичную точку или букву е (или Е); целые константы их не содержат. Так, 1.5еЗ/50 имеет значение 30.0 7/4.0 имеет значение 1.75 7/4 имеет значение 1 Операция деления по модулю (%) может применяться только для целочисленных операндов: 37 % 5 имеет значение 2 37 % 5.0 недопустима (int) 37.9 % 5 имеет значение 2 В последнем примере оператор преобразования типа (int) имеет приоритет перед операцией деления по модулю %. Если значение с плавающей точкой присваивается целочис- ленной переменной, то будет иметь место усечение. Оператор * = 3.9;
A.4. ЛЕКСИЧЕСКИЕ ВОПРОСЫ И СТРУКТУРА ПРОГРАММЫ 207 присваивает значение 3 переменной и Если переменная х имеет тип float, то при выполнении следующего оператора переменной х будет присвоено значение 7 (превращенное в число с плаваю- щей точкой 7.0) х = 39/5; А.4. ЛЕКСИЧЕСКИЕ ВОПРОСЫ И СТРУКТУРА ПРОГРАММЫ В большинстве случаев пробелы и переход на новые строки не оказывают никакого влияния на смысл программы. Последова- тельность символов вида компилятором игнорируется. Так обозначаются комментарии, предназначенные для человека. Идентификаторы, например, имена переменных, состоят из букв латинского алфавита и цифр, но первым символом должна быть буква. В языке Си символ подчеркивания <_) также счита- ется буквой. Приняв это во внимание и различая прописные и строчные буквы, мы насчитаем 53 различные буквы. Идентификаторы могут также применяться для обозначения констант, например #define MAXIMUM 1000 После такой "препроцессорной управляющей строки1' можно будет использовать идентификатор MAXIMUM просто как иное обозначение для числа 1000. Это только первый пример макро- команды. Вот более интересная макрокоманда, названная МАХ и представляемая строкой #defineМАХ 0с,у) х>у1 х:у которая означает, что в любом месте программы любая встреча- ющаяся строка вида MAXia, Ъ) автоматически заменяется на строку а>Ыа:Ь Во избежание недоразумений в более сложных случаях реко- мендуется применять скобки, что для данной макрокоманды могло бы выглядеть как #define МАХU, у) (Ос) > (у) ? (х) : (у))
208 Приложение. КРАТКОЕ ВВЕДЕНИЕ В ЯЗЫК СИ Существуют управляющие строки для включения файлов. Часто применяется строка ^include <stdio.h> Результат ее действия заключается в замене этой строки содер- жимым файла с именем stdio.h, который имеет название "файл заголовков для стандартного ввода/вывода". Если используются математические функции, такие как cos и sin, то в программу необходимо включить управляющую строку #include <math.h> Программа обычно содержит одну или несколько функций. В языке Си нет подпрограмм или процедур, а только функции. Да- же основная программа является функцией, имеющей имя main. Это просто терминологический вопрос, поскольку функции со- всем не обязательно должны возвращать значение и к ним можно обращаться точно так же, как в других языках — к процедуре. Это показано в следующей программе, которая считывает двух- мерные ортогональные координаты двух точек Р и Q и вычисляет расстояние между этими двумя точками. /* Эта программа вычисляет расстояние между */ /* двумя заданными точками Р и Q */ /* This program computes the distance between */ /* two given P and Q */ #include <m ath. h> maini) { float xP,yPyxQyyQ; printfCЗадайте хР, yPy xQ, yQ:") scan/ <"%/%/ %f%f\ &xP, &yP, &xQ, &yQ); printjlistance (xP, yP, xQ, yQ); } print jlistance Oc 1, y\, jc2, yl) float xl, yl, jc2, yl { float deltajc, delta_y> distance; delta_x = xl - xl; delta_y = yl - yl; distance = sqrtideltajc * deltajc + delta_y * delta_y); print ("Расстояние: %f\n\ distance); } Стандартные функции scanf, print, scrt будут описаны в парагра- фе А.9.
A.5. МАССИВЫ И УКАЗАТЕЛИ 209 А.5. МАССИВЫ И УКАЗАТЕЛИ После объявления массива float a[5]; доступны следующие пять переменных типа float: fl[0],fl[l],a[2],a[3Lfl[4] Можно также записать д[/], где индекс / — любое целочис- ленное выражение, значение которого не может быть ни отрица- тельным, ни больше 4. Индексы всегда начинают счет с 0. Между скобками в объявлении массива могут появиться только целые константы. В объявлениях вместо чисел часто используются имена констант, как в следующем примере, где также показано, что массивы могут иметь более одного индекса: # define NROWS 10 #define NCOLUMNS 8 int table[NROWS][NCOLUMNS\\ for(i = 0;i<NROWS;i++) for (j = 0; j < NCOLUMNS; /f+) { ... table[i]\j].. } Если v — переменная, то &v будет адресом этой переменной или, говоря более формально, обозначение &vопределяет указа- тель на переменную v. Если р — указатель на некоторый объект, то этот объект обозначается выражением *р. В следующей про- грамме демонстрируется, как можно объявить и использовать переменную в виде указателя. maini) { int U *р; /=123;/7 = &/;*р = 789; } Это не совсем практичная программа, но она показывает, как можно получить доступ к переменной, не используя ее имени. Здесь р является указателем на переменную г, поэтому выраже- ние *р эквивалентно L Это означает, что переменная i в конце выполнения программы будет иметь значение 789 вместо 123.
210 Приложение. КРАТКОЕ ВВЕДЕНИЕ В ЯЗЫК СИ Имя массива может быть использовано в качестве указателя на его первый элемент. Так, вместо &Д[0] можно просто запи- сать А. Если есть указатель на какой-либо элемент массива и к нему добавить единицу, то получится указатель на следующий элемент (независимо от количества байтов, занимаемых каждым элементом массива), следовательно, вместо &Л[i] можно просто записать А + и При объявлении массивы можно инициализировать, если для них отводится постоянное место в памяти, то есть когда массивы внешние или статические. Внешние переменные объявляются на самом внешнем уровне, вне функций. В следующем примере программы инициализируются внешние массивы X и У, затем печатается число 5.75 float X [4 ] - {6.0, 6.0, 5.9, 6.1}, У[4]= {-0.25,0.25,0.0,0.0}; maini) { printfC %An", X[0 ] + У[0 ]) } Статическим переменным при объявлении должно предшест- вовать ключевое слово static. В следующем примере программы массивы X и У являются внутренними для функции main, но они объявлены статическими, так уто для них отводится постоянное пространство в памяти и, следовательно, они могут быть инициа- лизированы maini > { static float Х[4 ] - {6.0,6.0,5.9, 6Л}, У[4]= {-0.25,0.25, ОД 0.0}; printC%An",X[0]+Y[0]); > А.6. ФУНКЦИИ В языке Си функций иногда могут иметь значение. Если фун- кция должна иметь возвращаемое значение, то оно присваивает- ся ей с помощью оператора возврата return. Как это следует из самого названия, обращение к этому оператору вызывает немед- ленный выход и завершение работы функции. Поэтому следую- щие две функции дают один и тот же результат intf(x) float x\ {if(x < 0) return -1; else return 1;} intf(x) float x; {ifix < 0) return—I; return 1;}
Л.6. ФУНКЦИИ 211 Для выполнения этого же действия можно написать и более ко- роткий вариант с применением условного выражения intf(x) float x; {return x < О ? -1; 1;} Обратите внимание на обозначение того, что функция имеет целое значение функции и аргумент с плавающей точкой. Опре- деленная так функция может быть вызвана обычным образом, как в примере n = Z*fiX*Y)+fiX)\ где X и У — переменные с плавающей точкой. Если тип аргумен- та объявлен как float, то нельзя обращаться к функции с аргу- ментом типа integer. To есть, если переменная i имеет тип integer, то запись fix) будет некорректным обращением к определенной выше функции/. Если функция не предназначена для возвращения значения, то нет необходимости включать в нее оператор return. Но и в этих случаях можно воспользоваться оператором возврата для немед- ленного выхода из функции. Для этого нужно просто опустить выражение между ключевым словом return и точкой с запятой. В большинстве случаев нужна только передача числовых зна- чений аргументов в функцию. Но иногда требуется так организо- вать работу функции, чтобы она присваивала значения перемен- ным, которые доступны ей только через аргументы. Это можно реализовать, допуская, что аргументами будут указатели на пе- ременные. В следующей функции выполняется взаимный обмен значениями между двумя целочисленными переменными interchange(p> q) int *p> *q; { intaux ; qux = *p ;*p = *q ;*q — aux ; } После выполнения строки программы 1-15 7-2; interchangei&i, &j); значениями переменных i и / будут 2 и 1, соответственно. Заме- тим, что аргументы &i и &J представляют собой указатели, кото- рые обозначаются через р и q внутри функции. Значения двух элементов целочисленного массива А [к ] и А [т ] можно взаимно поменять друг на друга interchangeiA + к> А + т); Напомним, что А + к является сокращенной записью для &А [к ].
212 Приложение. КРАТКОЕ ВВЕДЕНИЕ В ЯЗЫК СИ Говорят, что переменная аих в вышеприведенной функции относится к автоматическому классу памяти, то есть для нее не отводится постоянное место в памяти. Кроме того, существуют статический (static) и внешний (external) классы памяти, для переменных такого вида отводится постоянное место в памяти. Массивы автоматического класса не могут быть инициализиро- ваны. Простые переменные можно инициализировать всегда. Ес- ли переменная автоматическая и объявлена в функции F, то она будет иметь это инициализируемое значение при каждом обра- щении к функции F. Если же она объявлена статической, она получит указанное значение только при первом обращении к функции. Если функция завершает работу, а после к ней снова обратились, то статическая переменная будет иметь свое послед- нее значение, тогда как автоматическая переменная является неопределенной (если она не инициализирована). А.7. СТРУКТУРЫ Несколько переменных можно сгруппировать вместе в так называемую структуру. Предположим, что нужно записать в память некоторую информацию о точках в двухмерном про- странстве. Для каждой точки эта информация состоит из ортого- нальных координат х и у и кода, показывающего, лежит ли точка внутри определенного треугольника. Предположим, что имеем две точки Р и Q. Тогда можно объявить struct {float х, у ; int inside ;} Р, Q ; Теперь можно использовать компоненты структуры Р и Q со- вершенно так же, как и другие переменные, например Р.х = 1.5; Р.;у = 0.8; P.inside = 1; Q.jc = 2*P.x; Вместо такого объявления можно записать либо struct point {float х> у ; int inside ;} Р, Q ; либо struct point {float дс, у ; int inside ;}; struct point Py Q ; Две последние версии имеют то преимущество, что тот же са- мый тип структуры впоследствии можно использовать в простой записи struct point (как в последней строке объявления Р и Q) вместо указания полной записи struct {float jc, у; int inside;}. За-
A.8. ДИНАМИЧЕСКОЕ РАСПРЕДЕЛЕНИЕ ПАМЯТИ 213 метим, что при таком объявлении нужно обязательно использо- вать ключевое слово struct. Но можно использовать и другое средство, которое в общем случае позволяет задать тип для нового имени. Здесь можно за- писать typedef struct {float х, у ; int inside ;} POINT ; Теперь POINT будет новым именем для нашего типа структуры и далее можно пользоваться новым видом объявления POINT Л Q ; Массив Л, например, для 1000 точек может быть объявлен как struct \floqt х, у; int inside;} А [1000 ]; или, если выше было объявлено имя pointy struct point Л [1000]; или, если применен оператор typedef, POINT A[1000]; Теперь каждый элемент массива A[i] является структурой, со- стоящей из трех компонентов A [i ].х, A [i ].y, A [i ].inside. Структуры особенно полезны совместно с динамическим рас- пределением памяти, поскольку они могут содержать указатели на другие структуры. Таким образом можно строить списки, де- ревья и так далее. Если S — структура, содержащая поле указа- теля р, то этот указатель обозначается S.p, а указываемый объ- ект — *(S.p). Для последнего выражения имеется специальное обозначение S->p где два символа - и > напоминают стрелку и образуют единый символ операции. А.8. ДИНАМИЧЕСКОЕ РАСПРЕДЕЛЕНИЕ ПАМЯТИ Предположим, что было объявлено char*p, *malloc(), *realloc(); Тогда р будет указателем на символ и можно записать p = malloc(n); где п обозначает некоторое положительное целочисленное выра- жение, указывающее на количество байтов. Действие этого опе- ратора заключается в том, что, по возможности, для п символов отводится непрерывный участок памяти. Если требуемый объем
214 Приложение. КРАТКОЕ ВВЕДЕНИЕ В ЯЗЫК СИ памяти недоступен, то переменной р будет присвоено значение NULL — специальное значение для "пустого" указателя, не ука- зывающего на реальный объект; это позволяет выполнить про- верку (например, для выдачи сообщения о недостаточном объеме памяти) if (p = = NULL) {... /*Insufficient memory */ /*Недостаточно памяти */} Отведенные таким путем п байтов теперь доступны через указа- тель р. Пусть теперь необходимо поместить букву Q в i-ю позицию (О < i < п - 1) массива. Для этого можно записать ♦(Р + 0-ЧУ; Отведенную оператором malloc память можно освободить опера- тором free(p); Предположим, что блок из п байтов, на который указывает р, оказался слишком малым и его необходимо расширить до желае- мопнювого размера N(>n). Для этого можно записать p = realloc(p>N); Теперь, если указатель р не имеет значение NULL, то он указы- вает на блок из N байтов, причем первые п байтов этого блока будут иметь то же содержимое, которое в них было записано прежде. Память может отводиться и для других типов данных. Пред- положим, что нам нужна последовательность из к целых чисел. Поскольку стандартная функция malloc должна обязательно знать, сколько необходимо байтов, то нам интересно узнать, сколько байтов отводится для целого числа. Это число определя- ется машинно-независимым способом операцией sizeof(int) Другая сложность связана с типом указателей, которые необ- ходимы. Нам нужен указатель на целое вместо указателя на символ. Однако функция malloc связана с указателем на символ. Преобразование типа может быть осуществлено с помощью опе- ратора преобразования типа. Следовательно, запишем Ш *р; р = (int *) malloc(к * sizeof(int));
A.9. ВВОД/ВЫВОД 215 Здесь оператор преобразования типа (ш**) говорит о том, что желаемым типбм является указатель на целое. Целое число с номером/ 0=0» 1> ••• Л ~ 1) в этой последовательности обознача- ется через *(р+/). А.9. ВВОД/ВЫВОД В языке Си нет специальных конструкций для ввода и выво- да. Вместо этого применяется ряд стандартных функций вместе с предопределенным типом структуры, имеющей имя FILE. Все подробности об этом включаются в нашу программу очень удоб- ным образом, используя строку препроцессора ^include <stdio.h> Файл заголовков stdio.h содержит декларацию typede/в виде typedef struct {...} FILE; таким образом, если напишем FILE*fPl то переменная fp будет иметь тип "указатель на файл FILE", при этом компьютер будет знать все подробности о типе структу- ры FILE. Структура фактически доступна через указатель fp по- сле того, как файл открыт оператором fp= fopen (file-name, mode); в котором аргументы имеют следующий смысл: file-name - строка, содержащая имя файла в том виде, в ка- ком он записан в каталоге; mode - символы "г" или "w" для форматированного вво- да или вывода; при неформатированном вводе- выводе формат обозначения зависит от исполь- зуемого компилятора. Симметричной для функции fopen является функция fclose; функция fopen подключает файл к программе, а функция fclose отключает его. Из следующего примера программы видно, как можно, что-нибудь записать в файл EXAMPLE. Если до этого та- кой файл не существовал, то он будет создан. ^include <stdio.h> maini) { FILE*fp; int i;
216 Приложение. КРАТКОЕ ВВЕДЕНИЕ В ЯЗЫК СИ fp = /open ("EXAMPLE", " w"); /or (Ы;/<=4;/++) fprintf (fp, "i -% 1J Л- %2d V\ i, i*i); /close(fp);} После выполнения этой программы будет создан файл EXAMPLE со следующим содержимым: i-1 Л- 1 1-2 Л- 4 1-3 Л- 9 f-4 Л-16 Вместо функции fprintf мы можем задать функцию printf, опустив первый аргумент. Тогда результат появится на экране дисплея нашей рабочей станции вместо записи на диске. В этом случае полная программа могла бы иметь вид main () { inti; /or (i-l;/<-4;i++) printf("i=%Id i*i=%2d\ri', U Л); } Фактически запись printf(...) эквивалентна записи fprintf (stdout,...), где stdout — указатель файла для стандартно- го вывода, объявленного в хидерном файле stdio.h. Первым аргу- ментом функции printf (вторым аргументом функции fprintf) будет форматная строка, содержащая часть текста, который дол- жен появиться в литеральной форме на выходе, и элементы фор- мата, относящиеся к остальным аргументам. Здесь элементами формата являются обозначения % \d и %2. Они определяют, что значениями переменных / и i*i будут целые числа, которые дол- жны быть напечатаны в одной и двух десятичных позициях соот- ветственно. Все другие символы в форматной строке печатаются буквально, включая пробелы, символ новой строки обозначается через \п. В элементе формата букву d необходимо заменить на букву /, если ассоциированный элемент данных имеет тип float вместо integer. Чтение данных производится при обращении к аналогичным функциям fscanf(fp> format-string,...) если данные находятся в файле на диске, или scan/(format-string,...)
A.9. ВВОД/ВЫВОД 217 если данные должны быть введены с клавиатуры пользователя. Однако остальные аргументы теперь должны быть указателями, поскольку функции fscanf и scan/ должны иметь возможность присваивать значения переменным через эти аргументы. Напри- мер, если некоторое число должно быть задано пользователем, то можно записать printfCВведите число:"); scanf("%d", &number); (отсутствие символа & в обозначении переменной означало бы серьезную ошибку). Если переменная х имеет тип float, а пере- менная хх — тип double, то их значения считываются следую- щим образом: scanf("%f %lf, &х, &xx); В обозначении формата %//необходима буква /, так как &хх имеет тип "указатель на double". Значение функции, возвращаемое функцией fscanf, равно числу элементов, которые были считаны при их наличии. Оно равно нулю или отрицательному числу, если ничего нельзя было прочесть. Этот факт можно использовать для проверки достиже- ния конца файла. Аналогично указателю stdout имеется стандартный указа- тель файла stdin. Два обращения scanfi...) и fscanf(stdin,...) — эквивалентны. Одиночный символ может быть прочитан и записан более примитивным образом: ch = getc (fp); (ввод с диска) putcich, fp); (вывод на диск) ' ch = getchari); (ввод непосредственно с клавиатуры) putchar(ch); (вывод непосредственно на дисплей) Здесь запись getchari) совершенно эквивалентна записи getc(stdin), а запись putchar(ch) — записи putcich, stdout). Если мы проверим последний прочитанный символ и решим, что он должен быть использован еще раз, то его можно вернуть обратно во входной поток, записав ungetc(ch,fp); Функции printf, fprintf, scanf, fscanf выполняют так называе- мый форматированный ввод/вывод, на что указывает последняя буква / в их именах. Форматированные файлы имеют линейную
218 Приложение. КРАТКОЕ ВВЕДЕНИЕ В ЯЗЫК СИ структуру и числа в них представляются в виде последовательно- сти символов. Для файлов на диске, которые должны читаться и записываться только нашими собственными программами, будет более эффективно использовать бесформатный ввод/вывод. Это означает, что внутреннее и внешнее Представление данных идентично, поэтому числа вернее всего будут записываться в виде двоичных слов фиксированной длины. Для бесформатного ввода/вывода применяются функции /read (bufptr, size, л,/р); /write(bufptr, size, nyfp); Аргументы имеют следующие типы и смысл: bufptr указатель на char — указатель на блок памяти (иногда называемый буфером), size int — размер в байтах одного элемента данных, п int — количество элементов данных в буфере, fp указатель на FILE — указатель файла. Эти две функции выдают в качестве значения функции целое число, равное числу элементов данных, которые были прочита- ны или записаны. Как и в случае с функцией fscanf, оно может быть использовано для проверки, не было ли попытки прочитать данные после конца файла, поскольку тогда попытка чтения бу- дет завершена безуспешно и значение функции /read будет рав- но нулю. АЛО. СТАНДАРТНЫЕ МАТЕМАТИЧЕСКИЕ ФУНКЦИИ Строго говоря, доступный лабор предопределенных функций не является частью языка. Однако на практике удобно иметь на руках список таких функций. Мы часто используем математиче- ские функции, включенные в следующий список, в котором пе- речислены имена функций и типы аргументов вместе с кратким пояснением, что данная функция вычисляет: double cos(x) double х\ /* косинус */ double sin(x) double x; /* синус */ double tan(x) double x; /* тангенс */ double log(x) double x; /* логарифм натуральный */
A. 10. СТАНДАРТНЫЕ МАТЕМАТИЧЕСКИЕ ФУНКЦИИ 219 double sqrt(x) double x; double floor (x) double x; double ceil(x) double x; ^ int abc(i) int i; double fabs(x) double x; double acos(x) double x\ double asin(x) double x; double atan(x) double x; srand(seed) int seed; int rand (); long int time(p) long int p\ /* корень квадратный f /* ближайшее меньшее целое, /* например floor(4.9) - 4.0 /* ближайшее большее число, /* например сеИ(4Л) =5.0 /* модуль целого числа /* модуль числа с плавающей /* точкой двойной точности /* арккосинус /* арксинус /* арктангенс /* инициализация генератора /* случайных чисел rand () /* генератор случайных чисел /* время в секундах, отсчиты- /* ваемое от 1 января 1970 г., /* 0.00 час по Гринвичу */ ,■•/ ♦/ V V V */ V V V V V V V V V V
ЛИТЕРАТУРА Ammeraal, L. (1986). С for Programmers, Chichester: John Wiley & Sons. Ammeraal, L. (1987). Computer Graphics for the IBM PC, Chichester: John Wiley & Sons. Ammeraal, L. (1987). Programs and Data Structures in C, Chichester: John Wiley & Sons. Angell, I. O. (1981). A Practical Introduction to Computer Graphics, London: Mac- millan. [Имеется перевод: Энджел Й. Практическое введение в машинную графику. — М.: Радио и связь, 1984] Ayres, F. Jr (1967). Schaum's Outline Series, Theory and Problems of Projective Geometry, New York: McGraw-Hill. Escher, M. C, etal. (1972). The World of M.C. Escher, New York: Harry N. Abrams. Feuer, A., and N. Gehani (eds) (1984). Comparing and Assessing Programming Lan- guages Ada С Pascal, Englewood Cliffs, NJ: Prentice-Hall. Foley, J. D., and A. van Dam (1982). Fundamentals of Interactive Computer Graphics, Reading, Mass.: Addison-Wesley. [Имеется перевод: Фоли Дж., вэн Дэм А. Основы интерактивной машинной графики: В 2-х книгах. — М.: Мир, 1985. ] Forsythe, G. E., M. A. Malcolm and С. В. Moler (1977). Computer Methods for Math- ematical Computations, Englewood Cliffs, NJ: Prentice-Hall. Hopkins, E. J., and J. S. Hails (1953). An Introduction to Plane Projective Geometry* Oxford: The Clarendon Press. Kernighan, B. W., and D. M. Ritchi (1978). The С Programming Language, Engle- wood Cliffs, NJ: Prentice-Hall. [Имеется перевод: Керниган Б., Ритчи Д. Язык программирования Си. — М.: Финансы и статистика, 1985.] Kreyszig, E. (1962). Advanced Engineering Mathematics, New York: John Wiley && Sons. McGregor, J., and A. Watt (1984). The Art of Microcomputer Graphics for the BBC Micro/Electron, Reading, Mass.: Addison-Wesley. » Newman, M.N., and R.F. Sproull (1979). Principles of Interactive Computer Graphics, New York: McGraw-Hill. [Имеется перевод первого издания (1973): Ньюмен У., Спрулл Р. Основы интерактивной машинной графики. — М.: Мир, 1976.] Plum, Т. (1983). Learning to Program in C, Englewood Cliffs, NJ: Prentice-Hall.
предметный указатель Автоматический класс памяти 212 Аргументы программы 105 Бесконечно удаленная точка 70, 95 Ввод/вывод 34, 215 Вектор 50 столбец 18 строка 18 Векторное произведение 56 Вертикальные линии 111 Видимости, тесты 129 Видовое преобразование 84 Видовые координаты 84 Винтовая лестница 184 Внешние массивы 15, 210 Выпуклый полигон 59 Выражение 202 Генерация кривых 32 Гладкость кривых 43 Глаз 83 Горизонт 82 Грани 140 Границы рисунка 11 Грань задняя 123 Двойная точность 134, 199 Детерминант 53, 124 Диагональ 60 Динамическое распределение памяти 213 Длина вектора 51 Задняя грань 123 Истина 27, 200 Картинки размер 93*' Квадратичная функция 193 Константа NULL 204 Координаты мировые 20, 83 экранные 83 Кривых сглаживание 43 Куб 98,118 Линии вертикальные 113 Ложь 27, 200 Массив 16,209 Масштабирование 22, 31 Матричная запись 18 Мировые координаты 20, 83 Наблюдения, конус 97 направление 84,111 точка 84 Невидимые линии 117, 124 Невыпуклый полигон 59 Неформатированный ввод/вывод 35 Новая строка, символ 216 Нормаль (вектор) 122 Нулевой вектор 51 Область вывода 21 Объекта размер 93 Однородные координаты 19, 66 Окно 20 Оператор break 201 do while 201 return 210 sizeof 35,214 typedef 2\5 Оператор присваивания 200 Операторы 202
222 ПРЕДМЕТНЫЙ УКАЗАТЕЛЬ Описатель char 199 float 16.199 Описатель int 199 static 212 Ортогональная проекция 91 Отверстие 141, 176 в цилиндре 179 Отсечение 23 Параметр argc 105 argv 105 Переменная epsilon 134 Перенос 13,74,86 Перспектива 83 Перспективное преобразование 84,91 Пиксел 140 Пирамида 125 Пифагорово дерево 39 Поверхность 192 Поворот 14, 16, 74 положительное направление 74 Подбор размеров 30 Полигон 59, 141 Полусфера 191 Правая система координат 51,56 Преобразование видовое 84 перспективное 84, 91 Препроцессор 175,207 Призма 179 Проволочная модель 103 Программа GENPLOT 35 HI DUN 135 HIDUNPX 160, 175 Программы, структура 207 Проективная геометрия 66, 94 Производные 43 Псевдоперспектива 96 Расстояния в треугольнике 156 Рекурсия 37 Сглаживание кривых 43 Си, язык программирования 199 Скалярное произведение 52 Случайное число 33, 217 Совмещение преобразований 19 Спираль 182 Список, линейный 131 Сплайн, В-сплайн 43 Сплошное тело 102,118 Стержень 182 Страж 160 Структура 32,212 Сферические координаты 77,144 Тетраэдр 118 Тор 188 Точка схода 82 Точность представления чисел 134 Треугольник, ассоциированный 147 Треугольники, декомпозиция 59 Указатель 35,150, 209 Условное выражение 205 Устойчивость программы к ошибкам 63,117 Файл 34, 215 stdiah 208 Функция 11, 193, 210 draw И fc endgr 11 fobs 219 /close 35, 215 /open 35, 215 fprintf 35,216 /read 35,218 /scan/ 35,107,216 /write 35,218 getc 217 get char 217 init_yiewport 37 initgr 11 main 15 malloc 158,213 move 11 print/ 35,216 rand 34,219 realloc 158, 213 scan/ 35,216 srand 34, 219 time 219 ungetc 217 Файла конец 107, 218 Форматированный ввод/вывод 35 Цилиндр 179 Часовой стрелки, направление обхода против 58,142 Штриховая линия 64,174 Экран 83,92,145 расстояние до 93 Экранные координаты 83
Оглавление ПРЕДИСЛОВИЕ 5 Глава 1. ВВЕДЕНИЕ 7 1.1. Мотивация необходимости графического программирования 7 1.2. Программирование графики н£ языке Си 8 Упражнения 12 Глава 2. ДВУХМЕРНЫЕ АЛГОРИТМЫ 13 2.1. Преобразование и новые координаты 13 2.2. Поворот 16 2.3. Матричная запись 18 2.4. Окна и области вывода 20 2.5. Отсечение линий 23 2.6. Автоматический подбор размеров и позиции 30 2.7. Применение рекурсий 37 2.8. Сглаживание кривых 43 Упражнения 48 Глава 3. ГЕОМЕТРИЧЕСКИЙ ИНСТРУМЕНТ ДЛЯ АЛГОРИТМОВ ТРЕХМЕРНОЙ ГРАФИКИ 50 3.1. Векторы 50 3.2. Скалярное произведение 52 3.3. Детерминанты 53 3.4. Векторное произведение 56 3.5. Декомпозиция полигонов на треугольники 59 3.6. Однородные координаты" 66 3.7. Перенос и повороты в трехмерном пространстве ч 74 Упражнения 80 Глава 4. ПЕРСПЕКТИВНЫЕ ИЗОБРАЖЕНИЯ 82 4.1. Введение 82
4.2. Видовое преобразование 84 4.3. Перспективные преобразования 91 4.4. Программа для вычерчивания куба 98 4.5. Вычерчивание проволочных моделей 103 4.6. Направление наблюдения,бесконечность, вертикальные линии 111 Упражнения 115 Глава 5. УДАЛЕНИЕ НЕВИДИМЫХ ЛИНИЙ 117 5.1. Анализ эффективности 117 5.2. Входные данные и внутреннее представление 118 5.3. Алгоритм определения невидимых линий 125 5.4. Полигоны и пикселы 141 5.5. Улучшенная программа 157 Упражнения 174 Глава 6. ПРАКТИЧЕСКИЕ ПРИМЕРЫ 175 6.1. Введение 175 6.2. Полый цилиндр 179 6.3. Стержни по спирали 182 6.4. Винтовая лестница 184 6.5. Тор , 188 6.6. Полусфера / 191 6.7. Функция двух переменных 193 Упражнения 197 Приложение. КРАТКОЕ ВВЕДЕНИЕ В ЯЗЫК СИ 199 А. 1. Основные типы данных 199 А.2. Некоторые операторы 200 А.З. Операторы и выражения 202 А.4. Лексические вопросы и структура программы 207 А.5. Массивы и указатели 209 А.6. Функции 210 А.7. Структуры 212 А.8. Динамическое распределение памяти 213 А.9. Ввод/вывод 215 А. 10. Стандартные математические функции 218 Литература 220 Предметный указатель 221