В моите 3D игри и проекти често залагам на сладък нискополигонален анимационен стил. Отдавна исках да дам на моите модели истински очертания, подобни на анимационни, така че това е, върху което ще работим днес. В бъдеща публикация ще разгледаме оцветяването на триъгълниците, така че да изглеждат и анимационни.

Тази статия е част от продължаващата ми поредица от „уроци за ThreeJS със средна трудност“. Отдавна исках нещо средно между нивата на интро „Как да нарисуваме куб“ и „Да изпълним екрана с лудост на шейдъра“. Ето го.

Ако търсите в мрежата „OpenGL контурен ефект“, ще попаднете на много противоречива информация. След много проучвания установих, че има два често срещани начина за създаване на контур с помощта на модерни 3D графични API.

1. Начертайте обект два пъти; веднъж в цвета на контура и веднъж нормално.
2. Прокарайте цялата сцена през ефект за последваща обработка, който открива ръбове на ниво пиксел.

Вторият вариант е най-често използван днес в модерни двигатели за игри като Unity. Въпреки това не искам да го използвам, защото включва множество стъпки за последваща обработка, които могат да бъдат бавни на мобилни графични процесори и консумират повече памет. Освен това последващата обработка и WebVR не се смесват много добре в момента, така че за момента го избягвам. (Ще преразгледаме това, когато разгледам светещите ефекти). Нека се съсредоточим върху първия метод, рисувайки един и същ обект два пъти.

Рендирането на обект два пъти може да изглежда разточително, но не забравяйте, че повечето графични процесори са с ограничена честотна лента. След като получите геометрията на графичния процесор, той може да рендира едно и също нещо отново и отново почти без разходи. И в повечето случаи нещата, които искам да очертая, са статична геометрия.

Нека започнем с изобразяване на торичен възел два пъти, веднъж в черно и веднъж в жълто. Този трик, който прави тази работа, е мащабирането на контура, така че да е малко по-голям от основния обект.

//create a cube
obj = new THREE.Group()
const c1 = new THREE.Mesh(
    new THREE.TorusKnotBufferGeometry(0.6,0.1),
    new THREE.MeshLambertMaterial({
        color:’black’, 
    }))
const s = 1.03
c1.scale.set(s,s,s)
obj.add(c1)
obj.add(new THREE.Mesh(
    new THREE.TorusKnotBufferGeometry(0.6,0.1),
    new THREE.MeshPhongMaterial({
        color:’yellow’, 
    })
))

Стартирайте това и вижте какво ще се случи. Хм. В зависимост от геометрията, която използвате, ще видите или нещо, което е изцяло черно или двуцветно, частично черно и частично жълто. Също така забелязвате няколко черни триъгълника, стърчащи от жълтото? Това се нарича z-бой. И така, какъв е проблемът.

Всъщност има смисъл. Черният възел е леко разширен, така че навсякъде, където жълтото не се вижда, черната нощ е леко пред него. И така, как можем да разрешим това?

Избиването се обяснява за 15 секунди

Ще се възползваме от един малък трик.

Когато графичният процесор изобразява триъгълник, той обикновено изобразява само предни триъгълници. Това са триъгълниците, които гледат към камерата. Всеки триъгълник, който е обърнат настрани от камерата, по дефиниция ще бъде невидим, така че не си прави труда да ги нарисувате. Ако имахме сфера, тогава всъщност щеше да бъде нарисувано само предното полукълбо. Графичният процесор е избрал триъгълниците, образуващи обърнатото назад полукълбо.

За контурния ефект искаме правилният обект да бъде начертан само с лицева геометрия. Това вече се случва. Въпреки това, за очертанията искаме да бъдат начертани само триъгълниците, обърнати на гърба. Тогава те ще бъдат зад правилните форми, видими само по ръбовете.

Както се случва, ThreeJS вече знае как да рисува отпред срещу отзад. Просто трябва да му кажем какво искаме. Кодът по-долу е същият като по-горе, той просто задава правилно свойството side на двата материала.

obj = new THREE.Group()
const c1 = new THREE.Mesh(
    new THREE.TorusKnotBufferGeometry(0.6,0.1),
    new THREE.MeshLambertMaterial({
        color:’black’, 
        side: THREE.BackSide
    })
)
const s = 1.03
c1.scale.set(s,s,s)
obj.add(c1)
obj.add(new THREE.Mesh(
   new THREE.TorusKnotBufferGeometry(0.6,0.1),
   new THREE.MeshPhongMaterial({
       color:’yellow’, 
       side: THREE.FrontSide
   })
))

Сега изглежда така:

Перфектно!

Коригиране на нормалите

Всъщност не, не е съвсем перфектно. Ако се вгледате внимателно, ще забележите, че очертанията на частите на обекта, които са отзад, са по-тънки от тези отпред. Това е така, защото ние просто мащабираме целия обект. Този наивен подход ще работи само за идеално изпъкнали обекти като сфера. С възела или всеки модел от реалния живот трябва да удебелим правилно геометрията.

Както се случва, повечето геометрии вече имат нормали върху себе си. Тези нормали са перпендикулярни на повърхността на геометрията. Ако разширим точките по посока на нормалите, тогава всичко трябва просто да работи. Можем да направим това с леко модифициран вертекс шейдър. Вижте *ТОЗИ ДРУГ БЛОГ* за обяснение на персонализираните вертекс шейдъри.

//create a cube
const mat = new THREE.MeshLambertMaterial({
    color:’black’, 
    side:THREE.BackSide 
})
mat.onBeforeCompile = (shader) => {
    const token = ‘#include <begin_vertex>’
    const customTransform = `
        vec3 transformed = position + objectNormal*0.02;
    `
    shader.vertexShader = 
        shader.vertexShader.replace(token,customTransform)
}

Кодът по-горе създава персонализиран материал за контурния обект. Останалото е идентично с предишното. Вътре в шейдъра той добавя част от objectNormal към position на всеки връх, разширявайки го навън. Променете 0.02 на по-голяма стойност за по-дебел контур.

Сега изглежда така:

Magnifique. Създадохте контур, подобен на карикатура.

Бъдете забелязани

Между другото, ако работите върху страхотно WebVR изживяване, което бихте искали да покажете направо във Firefox Reality, „уведомете ни“.