У 1998 році Looking Glass Studios випустила стелс-гру Thief: The Dark Project. У той час апаратне 3D-прискорення тільки зароджувалося, тому в процесі розробки воно не використовувалося, гра рендерилася тільки програмно.
Я був основним автором базової технології рендерінга Thief (хоча я і не писав рендерери об'єктів і персонажів), а також пов'язаних з нею елементів. Той же движок рендерінга, модифікований іншими людьми для використання апаратного 3D-прискорення, також використовувався для рендерінгу System Shock 2 і Thief 2.
Рушій був написаний приблизно в один час з Quake (хоча гра вийшла набагато пізніше), і загальний зовнішній вигляд сильно нагадує Quake. Багато технологій були скопійовані з Quake або надихалися ним, але часто їх робота трохи або значно відрізнялася.
Програмний рендеринг Quake докладно задокументований Майклом Абрашем (Michael Abrash) у серії статей, які були перевидані в його книзі Graphics Programming Black Book. Техніки, використані в Thief, ніколи раніше не описувалися, і я буду радий нарешті розповісти про них, навіть якщо зараз вони абсолютно застаріли. По можливості, я спробую описати їх у зв'язку з більш відомими техніками Quake.
Важливі ігри того часу зі схожою технологією рендерінга:
- 22 червня 1996 - Quake 1 (QTest у лютому 1996 року)
- 22 травня 1998 - Unreal
- 19 листопада 1998 - Half-Life (зроблений на основі доповненого движка Quake 1)
- 30 листопада 1998 - Thief: The Dark Project
Зміст
- Видимість
- Зменшення перерисування
- Відсікання об'єктів
- Впорядкування об'єктів
- Освітлення та тіні
- Модель CSG
- Реалізація CSG
- Перспективне текстурування
- Ефекти текстурування
- Інше
Видимість
В іграх Looking Glass до Thief використовувалися світи на основі сіток. У System Shock 1 і Ultima Underworlds видимість розраховувалася проходженням по сітці. Однак Thief міг мати абсолютно довільну форму, тому його движок мені довелося розробляти з нуля (я почав роботу задовго до Thief і думав про движок навіть до того, як ми випустили Terra Nova: Strike Force Centauri).
Thief був заснований на ідеї розрахунку видимості і відсікання за допомогою порталів і комірок, опублікованій Сетом Теллером (Seth Teller) в його кандидатській дисертації 1992
року. Фактично рендерер світу Thief назвали «Portal», але оскільки у цього імені було нове популярне значення, я просто називав його «Thief» або «движок Thief» (але в движку було набагато більше, ніж просто рендерер, наприклад, система об'єктів, AI, фізика, і я не мав до цього ні найменшого відношення. Я не писав «движок Thief», тільки сам рендерер).
Спочатку я досліджував цю ідею як уявний Священний Грааль Looking Glass: розробку CRPG з відкритими і закритими просторами, оскільки останні CRPG компанії (Ultima Underworld 1 і 2, System Shock 1) були «закритими підземеллями». Але у нас був і движок з відкритими просторами для Terra Nova, до того ж багато хто з нас відчували бажання рано чи пізно створити гіпотетичну Underworld 3 або щось подібне з підземеллями, відкритими просторами, будівлями і всім подібним (це було ще до Daggerfall). Намагаючись уявити, як ми можемо вирізати отвір в ландшафті Terra Nova для додавання підземелій, я усвідомив, що портали можна використовувати для безпроблемної інтеграції декількох окремих рендерерів (наприклад, рендерерів відкритих і закритих просторів), розміщуючи їх на кордонах просторів. Тому за довгі різдвяні канікули я написав довгий документ, зафіксувавши свої думки. Думаю, потім ці думки роїлися у мене в голові, і я знав, що у нас немає ніяких ідей про те, як реалізувати світ без сіток. Тому здалося життєво необхідним спробувати написати весь рендерер закритих просторів за допомогою порталів. І для досліджень я створив одну реалізацію.
Ідея порталів і комірок використовувалася в Quake для попереднього розрахунку заздалегідь обчисленого набору потенційно видимих об'єктів (PVS, potentially visible set). Вона теж описана в дисертації Сета Теллера. Але в Thief вона застосовувалася в реальному часі: Thief заздалегідь обчислював портали і комірки, але не розраховував ніяку додаткову інформацію про видимість. (Вважаю, що в Descent, яка була випущена ще до початку роботи над движком Thief, це реалізовано, але я не знаю подробиць.)
Рушій Thief зберігав модель рівня з відкритими просторами (які гравець міг бачити і/або ходити по них), розділеними на випуклі багатогранники, звані «комірками». Кожна комірка містила від нуля і більш видимих «полігонів світу» вздовж кордонів осередку, які були видимими поверхнями світу. Комірки, поєднані з іншими осередками, мали особливі граничні полігони, звані «порталами», які позначали сусідство між осередками. Якщо гравець може бачити всередині заданої комірки, то він може бачити і всередині сусідніх комірок через портали в них.
Для визначення того, яка частина рівня видима в даний момент і вимагає рендерінгу, рушій виконував пошук в ширину порталів і комірок, починаючи з точки огляду. Після проходження всіх порталів досягнуті комірки додавалися до списку видимих. Кожен розглянутий портал перетворювався на 3D, потім його перевіряли на зверненість задньою стороною, якщо він не був звернений, то проектувався в 2D. Потім генерувався «граничний восьмикутник», що складається зі звичайних двомірних обмежуючих прямокутників і обмежуючого прямокутника, повернутого на 45 градусів.
Якщо портал вів з комірки A в комірку B, то ми порівнювали граничний восьмикутник порталу з граничним восьмикутником видимості комірки A. Якщо вони не перетиналися, то портал був невидимий. Якщо він був бачимо, то перетин восьмикутника комірки A і восьмикутника порталу був частиною комірки B, яка бачима через цей портал, тому додавалася до списку. (Було можливо, що гравець бачить всередині комірки B по різних шляхах, тому движок повинен був накопичувати всі видимі області, що він робив, просто зберігаючи консервативний граничний восьмикутник всіх восьмикутників вхідних шляхів уздовж шляхів. Якщо у гравця були два невеликі шляхи видимості в один осередок, шляхи, що знаходяться на протилежних кінцях осередку, то граничний восьмикутник ставав набагато більше, зазвичай розміром з усю комірку.) Комірка, що містить точку огляду, вважалася завжди видимою.
Quake заздалегідь обчислював схожу інформацію: для кожної комірки Quake зберігав список всіх інших комірок, видимих з будь-якої точки цієї комірки. Цей результат міг бути менш точним: наприклад, якщо у вас є довгий коридор з безліччю бічних кімнат, і цей коридор є однією коміркою, то Quake буде намагатися рендерити всі бічні кімнати, а Thief намагається відрендерити тільки вхідну комірку до кожної з бічних кімнат (оскільки ці комірки завжди сусідні з коридором і видимі), але Thief може відсікати самі кімнати, якщо в даний момент вони невидимі.
Повний аналіз порталів і осередків погано масштабується на більшу кількість полігонів, тому до часу виходу останньої гри на движку він пригальмовував (іншими словами, при апаратному прискоренні він сильно обмежував).
Зменшення перерисування
Quake зменшував перерисування за допомогою техніки буферизації інтервалів («списку граней»), в якій поверхні світу (концептуально) малювалися спереду назад і кожен піксель промальовувався тільки один раз.
Thief розмальовував полігони ззаду вперед, тому вони перерісовували один одного. Перерисовка рухом Thief вже була меншою порівняно з «наївною» перерисовкою Quake (за заздалегідь заданим списком граней) завдяки більш жорстким кордонам проходу порталів, описаним вище.
Thief ще більше зменшив перерисування, обрізаючи кожен відрендерений полігон світу за граничним восьмикутником видимості осередку, що містить цей полігон. (Граничні восьмикутники використовувалися в тому числі і тому: у деяких випадках вони значно знижували перерисування порівняно з використанням звичайних обмежувальних прямокутників. У граничному випадку, якщо ми обрізали полігони до точного порталу, що веде до кожної комірки, це теж би призвело до єдиної відмальовки пікселя.) Я не пригадаю, щоб у нашій типовій перерісовці використовувався цей підхід.
Щоб це працювало, Thief повинен був зберігати полігони світу в осередках. Це означає, що полігони світу, що належать кільком осередкам, повинні бути розділені. У Quake часто можна було зберегти їх як єдині полігони, тому що він зберігав полігони в BSP-дереві безпосередньо на розділюючих площинах. Але я не знаю, наскільки це створювало відмінності, тому що і Thief, і Quake все одно повинні були розділяти полігони для кешування поверхонь.
2D-обрізка до граничного восьмикутника означала, що багато полігонів Thief в результаті рендерилися як 8-сторонні полігони. Це створювало проблеми. Фактично, обрізка була зрозумілою і ефективною, тому що це було всього лише 2D уздовж простих осей, і на вершинах полігонів не було координат текстур S, T (наприклад, U, V), адже в Thief використовувалася техніка, яку я описав в PC Game Programmer's Encyclopedia. По цій техніці координати текстур задавалися як базис 3D-векторів, незалежних від вершин, після чого координати текстур для інтервалів і пікселів обчислювалися безпосередньо з векторних базисів.
Небо (скайбокс) відображалося позначкою полігонів спеціальною текстурою. При відмальовуванні брався перетворений 2D-полігон і обрізався для кожного полігону неба за допомогою накладення текстури скайбоксу на кожен, або що-подібне. Зараз я вже не пам'ятаю точно.
Відсікання об'єктів
При переміщенні об'єктів за рівнем рушіїв повинен відстежувати, в яких комірках знаходився об'єкт. Він робить це інкрементно: використовуючи комірки, в яких об'єкт був раніше, рушій приймає поточне рішення. Це означає, що ця частина движку могла насправді обробляти довільні нереалістичні топології (в яких портали можуть вести до просторів не непослідовно, і згортати простір на себе), тому що «фізика» могла виконувати такі локальні операції переміщення, але у нас ніколи не було зособа реалізувати такі фокуси. Пізніше ми почали використовувати BSP-дерево для сортування комірок, що вимагало їх послідовного визначення. Сам движок Thief в будь-якому випадку їх не підтримував. (Насправді у нас був спосіб реалізувати такі штуки: перед написанням цього рушія я створив портальний рушій у стилі Doom, що підтримує простори над просторами. Він дозволяв створювати кімнати, що накладаються один на одного, які могли мати різну висоту (вважаю, в Dark Forces використовувалася схожа технологія) або ту ж висоту (тому що насправді висота не впливала на роботу цього движку в стилі Doom). За допомогою своїх колег я навіть створив для нього редактор, тому зміг пізніше використовувати його рівні в новому, повністю тривимірному движку. Але у нас ніколи не було можливості зробити повністю тривимірні рівні з такими властивостями.)
Після визначення всіх комірок, які повинні були рендеритися, Thief вирішував, які об'єкти потрібно рендерити. Рендерити потрібно було тільки об'єкти, що знаходяться у видимих комірках. Однак Thief також обчислював обмежувальний 2D-прямокутник (або восьмикутник) кожного потенційно видимого об'єкта і порівнював його з граничним восьмикутником всіх комірок, що містять об'єкт. (У Quake не відбувалося нічого такого, тому що він не обробляв портали в реальному часі.)
Оскільки движок Thief краще справлявся з повним відкиданням невидимих комірок,
він міг відсікати об'єкти, які в даний момент були невидимі, хоча і знаходилися у видимих комірках. Thief в загальному випадку міг обробляти більш щільно заповнені об'єктами світи, ніж дозволяв рендерер з технологією Quake. Можливо, ми і не змогли б відобразити більше об'єктів на екрані, але їх можна було розмістити на рівнях, тому що движок обмежував тільки кількість видимих об'єктів. Завдяки цьому в Thief можна було створювати щільно заставлені об'єктами кухні, обідні столи і шафи.
Але насправді нам просто пощастило, адже ми вибирали алгоритми не спеціально для цієї мети. Розробка движку виконувалася набагато раніше, ніж поступово почали проявлятися риси гейм-дизайну Thief.
Впорядкування об'єктів
У Quake об'єкти рендерилися генеруванням z-буфера з полігонів світу (навіть незважаючи на те, що полігони світу не проходили z-тестування) з подальшим тестуванням і оновленням цього z-буфера при рендерингу об'єктів.
У Thief z-буфер не використовується. Замість цього Thief відмальовував світ ззаду вперед («алгоритм художника»), і переміг рендеринг полігонів світу і об'єктів, щоб вони правильно відсікали один одного.
У багатьох іграх ери програмного рендерінга (де рідко застосовувалися z-буфери) виникали проблеми сортування: об'єкти ставали видимими через стіни або невидимими перед стінами. Цього відбуватися не повинно, а портали/комірки не гарантують відсутності таких помилок. (Наприклад, вони виявлялися в Descent.)
Це дуже засмучувало мене, і я старанно працював над вирішенням проблеми. Алгоритм сортування в Thief - найбільш витончений сортувальник алгоритмом художника, який я коли-небудь бачив. Пам'ятаю, що в якийсь момент мені навіть довелося написати невеликий математичний доказ його частини.
Я не пам'ятаю подробиць, але постараюся в загальних рисах розповісти про деякі проблеми і його роботу.
Спочатку комірки сортувалися на підставі порядку проходження виявлених порталів, що забезпечував порядок сортування спереду назад. Його можна було звернути для візуалізації ззаду вперед. Однак виникли проблеми, і на момент випуску Thief комірки насправді сортувалися BSP-деревом. Це означало, що порядок сортування був дуже далеким від пошуку в ширину. Якщо гравець знаходився поруч з кореневою площиною, що розділяє, порядок відмальовки міг відмалювати дуже близькі до глядача комірки до відмальовки дуже далеких від глядача, у разі, коли комірки і глядач опинялися на відповідних сторонах якоїсь площини BSP.
Завдяки BSP-дереву не було небезпеки того, що полігони світу будуть рендеритися в неправильному порядку, але існувала ймовірність неправильного сортування об'єктів щодо один одного або полігонів світу. Щоб уникнути цього, движок Thief (тут я знову не впевнений) привласнював коміркам номера в порядку відмальовки. Об'єкт у комірці N зазвичай повинен був рендеритися відразу після відмальовки (спрямованих всередину) полігонів світу в комірці N і перед відмальовуванням стін комірки N + 1. Однак іноді об'єкти належали кільком коміркам. Об'єкт в комірках M і N, де M < N, повинен відображатися після всіх стін комірки M, але його частини в комірці M можуть бути заслонені стінами в комірці N або в будь-якій з комірок між M і N. (В голову приходить частий приклад: коридор (комірка) з нішею (комірка), в якій є факел. Факел злегка видається в коридор. Ніша - це M, а коридор - N. Наприклад, якщо точка огляду знаходиться в коридорі, то коридор «ближче» до точки огляду щодо рендерінгу ззаду вперед. Наприклад, одна зі стін коридору може перекривати частини ніші.)
Щоб впоратися з такими труднощами, Thief приймав рішення, чи прийнятно відмальовувати даний об'єкт в самій ближній комірці N (або, по суті, в будь-якій точці порядку відмальовки між самою дальньою і самою ближньою). Для цього від обчислював граничні восьмикутники для об'єктів і полігонів і перевіряв, чи перекриваються полігони об'єктами. Якщо полігон світу повинен бути «попереду» об'єкта і граничні восьмикутники перетиналися, то було небезпечно переміщати об'єкт в порядку рендерингу пізніше, ніж полігон світу.
Рушій Thief вирішував, в якому діапазоні можна відображати кожен об'єкт між найбільш далекими і ближніми комірками, що містять цей об'єкт. Якщо об'єкт був тільки в одній комірці, він завжди вважався таким, що знаходиться в цій комірці. Визначившись з цим, Thief потім намагався дозволити завдання сортування об'єктів відносно один одного. Оскільки об'єкт може знаходитися в декількох комірках, і бути за або перед об'єктом, що знаходиться в одній комірці, об'єкти тільки в одній комірці в будь-якому випадку могло знадобитися зрушити в порядку сортування вперед або назад. Іншими словами, остаточний діапазон комірок, в якому об'єкт міг розглядатися для рендерингу в кінці міг бути більше, ніж просто діапазон від самої дальньої і самої ближньої комірками.
Thief використовувати ці діапазони для пошуку порядку сортування об'єктів, зберігаючи порядок сортування комірок незмінним, щоб об'єкти і полігони світу сортувалися правильно. (Саме тут мені і знадобився математичний доказ.) Однак іноді це було неможливо. Наприклад, в описаному вище випадку факела в ніші один полігон світу в осередку N міг перекриватися факелом (стіна з нішею на ньому, видатна за нішу), але інший полігон світу міг перекривати нішу і частину факела (стіна з нішею на ньому, що видає в бік глядача). У цьому випадку немає ніякого порядку сортування об'єктів і комірок, який би спрацював, тому що частини факела перекривають комірку, а частини тієї ж комірки перекривають факел. Це можна виправити, перемежуючи полігони світу з осередку з факелом, замість постійної відмальовки всіх полігонів світу з одного осередку як єдине ціле. Але не всі випадки можна виправити таким чином, тому в Thief такий підхід ніколи не застосовувався (він завжди відмальовував всі полігони світу з одного осередку як єдине ціле).
У Thief був зовсім інший, надзвичайно витратний механізм, на який движок покладався для вирішення складних проблем сортування. Кожен об'єкт можна було «розділити» на декілька частин, по одній на комірку. Кожна комірка вставляє лише ту частину об'єкта, яка містилася в цій комірці. Це виконувалося не дійсним поділом об'єкта, а множинним рендерингом, по одному на кожну «частину». Також для кожної частини виконувалася динамічна обрізка об'єкта «власною площиною обрізки» за допомогою технології, схожої на обрізку за пірамідою видимості (яка теж виконувалася). У складних випадках потрібно було кілька площин обрізки. Однак це було необхідно тільки для обрізки до порталів між комірками, в яких знаходився об'єкт, і обрізка не проводилася буквально по всіх площинах, що задають кожну комірку. Якщо ця техніка застосовувалася, частина об'єкта могла завжди вимальовуватися в комірці або після неї. Однак, хоча це не збільшувало кількості обчислень на піксель, але вимагало множинної трансформації та обрізки об'єкта, тому було досить витратним. (Тому я радив дизайнерам розташовувати смолоскипи повністю в нішах.) Це було особливо погано при рендерингу персонажів, адже у них був скінінг. Думаю, через це доводилося кілька разів виконувати трансформації скінінгу.
Але виникала ще одна проблема: якщо у вас є три комірки, розташовані таким чином, що портали між ними утворюють букву T, то динамічна обрізка може створювати на кордоні T-подібний перетин. Надалі вони можуть призвести до розривів у рендерингу.
Я не пам'ятаю, як ми вирішили цю проблему, і чи вирішили взагалі.
При розробці я помітив, що якщо використовувати всі стіни комірки для площин користувача обрізки (не тільки порталів), то в разі перетину об'єкта зі стіною він відсікається, що виглядає в точності як рендеринг з z-буфером. Зазвичай такі артефакти виникали через те, що фізика гри дозволяла об'єктам на перетинатися короткий час, тому краще було не обрізати ці обекти, щоб вони не виглядали (з більшості кутів) такими, що проходять через стіни. Але ми могли використовувати такий ефект, щоб дозволити персонажам переміщатися крізь стіни, хоча у нас і не було z-буфера.
Після мого позитивного досвіду з власними площинами обрізки мене довгий час засмучувало, що графічні процесори не можуть ефективно/зручно підтримувати їх. (Навіть у D3D9 вони все ще виконувалися в піксельному шейдері, хоча можна було використовувати z-буфер і шаблон для більш точного відсікання і зменшення розрахунку піксельних шейдерів.)
Освітлення та тіні
Базова техніка рендерінгу полігонів з накладеними текстурами і якісними попередньо розрахованими тінями дуже схожа (аналогічна?) використаної в Quake. Для зберігання освітлених поверхонь у Thief використовувалися карти освітлення і кеш поверхонь. Раджу вивчити цю тему в нотатках Майка Абраша.
Наскільки я пам'ятаю, цю техніку додали в движок після того, як ми (команда розробників Thief) побачили реліз «QTest» Quake. Однак я сумніваюся, що в той момент вже існувала команда розробки Thief. За даними Вікіпедії, QTest вийшов 24 лютого 1996 року. Terra Nova вийшла 5 березня 1996 року, так що я думаю, що ми зібрали фінальну версію Terra Nova до часу випуску QTest. Але не пригадаю, що у нас вже була ціла команда. Насправді я не впевнений, коли точно я створив вихідну дослідницьку версію цього движку.
Об'єкти випускали промені до всіх (або N) джерел світла для визначення їх видимості. В залежності видимості об'єкта від джерела, для нього включалося або відключалося все освітлення. Так ми симулювали вхід або вихід об'єктів з тіні. Але я не знаю, чи випустили ми гру з цією технікою, чи використовували перевірку карти освітлення на підлозі. Як мені пам'ятається, ми принаймні застосовували таку перевірку, щоб визначати видимість гравця для охоронців. Завдяки цьому гравця не могли збити з пантелику місця, які зовні виглядали знаходяться в тіні.
Модель CSG
Рівні Thief створювалися за допомогою методів конструктивної суцільної геометрії (Constructive Solid Geometry, CSG), заснованих на знаннях про роботу Quake. Однак технологічно реалізація Thief сильно відрізнялася від Quake.
Модель CSG у Thief була «часом». Краще пояснити це на прикладі аналогії з Photoshop. Photoshop містить шари. Об'єкти, розташовані на одному шарі, перекривають об'єкти на нижніх рівнях, якщо тільки не використовуються більш цікаві моделі змішування. У такому разі об'єкти в шарах змінюють видимі об'єкти під ними, але не впливають на об'єкти над ними.
З точки зору алгоритмів можна створювати кінцеве зображення як послідовну обробку кожного шару зображення внизу вгору з комбінуванням поточного зображення. Під час обробки кожного зображення поточне зображення містить накопичені ефекти всіх попередніх (нижніх) шарів. Після обробки шару поточне зображення змінюється і цей шар більше не впливає на поточне зображення, крім даних, що залишаються в поточному зображенні.
Зазвичай ми сприймаємо модель шарів Photoshop як стек 2D-шарів, але замість цього можна вважати вищеописаний алгоритм моделлю, і думати про шари не як про «вертикальний» стек, а як про порядкову послідовність операцій для виконання. Саме це я і маю на увазі під «часовою» моделлю, яка використовувалася в Thief для CSG. (Якщо вертикальний стек шарів Photoshop - вертикальний третій вимір двовимірних зображень, модель шарів Thief буде четвертим виміром 3D-форм, і сприймати його як чотиримірний простір не дуже ефективно).
CSG Thief отримує як вхідні дані набори «операцій», побудованих у порядку виконання. Ці операції були «розташуваннями пензля», де пензлем було тривимірне випукле тверде тіло, а також характеристикою того, як область, покрита цим твердим тілом, буде змінена операцією. Весь простір спочатку твердий, тому однією операцією пензля була «вирізати в цій області отвір», іншими словами, «змінити область, покриту цим пензлем, щоб звільнити його». Наприклад, таку операцію використовують для вирізання кімнати. Інша операція мала тверде тіло, можна було використовувати її для розміщення колони. Інша операція додавала воду. Ще одна - лаву. Оскільки простір міг бути одного з 4 типів (твердь, повітря, вода або лава - ей, це ж 4 класичних елементи!), кожну операцію можна розглядати по тому, який тип виведення вона створювала. Крім того, ми зробили ці операції селективними. Наприклад, «пензель заливки» перетворювала повітря на воду, але не зачіпала інші типи. Це спрощувало заповнення області водою - можна було створити її повністю з повітря, а потім залити нижню частину водою. Завдяки часовому аспекту можна було потім при бажанні змінити частину води на «повітря». Можна було створювати більш складні типи пензлів (повітря перетворити на воду, воду - на твердь, твердь - на повітря, а лаву не чіпати), але це не було особливо корисно, тому, як мені здається, основними типами кистей були «безумовні однотипні» і «умовні однотипні».
Як і в шарах Photoshop, тимчасовий аспект керувався користувачем: можна було пересунути пензель вперед або назад у часі. На жаль, це було набагато складніше візуалізувати (вікна «шари» у нас не було). Не знаю, наскільки радувала дизайнерів така система.
Якщо порівняти з Quake, рівень у ньому починався з повністю відкритого простору, в якому розташовувалися тверді об'єкти. Там використовувалася більш традиційна для CSG операція явного віднімання. Вода створювалася першим за часом використанням всіх кистей води, тому ефективна часова послідовність операцій виглядала так: тільки повітря = > частина повітря перетворюється на воду = > розміщення всіх твердих частин. Оскільки віднімання було явним для «твердих» пензлів до того, як вони додавалися в світ, віднімання не могло «випадково» видалити воду, тому необхідності в явній часовій моделі не було (набір доступних дій в Quake був більш обмежений, але на практиці в ньому підтримувалося майже все потрібне. Але незважаючи на більшу кількість дій у Thief, дизайнери іноді йшли на досить дивні послідовності операцій для створення складних форм).
Оскільки рівні Quake спочатку порожні, у грі були невидимі «зовнішні» поверхні, які вимагали окремого процесу виявлення та видалення. Якщо рівень не був герметичним, то зовнішніх поверхонь можна було досягти. При цьому автоматизовані інструменти не могли видалити їх. У Thief рівні спочатку «тверді», тому необхідності в цьому ніколи не було. (Думаю, CSG в Unreal теж спочатку «тверда».)
Реалізація CSG
Я поняття не мав про те, що робив, коли реалізовував систему CSG Thief (незважаючи на можливість закидати питаннями Джона Кармака), тому зробив жахливий вибір. Система Thief розділяла світ на випуклі багатогранники і відстежувала тип кожного багатогранника (повітря, твердь, вода, лава, які я називав «середовищами»). Для цього я зберігав світ як незмінне BSP-дерево. BSP-дерево класифікує простір як випуклі багатогранники (за винятком граней, в яких світ може мати необмежені форми, що розширюються до нескінченності).
Використовуючи BSP-дерево, я отримував перевагу в продуктивності, але застосував я його не з цієї причини: насправді я просто не міг придумати жодного іншого способу обчислення вихідних даних. А таким способом я міг послідовно додавати кожен пензель до BSP-дерева і застосовувати операції трансформації середовища до кожного BSP-листа, який містила пензель.
Але якщо вивчити вихідні Quake, можна зрозуміти, що існує і інший спосіб: безпосередньо перетнути кожен пензель з кожним іншим пензлем без просторової структури даних. При акуратній реалізації можна створити збільшувальний список не накладаються один на одного випуклих багатогранників. Потім можна додати просторову структуру даних для прискорення визначення багатогранників, які можуть накладатися, не впливаючи на самі обчислення.
Різниця в тому, що в CSG-дереві на основі BSP-дерева можуть виникати ситуації, коли пензель, який використовується на ранньому етапі обробки, вставляється в дерево рано, що призведе до появи площини поділу BSP поруч з коренем, яка потім розшириться на весь рівень або на значну його частину. Вона може випадково наблизитися до межі або вершини абсолютно не пов'язаної з нею пензля, наводячи ще одному поділу цієї пензля і додатковим проблемам з епсилон-ентропією. Оскільки в CSG вже існували жахливо повільні численні геометричні алгоритми з епсилон-проблемами, додавання ще одного було жахливим. Редактор Thief сумно відомий дивними проблемами, при яких зміна в одному пензлю могла призвести до збою в генераторі CSG абсолютно не пов'язаного місця в рівні. Під збоєм мається на увазі те, що рівень просто не буде «компілюватися».
У процесі розробки Thief я змінив всі значення в движку CSG з float на double і зменшив епсилон-проблему. Все стало краще, що дозволило поліпшити роботу, але не вирішило проблему абсолютно. Однак тепер я усвідомлюю, що набагато краще було б зовсім відмовитися від BSP-дерева.
Епсилон-проблеми посилювалися тим, наскільки шалено я будував полігони і портали безпосередньо з BSP-дерева. T-подібні перетини були відсутні, обчислювалася загальна сітка вздовж кожної площини поділу BSP, щоб гарантувати, що вершини з одного боку площини поділу завжди мають відповідну вершину з іншого боку. При цьому вводилося набагато більш складне безліч інваріантів, яке потрібно було підтримувати і у якого теж були епсилон-проблеми. Це значило, мені не потрібно писати знищувач Т-подібних перетинів постобробки, як це реалізовано в Quake, але тепер я бачу, що такий підхід був би кращим.
Перспективне текстурування
В іграх Looking Glass до Thief, таких як System Shock 1 і Terra Nova, для перспективного текстурування використовувався перетворювач «ліній з постійною Z».
У таких іграх зазвичай використовували перспективне текстурування для великих близьких полігонів, а для далеких полігонів - аффінне перетворення.
У Thief використовувався спеціально написаний перспективний перетворювач для всіх полігонів. У ньому застосовувався трюк, використаний у Quake: ділення з плаваючою комою для перспективної корекції для кожних N, яке виконувалося паралельно з накладанням текстур наступних N. Трюк працював, тому що Pentium міг виконувати ділення з пла
