8.2 Material และ Texturing (PBR, Substance)

เฟส 8 · Technical Art · เวลาเรียน: 25–45 h

workflow material แบบ PBR, texture map (albedo, normal, roughness, metallic) และเครื่องมืออย่าง Substance Painter และ Designer

โมเดล 3D ทุกตัวที่คุณเห็นในคอร์สนี้จนถึงตอนนี้เป็นสีเทาเรียบ ๆ ทั้งหมด บทนี้จะแก้ปัญหานั้น คุณจะได้เรียนว่าเกมเอนจินตัดสินใจยังไงว่าพื้นผิว (surface) ของวัตถุจะหน้าตาเป็นแบบไหน — สีดิบ ๆ ของมัน, มันดูเหมือนโลหะหรือพลาสติก, มันดูเรียบหรือเป็นรอยขีดข่วน และรอยนูนเล็ก ๆ ถูกหลอกตาให้เห็นได้ยังไงโดยไม่ต้องเพิ่มสามเหลี่ยม (triangle) แม้แต่ชิ้นเดียว นี่คืองานที่ technical artist ทำแทบทุกวัน — ไม่ใช่การวาดภาพสวย ๆ เพื่อความสวยงามอย่างเดียว แต่คือการเข้าใจจริง ๆ ว่า texture ไหนควบคุมส่วนไหนของหน้าตาวัตถุ เพื่อให้ฉากทั้งฉากถูกต้องและเรนเดอร์ได้เร็วไปพร้อมกัน

เราจะค่อย ๆ สร้างความเข้าใจทีละชิ้น: material จริง ๆ คืออะไร, texture ถูกห่อหุ้ม (wrap) เข้ากับรูปทรง 3D ที่ขรุขระได้ยังไง แล้วค่อยไปถึงชุด texture map มาตรฐานแบบ PBR (Physically Based Rendering) ที่เครื่องมืออย่าง Substance Painter กับ Substance Designer ใช้กัน จบบทนี้แล้วคุณควรมองตัวละครหรือ prop ในเกมแล้วพูดออกมาได้ว่า "ความมันวาวตรงนั้นมาจาก roughness map, รอยสึกตรงขอบนั่นคือ metallic map, ส่วนรอยแตกเล็ก ๆ พวกนั้นคือ normal map" — ไม่ใช่การเดา แต่เพราะคุณรู้จริง ๆ ว่าแต่ละ map ทำหน้าที่อะไร

1. Material คืออะไร? Material vs Shader vs Texture

คำสามคำนี้มือใหม่มักสับสนกันตลอด: texture, shader, และ material มันคือสามสิ่งที่ต่างกันจริง ๆ และถ้าเข้าใจความต่างตรงนี้ได้ ทุกอย่างในบทนี้จะเข้าที่เข้าทางเอง

texture คือรูปภาพธรรมดา — ตาราง 2D ของพิกเซล แต่ละพิกเซลคือสี เก็บอยู่ในหน่วยความจำ รูปถ่ายกำแพงอิฐที่เซฟเป็นไฟล์ .png ก็คือ texture ไม่มีอะไรลึกลับไปกว่านั้น

shader คือโปรแกรมเล็ก ๆ ที่รันบน GPU (Graphics Processing Unit — ชิปที่ถูกสร้างมาเฉพาะเพื่อวาดพิกเซลให้เร็ว) หน้าที่ของ shader คือตัดสินใจว่า สำหรับทุกพิกเซลบนหน้าจอ พิกเซลนั้นควรเป็นสีอะไร โดยดูจากอินพุตอย่างทิศทางของแสง, มุมกล้อง, และ texture ที่มันได้รับมา ตัว shader เองไม่รู้ว่าจะใช้ texture ไหน — มันมีแค่ช่อง ("เสียบ texture ตรงนี้") รอให้ใครมาใส่ให้

material คือ shader ที่ช่องต่าง ๆ ถูกใส่ข้อมูลจริงเข้าไปแล้ว: texture ตัวนี้ใส่ในช่องนี้, ตัวเลขนี้ใส่ในช่องนั้น shader ตัวเดียวกันสามารถให้ผลลัพธ์เป็นท่อสนิมหรือของเล่นพลาสติกมันวาวก็ได้ — โค้ดตัวเดียวกัน แต่ข้อมูล material ที่เสียบเข้าไปต่างกัน

ลองเปรียบเทียบแบบนี้: shader คือสูตรอาหาร (function ที่มีชื่อวัตถุดิบกำกับไว้) material คือสูตรนั้นที่กรอกวัตถุดิบจริงเข้าไปแล้ว (แป้งยี่ห้อนี้ น้ำตาลกี่กรัม) ส่วน texture คือวัตถุดิบชิ้นเดียว (แป้งหนึ่งถุง)

using UnityEngine;

public class MakeRustyMetal : MonoBehaviour
{
    public Texture2D rustTexture;

    void Start()
    {
        // Find a shader: a GPU program, not yet filled in with any data.
        Shader litShader = Shader.Find("Universal Render Pipeline/Lit");

        // A Material = a shader + the actual textures and numbers it uses.
        Material rustyMetal = new Material(litShader);
        rustyMetal.SetTexture("_BaseMap", rustTexture);
        rustyMetal.SetFloat("_Metallic", 1.0f);    // fully metal
        rustyMetal.SetFloat("_Smoothness", 0.2f);  // rough, not shiny

        GetComponent<Renderer>().material = rustyMetal;
    }
}

สคริปต์นี้ไม่ได้ปริ้นอะไรออกทาง console ที่เกิดขึ้นแทนคือ: ทุกครั้งที่ GPU วาดพิกเซลที่เป็นของ object นี้ มันจะรันโค้ดของ shader ชื่อ Lit และโค้ดนั้นจะอ่านค่า _BaseMap, _Metallic, และ _Smoothness จาก material ตัวนี้โดยเฉพาะ ผลลัพธ์คือพื้นผิวสีน้ำตาลเทาหม่น ๆ ไม่มันวาว — แทนที่จะเป็นสีเทาธรรมดาที่เป็นค่าเริ่มต้นของ shader — ทั้ง ๆ ที่โค้ดของตัว shader เองไม่ได้เปลี่ยนแม้แต่บรรทัดเดียว

+--------------------+ | SHADER | a GPU program: "how do I turn light + textures | ("Lit" / "PBR") | into a pixel color?" -- has empty slots +----------+---------+ | fill the slots with real data v +--------------------+ | MATERIAL | | _BaseMap: rust.png | | _Metallic: 1.0 | | _Smoothness: 0.2 | +----------+---------+ | applied to a mesh's Renderer v [ rendered pixels on screen ]
Tip shader ตัวเดียว มี material หลายตัว คือประเด็นทั้งหมดของการแยกสองสิ่งนี้ออกจากกัน shader "Lit" ตัวเดียวในเกมจริง ๆ อาจถูก material หลายร้อยตัวใช้ร่วมกัน — เกราะ, ผิวหนัง, ผ้า, หิน — แต่ละตัวก็แค่เซ็ต texture กับตัวเลขต่างกันที่เสียบเข้าไปในโค้ด GPU ตัวเดียวกัน

2. UV Unwrapping — คลี่โมเดล 3D ให้แบนเป็น 2D

texture คือภาพแบน 2D โมเดลในเกมคือรูปทรง 3D ที่ขรุขระ แล้วเอนจินจะรู้ได้ยังไงว่าพิกเซลไหนของภาพแบนนั้นควรอยู่ตรง ข้อศอกซ้ายของโมเดล มันต้องมีการแมป (mapping) — สำหรับทุกจุดบนพื้นผิว 3D "พิกเซลไหนของ texture 2D ควรอยู่ตรงนี้"

การแมปนั้นถูกเก็บเป็นตัวเลขสองตัวเพิ่มเติมที่ติดอยู่กับทุก vertex (จุดมุม) ของ mesh เรียกว่า U กับ V (ใช้ตัวอักษรนี้แทน X/Y เพื่อไม่ให้สับสนกับตำแหน่ง 3D จริงของโมเดล) U วิ่งข้ามภาพ texture จากซ้ายไปขวา ตั้งแต่ 0 ถึง 1 ส่วน V วิ่งขึ้นภาพ จากล่างขึ้นบน ตั้งแต่ 0 ถึง 1 รวมกันแล้วคู่ (U, V) คือพิกัดจุดหนึ่งภายในภาพ texture ที่แบนราบ เรียกว่า UV space

UV unwrapping คือกระบวนการ — ทำด้วยมือในโปรแกรม 3D อย่าง Blender หรือ Maya หรือทำอัตโนมัติด้วยอัลกอริทึม — ในการตัด mesh 3D ตามขอบบางเส้นแล้วคลี่มันให้แบนราบ เหมือนกับที่คุณตัดกล่องกระดาษให้แบนวางบนโต๊ะได้ สามเหลี่ยมทุกชิ้นของ mesh จะไปอยู่ที่ไหนสักแห่งในสี่เหลี่ยมแบน 0..1 คูณ 0..1 นั้น พร้อมให้ texture ถูกวาดลงไปได้

ขอบที่คุณตัดเพื่อคลี่โมเดลให้แบนเรียกว่า seam ตรงไหนที่มี seam texture อาจเห็นได้ชัดว่าไม่ต่อกันสนิท เหมือนของขวัญที่ห่อแล้วเห็นแถบเทปติดอยู่ UV layout ที่ดีจะซ่อน seam ไว้ในจุดที่สังเกตยาก — ในซอกรักแร้, ตามตะเข็บชายเสื้อ, หรือด้านหลังศีรษะ

A cube's mesh, cut along seams and unfolded flat (a "cross" layout): +--------+ | TOP | +--------+ +-------+--------+-------+-------+ | LEFT | FRONT | RIGHT | BACK | +-------+--------+-------+-------+ +--------+ | BOTTOM | +--------+ Each edge between two boxes above is a real 3D edge that got cut open (a seam) so the cube's surface could lie flat in 2D UV space.

คุณสามารถตรวจสอบ UV ของ mesh ได้ตรง ๆ ในโค้ด — มันก็แค่ตัวเลข คู่หนึ่งต่อหนึ่ง vertex:

using UnityEngine;

public class InspectUVs : MonoBehaviour
{
    void Start()
    {
        Mesh mesh = GetComponent<MeshFilter>().sharedMesh;
        Vector2[] uvs = mesh.uv; // one UV coordinate per vertex

        Debug.Log("Vertex count: " + mesh.vertexCount);
        Debug.Log("First vertex UV: " + uvs[0]);
    }
}
Vertex count: 24
First vertex UV: (0.00, 0.67)

cube เริ่มต้นของ Unity มี 24 vertex ไม่ใช่ 8 — เพราะแต่ละหน้าใน 6 หน้าต้องมี vertex มุมของตัวเอง 4 จุด เนื่องจากแต่ละหน้าต้องมีพิกัด UV เป็นของตัวเองแยกกัน (ถ้าใช้ vertex มุมร่วมกันจะไม่สามารถชี้ไปยังจุดที่ต่างกันหกจุดใน texture พร้อมกันได้) vertex แรกอยู่ที่ U=0, V=0.67: "ตอน shade มุมนี้ ให้ไปอ่านค่าจาก texture ที่ตำแหน่ง 0% จากซ้าย และ 67% จากล่าง"

Common mistake วาง seam ไว้ในจุดที่เห็นชัดเกินไป (ตัดผ่ากลางหน้าตัวละครตรง ๆ, ตัดผ่ากลางหน้าอก) เพียงเพราะมันเป็นจุดที่ตัดง่ายที่สุด seam ควรอยู่ตรงจุดที่โครงสร้าง topology ซ่อนมันได้ตามธรรมชาติ ไม่ใช่ตรงไหนที่คลี่ได้เร็วที่สุด

3. Texel Density — รักษารายละเอียดให้สม่ำเสมอ

พิกเซลหนึ่งของ texture ตามที่มันปรากฏบนพื้นผิว 3D เรียกว่า texel (ย่อจาก "texture element" คือพิกเซลในโลกของ texture ที่เทียบเท่ากับพิกเซลบนหน้าจอ) texel density คือจำนวน texel ที่คลุมระยะทางจริงในโลกเกม มักเขียนเป็นพิกเซลต่อเมตร เช่น 256 px/m

texel density สำคัญเพราะมันควบคุมความคมชัด ถ้า prop สองชิ้นที่อยู่ใกล้กันมี texel density ต่างกันมาก ตัวที่ density ต่ำจะดูเบลอกว่ารอบข้างอย่างชัดเจน — หรือในทางกลับกัน prop ที่ density สูงเกินไปก็เปลืองหน่วยความจำ texture ไปกับรายละเอียดที่ไม่มีใครเข้าไปดูใกล้ขนาดนั้น การรักษา texel density ให้สม่ำเสมอทั้งฉากทำให้ไม่มีอะไรดูนุ่มผิดปกติหรือคมเกินไปเมื่อเทียบกับของรอบข้าง

float ComputeTexelDensity(int textureResolutionPx, float uvIslandFraction, float worldSizeMeters)
{
    float texturePixelsUsed = textureResolutionPx * uvIslandFraction;
    return texturePixelsUsed / worldSizeMeters; // pixels per meter
}

// A 1024px texture, where this object's UV island uses half of it,
// covering 2 meters of the object in the game world:
float density = ComputeTexelDensity(1024, 0.5f, 2f);
Debug.Log(density);
256

ตัวอย่างนี้ได้ผลลัพธ์ 256 พิกเซลต่อเมตร: 1024 * 0.5 = 512 พิกเซลของ texture ถูกกระจายไปทั่วพื้นผิว 2 เมตร ซึ่งคือ 512 / 2 = 256 px/m ถ้าผนังข้าง ๆ ใช้ texture 1024px ตัวเดียวกันเป๊ะ แต่ UV island ของมันคลุมแค่ 10% ของ texture นั้น แล้วยืดไปทั่ว 4 เมตร density ของมันจะเหลือแค่ 1024 * 0.1 / 4 = 25.6 px/m — เบลอเห็นได้ชัดเมื่อสองผนังนี้อยู่ติดกัน

Wall A: texture stretched thin over a big wall -- LOW texel density (64 px/m) +----------------------------------+ |## ## ## ## ## ## | big, blurry blocks of detail +----------------------------------+ Wall B: same texture, smaller wall -- HIGH texel density (256 px/m) +------------+ |# # # # # # | small, crisp detail +------------+
Tip โปรดักชันจริง ๆ มักมีงบ texel density กำกับไว้: ตารางสั้น ๆ แบบ "hero character: 1024 px/m, prop กลางฉาก: 512 px/m, background ไกล ๆ: 128 px/m" อาร์ติสต์ทุกคนเช็ค UV ใหม่กับตารางนี้ แทนที่จะกะด้วยสายตาเอาเอง

4. ชุด PBR Texture Map — ภาพรวม

PBR ย่อจาก Physically Based Rendering: วิธีเขียนสูตรคำนวณแสงใน shader ให้พื้นผิวตอบสนองต่อแสงด้วยสูตรที่อิงหลักฟิสิกส์จริง ๆ ว่าแสงกระจายตัวออกจากพื้นผิวขรุขระยังไง หรือสะท้อนออกจากโลหะต่างจากวัสดุอื่นยังไง — แทนที่จะใช้ลูกเล่นแบบเก่า ๆ ที่ดูดีแค่ภายใต้แสงแบบเดียว แล้วพังทันทีถ้าเปลี่ยนสภาพแสง

เพราะสูตรคำนวณของ shader ตอนนี้เป็นมาตรฐานเดียวกัน (สูตรใกล้เคียงกันไม่ว่าจะเป็น Unity, Unreal, หรือเครื่องมืออย่าง Substance) อาร์ติสต์ก็แค่วาดชุด texture map มาตรฐานที่ทุกคนรู้จักป้อนเข้าไปในสูตรนั้น แล้ว material จะดูถูกต้องตามหลักฟิสิกส์ไม่ว่าจะเอาไปวางในสภาพแสงแบบไหน — แดดจ้า, ถ้ำมืด, หรือแสงสตูดิโอสีแปลก ๆ ชุดมาตรฐานนั้นคือ:

หกหัวข้อถัดไปจะลงลึกแต่ละ map พวกนี้ทีละตัว นี่คือหน้าตาของ object ตัวเดียวกันเมื่อแยกออกเป็นแต่ละ map — เป็นมุมมองแบบเดียวกับที่ Substance Painter แสดงให้เห็นตอนวาด ทีละ channel:

+----------+ +----------+ +----------+ +----------+ |BaseColor | | Normal | |Roughness | | Metallic | |(a photo- | |(purple- | |(gray = | |(black = | | like flat| | blue, | | shiny, | | not | | color) | | bumpy) | | white = | | metal, | | | | | | rough) | | white = | | | | | | | | metal) | +----------+ +----------+ +----------+ +----------+ +----------+ +----------+ +----------+ | AO | | Height | | Emission | |(dark in | |(gray = | |(black + | | cracks, | | up/down | | a glowing| | white in | | bumps) | | shape) | | the open)| | | | | +----------+ +----------+ +----------+

5. Base Color / Albedo Map

base color map (เรียกอีกชื่อว่า albedo มาจากภาษาละตินแปลว่า "ความขาว" หมายถึงพื้นผิวสะท้อนแสงออกมามากแค่ไหน) เก็บสีดิบของวัสดุโดยเอาแสงเงาออกให้หมด: ไม่มีเงาที่ bake ติดไว้, ไม่มีไฮไลต์ที่ bake ติดไว้, ไม่มีคราบดำจาก ambient occlusion ที่ bake ติดไว้ มีแค่ "วัสดุนี้สีอะไร ถ้าเห็นภายใต้แสงเรียบ ๆ สม่ำเสมอ"

ตรงนี้แหละที่มือใหม่สายวาดภาพแบบดั้งเดิมมักพลาด base color map ไม่ใช่ภาพวาดที่เสร็จสมบูรณ์แล้ว — มันควรดูแบนและน่าเบื่อนิด ๆ เมื่อดูเดี่ยว ๆ เพราะ shader จะเป็นคนใส่แสง เงา และไฮไลต์ทั้งหมดทับลงไปแบบเรียลไทม์อีกที ถ้าคุณวาดเงาลงไปใน base color map ตรง ๆ แล้ว shader แบบเรียลไทม์ก็ยังทอดเงาตรงนั้นอีก คุณจะได้จุดมืดซ้อนสองชั้นที่ดูผิดทันทีที่มุมแสงเปลี่ยน

#if UNITY_EDITOR
using UnityEditor;

void FixImportSettings(string path, bool isColorData)
{
    TextureImporter importer = (TextureImporter)AssetImporter.GetAtPath(path);
    // true for BaseColor/Emission (real color), false for Normal/Roughness/
    // Metallic/AO/Height (raw data, not meant to look like a color at all).
    importer.sRGBTexture = isColorData;
    importer.SaveAndReimport();
}
#endif

คอมเมนต์บรรทัดสุดท้ายสำคัญกว่าที่เห็น สายตามนุษย์ไม่รับรู้ความสว่างเป็นเส้นตรง — texture ที่ตั้งใจให้เห็นเป็นสี (base color, emission) ถูกเก็บด้วยเส้นโค้ง sRGB (เส้นโค้งความสว่างมาตรฐานที่ตรงกับการรับรู้แสงของสายตา เพื่อให้สีออกมาถูกต้องบนจอ) และต้อง import โดยเปิดแฟล็ก "sRGB" ไว้ ส่วน texture ที่เก็บตัวเลขข้อมูลดิบแทนที่จะเป็นสีจริง — normal map, roughness map — ต้อง import โดยปิด sRGB ไว้ ไม่งั้น GPU จะไป "แกะเส้นโค้ง" ตัวเลขที่ไม่เคยตั้งใจให้ถูกแกะตั้งแต่แรก แล้วข้อมูลก็จะออกมาผิด

Common mistake ปล่อยให้ "sRGB" ถูกติ๊กไว้บน normal map, roughness map, หรือ metallic map สิ่งเหล่านี้ไม่ใช่สี — มันคือตัวเลขดิบที่ซ่อนอยู่ในไฟล์ภาพ การติ๊ก sRGB บน data map จะทำให้ค่าทุกค่าที่ shader อ่านจากมันเพี้ยนไปโดยเงียบ ๆ

6. Normal Map — หลอกตาให้เห็นรายละเอียดพื้นผิว

ในกราฟิก 3D normal คือทิศทางที่ชี้ตรงออกจากพื้นผิว สูตรคำนวณแสงต้องรู้ว่าพื้นผิวหันไปทางไหนเพื่อคำนวณว่ามันควรสว่างแค่ไหน — พื้นผิวที่หันเข้าหาแสงจะสว่าง พื้นผิวที่หันหนีแสงจะมืด สามเหลี่ยมแบนหนึ่งชิ้นมี normal แค่ทิศทางเดียวสำหรับทั้งหน้า

พื้นผิวจริง ๆ ไม่ได้แบน อิฐ, ผิวหนัง, ลายไม้ เต็มไปด้วยรอยนูนระดับไมโครหลายล้านจุด แต่ละจุดเอียงไปคนละมุมนิดหน่อย การสร้างโมเดลรอยนูนทุกจุดด้วยสามเหลี่ยมจริง ๆ จะต้องใช้ geometry มหาศาลเกินจำเป็นสำหรับสิ่งที่กล้องจะเห็นแค่เป็น texture ละเอียด ๆ เท่านั้น normal map แก้ปัญหานี้โดยไม่ต้องเพิ่มสามเหลี่ยมแม้แต่ชิ้นเดียว: มันคือ texture ที่ไม่ได้เก็บสีเลย — มันเก็บทิศทาง, normal ปลอมหนึ่งตัวต่อหนึ่งพิกเซล, ซ่อนอยู่ใน channel สีแดง เขียว น้ำเงินของภาพ

normal map มักถูกเก็บในรูปแบบ tangent space เกือบตลอด: channel R, G, B เข้ารหัสทิศทางที่สัมพันธ์กับแกนซ้าย/ขวาของพื้นผิวเอง (เรียกว่า tangent), แกนขึ้น/ลงของพื้นผิวเอง (เรียกว่า bitangent), และแกนชี้ตรงออกของพื้นผิวเอง (normal ทาง geometry จริง) — ไม่ใช่แกน X/Y/Z ตายตัวของโลก นี่คือสิ่งที่ทำให้ normal map texture ตัวเดียวถูกนำไปใช้ซ้ำที่ไหนก็ได้: ลาย bump ของอิฐแบบเดียวกันสามารถปูซ้ำรอบกำแพงโค้งได้ และทุกจุดบนนั้น shader จะแปลงทิศทางแบบ tangent-space นั้นให้เป็นทิศทางโลกจริง โดยใช้แกนท้องถิ่นของพื้นผิว ณ พิกเซลนั้นเป๊ะ ๆ

Texture RGB channel : what it stores in tangent space R (red) stores X (tangent direction, left / right) G (green) stores Y (bitangent direction, up / down) B (blue) stores Z (normal direction, straight out) A perfectly flat pixel: R=128 G=128 B=255 == direction (0, 0, 1) == "point straight out, no bump here"
// Fragment shader excerpt (HLSL, Unity URP style)
half3 SampleBump(float2 uv, half3 tangentWS, half3 bitangentWS,
                  half3 normalWS, sampler2D bumpMap)
{
    half4 packed = tex2D(bumpMap, uv);
    half3 tangentNormal = UnpackNormal(packed); // RGB (0..1) -> direction (-1..1)

    // Rotate the tangent-space bump direction into world space using
    // this pixel's own local tangent/bitangent/normal axes.
    half3x3 tbn = half3x3(tangentWS, bitangentWS, normalWS);
    half3 worldNormal = normalize(mul(tangentNormal, tbn));

    return worldNormal;
}

ลองไล่มือดูสอง texel ตัวอย่าง texel ที่แบนราบมีสี (128, 128, 255) normalize แล้วจะได้ประมาณ (0.50, 0.50, 1.00) แล้ว UnpackNormal จะแปลงเป็นประมาณ (0, 0, 1) — "ชี้ตรงออก" ดังนั้น worldNormal ก็จะเท่ากับ normal ทาง geometry จริง: ไม่เห็นรอยนูนเลย ส่วน texel สีออกแดงนิด ๆ (200, 128, 255) unpack แล้วได้ประมาณ (0.50, 0.00, 0.87) — เอียงไปทางแกน +X (tangent) ของพื้นผิวเอง แค่เอียงนิดเดียวนั้นก็มากพอที่สูตรคำนวณแสงจะได้ความสว่างต่างจากพิกเซลข้าง ๆ ที่แบนราบ ซึ่งตานี่แหละที่อ่านออกมาเป็นรอยนูน ทั้ง ๆ ที่ mesh จริงข้างล่างแบนสนิท

Real bumpy geometry (expensive: needs a huge number of triangles) ^^ ^^ ^^ ^^ ^^ ------------------------------ (the true surface, lots of triangles, each little bump is real geometry) Flat polygon + a normal map (cheap: same lighting result, ONE flat triangle) ------------------------------ (the true surface: flat, few triangles) ^ ^ ^ ^ ^ ^ (fake per-pixel normals read from the texture, tilted to imitate the bumps above -- lighting can't tell the difference)
Common mistake โมเดลที่ดูเหมือน "กลับด้านใน" — รอยนูนกลายเป็นรอยบุ๋ม รอยบุ๋มกลายเป็นรอยนูน ทั้ง ๆ ที่ geometry ถูกต้องทุกอย่าง อาการนี้เกือบทุกครั้งเกิดจาก green channel ไม่ตรงกัน: บางเครื่องมือ bake normal map ด้วยรูปแบบสไตล์ OpenGL สำหรับ green channel บางเครื่องมือใช้สไตล์ DirectX ซึ่งสองแบบนี้กลับด้านกันพอดี การกลับ green channel คืน (มีอยู่ในแบบฝึกหัดท้ายบทนี้) มักแก้ปัญหาได้ทันที

7. Roughness Map

roughness คือตัวเลขตั้งแต่ 0 ถึง 1 บอกว่าพื้นผิวขรุขระระดับไมโครแค่ไหน ในสเกลที่เล็กเกินกว่าจะเห็นรอยนูนแต่ละจุด เห็นแค่ผลรวมทางสถิติของมันต่อการกระจายแสง roughness เท่ากับ 0 หมายถึงพื้นผิวเรียบเงาแบบกระจก: แสงสะท้อนออกไปในทิศทางเดียวแคบ ๆ คมชัด เกิดไฮไลต์ (จุดสว่างที่มองเห็นตรงที่แสงสะท้อน — เรียกว่า specular highlight) เล็ก สว่าง คมชัด roughness เท่ากับ 1 หมายถึงพื้นผิวขรุขระมาก: แสงกระจายไปหลายทิศทางพร้อมกัน ทำให้ไฮไลต์เดียวกันนั้นแผ่กว้างออกเป็นปื้นหม่น ๆ เบลอ ๆ

บาง engine กลับค่านี้แล้วเรียกว่า smoothness แทน — Standard shader รุ่นเก่าของ Unity กับ Lit shader ของ URP ต่างก็มี "Smoothness" ให้ปรับ โดย smoothness = 1 - roughness แนวคิดทางฟิสิกส์เดียวกัน แค่ตัวเลขวิ่งกลับทาง ต้องเช็คให้แน่ใจก่อนเสมอว่า slider ของ shader ตัวนั้นควบคุมค่าไหนจริง ๆ ก่อนพิมพ์ค่าลงไป

เพราะ roughness ถูกเก็บเป็น texture ระดับเทา พิกเซลหนึ่งจึงต่างจากพิกเซลข้างเคียงได้ — ดาบเล่มเดียวสามารถมีใบดาบขัดเงา roughness ต่ำ อยู่ติดกับด้ามหนังที่สึกหรอ roughness สูง โดยใช้ texture เดียว วาดค่าเทาสองระดับลงไป

Roughness 0.0 (smooth) 1.0 (rough) |------------------------------------------| highlight: tiny, bright, sharp -> huge, dim, blurry smear

เพื่อประหยัดหน่วยความจำ texture engine มักจะยัด grayscale map หลายตัวลงไปใน channel สีต่างกันของ texture เดียว แทนที่จะใช้ไฟล์เต็ม ๆ สี่ไฟล์แยกกัน ยกตัวอย่าง Lit shader ของ URP จะอ่าน "Mask Map" ตัวเดียว โดย R = Metallic, G = Ambient Occlusion, B = detail mask, และ A = Smoothness — ข้อมูลสี่ map รวมอยู่ในไฟล์ภาพเดียว เพราะแต่ละ channel ก็คือภาพระดับเทาที่เป็นอิสระจากกันในตัวเองอยู่แล้ว

Tip เทคนิคยัด channel นี้เป็นเหตุผลที่ Substance Painter กับ Designer เลือก export เป็น "ORM" (Occlusion/Roughness/Metallic) รวมกัน หรือ mask map เฉพาะของแต่ละ engine เป็นค่าเริ่มต้น แทนที่จะแยกเป็น PNG สามหรือสี่ไฟล์ — ไฟล์ texture น้อยลงหมายถึง GPU อ่าน texture ต่อพิกเซลน้อยครั้งลง ซึ่งเร็วกว่า

8. Metallic Map

ค่า metallic ในทางฟิสิกส์แล้วเกือบทุกครั้งจะเป็น 0 หรือ 1 เท่านั้น ค่า 0 หมายถึง dielectric (วัสดุที่ไม่นำไฟฟ้า — ไม้, พลาสติก, ผิวหนัง, หิน, สนิม) ค่า 1 หมายถึงโลหะแท้ (เหล็ก, ทอง, อะลูมิเนียม) สูตรคำนวณของ PBR shader ใช้ตัวเลขตัวเดียวนี้ตัดสินใจในทางฟิสิกส์ว่าพื้นผิวจะแบ่งแสงที่เข้ามาระหว่างพฤติกรรมสองแบบที่ต่างกันมาก: โลหะสะท้อนแสงโดยเอาสี base color ของตัวเองไปย้อมแสงที่สะท้อนออกมา และแทบไม่คืนแสงนั้นเป็นแสงกระจาย (diffuse) แบบนุ่ม ๆ เลย ส่วน dielectric ทำตรงกันข้าม — แสงที่สะท้อนออกมาจะเป็นสีขาวหรือเทากลาง ๆ ไม่ว่าตัวมันจะสีอะไร และสีส่วนใหญ่ที่คุณเห็นจริง ๆ มาจากแสงกระจาย (diffuse) แทน

metallic map คือ texture ระดับเทาที่ทำให้ object เดียวเป็นโลหะเปลือยบางส่วนและเป็นวัสดุอื่นบางส่วนได้ — เช่นท่อสนิม มีจุดเหล็กเปลือย (metallic ใกล้ 1) อยู่ติดกับจุดสนิม (metallic ใกล้ 0 เพราะสนิม หรือ iron oxide เป็น dielectric ทั้ง ๆ ที่เริ่มต้นมาจากโลหะ)

Common mistake วาดค่าเทากลาง ๆ (ประมาณ 0.5) ลงใน metallic map "เพื่อความชัวร์" หรือ "เพื่อให้กลืนกัน" ในทางฟิสิกส์แล้ว แทบไม่มีอะไรในโลกจริงที่เป็นโลหะครึ่งหนึ่ง ค่า metallic เทากลาง ๆ จะให้ผลลัพธ์ที่มัว ๆ และผิดหลักฟิสิกส์ ซึ่งสูตร PBR ไม่ได้ถูกออกแบบมาให้เรนเดอร์ออกมาดีเลย ค่า metallic ควรอยู่ใกล้ 0 หรือใกล้ 1 มีแค่ขอบเปลี่ยนผ่านบาง ๆ ตรงที่วัสดุสองอย่างมาเจอกันเท่านั้น

9. Ambient Occlusion, Height/Displacement, และ Emission Map

Ambient Occlusion (AO)

ambient occlusion คือ map ระดับเทาที่คำนวณล่วงหน้าจาก geometry ของโมเดล 3D เอง เก็บว่าแต่ละจุดบนพื้นผิวเปิดโล่งรับแสง ambient แบบอ้อม (แสงที่กระเด้งไปมาทั่วฉากโดยรวม ไม่ใช่แสงตรงจากแหล่งที่มองเห็นแหล่งเดียว) มากแค่ไหน ค่า 0 (ดำ) หมายถึงจุดนั้นถูกซุกลึกอยู่ในซอกที่แสง ambient แทบเข้าไม่ถึง ค่า 1 (ขาว) หมายถึงจุดนั้นเปิดโล่งไม่มีอะไรบังแสงที่เข้ามาใกล้ ๆ เลย

การ bake สิ่งนี้ไว้ล่วงหน้าจะเพิ่มเงาสัมผัส (contact shadow) แบบนุ่ม ๆ ตามตะเข็บ, รูสกรู, และมุมแคบ ๆ — รายละเอียดที่แสงแบบเรียลไทม์คำนวณไม่คุ้มค่า หรือต้องใช้ความละเอียดของเงาสูงมากถึงจะจับได้ AO ควรทำให้แสง ambient/indirect มืดลงเท่านั้น ไม่ควรบังแสงตรงหรือทอดเงาแข็ง ๆ — หน้าที่นั้นเป็นของระบบเงาเรียลไทม์ของเอนจิน

Height / Displacement

height map คือ texture ระดับเทาที่เก็บว่าแต่ละจุดควรดู "สูงขึ้น" หรือ "ต่ำลง" จากพื้นผิวฐานแค่ไหน โดยเทากลางหมายถึง "ไม่เปลี่ยน" มันมีหน้าที่ต่างกันสองแบบขึ้นอยู่กับว่าถูกใช้ยังไง เมื่อใช้เป็น parallax map มันคือลูกเล่นราคาถูกใน shader — Parallax Occlusion Mapping จะขยับว่า texel ไหนถูกอ่าน โดยดูจากมุมมองกล้องกับค่า height นี้ หลอกให้เห็นความลึกจริง ๆ บนพื้นผิวที่ข้างใต้ยังคงเป็น geometry แบนสนิท พื้นหินกรวดที่ใช้ลูกเล่นนี้จะดูแบนเมื่อมองตรงลงมา แต่จะเห็นความลึกและซอกที่ซ่อนอยู่อย่างสมจริงเมื่อมองมุมเฉียง เมื่อใช้เป็น displacement จริง ๆ map ตัวเดียวกันนี้จะถูกใช้ขยับ vertex ของ mesh ขึ้นลงจริง ๆ แทน — ซึ่งจะได้ผลก็ต่อเมื่อ mesh มีสามเหลี่ยมมากพอให้ขยับอยู่แล้วใกล้ ๆ บริเวณนั้น (เทคนิคที่เรียกว่า tessellation มักถูกใช้เพิ่มรายละเอียดพิเศษนั้นให้อัตโนมัติ เฉพาะตรงที่กล้องอยู่ใกล้พอจะต้องใช้)

Emission

emission map คือ texture สี (ไม่ใช่ระดับเทา — มันเก็บสีเรืองแสง RGB จริง ๆ) ทำเครื่องหมายว่าส่วนไหนของพื้นผิวควรดูเหมือนเปล่งแสงได้เอง ไม่ขึ้นกับอะไรอื่นในฉาก: ป้ายนีออน, ดวงตาเรืองแสงของตัวละคร, หน้าจอที่ติดสว่าง

Renderer screenRenderer = GetComponent<Renderer>();
Material screenMat = screenRenderer.material;

screenMat.EnableKeyword("_EMISSION");
screenMat.SetTexture("_EmissionMap", screenGlowTexture);
screenMat.SetColor("_EmissionColor", Color.cyan * 3f); // brighter than pure white

โค้ดนี้ทำให้ prop หน้าจอดูสว่างแม้อยู่ในห้องที่มืดสนิท เพราะ emission ไม่ได้รับผลจากแหล่งแสงภายนอกเลย การคูณสีด้วย 3f ดันความสว่างให้เกิน 1.0 ซึ่งสำคัญถ้าโปรเจกต์ใช้เอฟเฟกต์ post-processing แบบ bloom (แสงฟุ้งนุ่ม ๆ ที่กล้องเพิ่มรอบพิกเซลที่สว่างมาก) — มีแค่พิกเซลที่สว่างเกินประมาณ 1.0 เท่านั้นที่จะฟุ้งออกมาจริง ๆ

+----------+ +----------+ +----------+ | AO | | Height | | Emission | | dark = | | light = | | black = | | blocked, | | raised, | | no glow, | | white = | | dark = | | bright | | open | | lowered | | = glows | +----------+ +----------+ +----------+

10. สองแนวทาง: Metalness/Roughness vs Specular/Glossiness

มีวิธีอธิบายพฤติกรรมของพื้นผิวต่อแสงแบบเดียวกันทางฟิสิกส์อยู่สองแบบที่ใช้กันมาในประวัติศาสตร์ วิธีไหนที่ pipeline ใช้ขึ้นอยู่กับเครื่องมือและเวอร์ชันของ engine ที่เกี่ยวข้อง

metalness/roughness workflow — ค่าเริ่มต้นสมัยใหม่ใน Unity, Unreal, และ Substance — อธิบายพื้นผิวด้วย Base Color, Metallic, และ Roughness ตามที่คุยไปในหัวข้อก่อนหน้า มันกระชับ: metallic กับ roughness ยัดลงใน channel แค่สองตัวของ texture เดียวกันได้ ตามที่แสดงในหัวข้อ 7

specular/glossiness workflow — แบบเก่ากว่า ยังพบได้ในบาง engine และ pipeline ที่เน้นมือถือ — อธิบายพื้นผิวด้วย Diffuse Color, Specular Color แบบ RGB เต็ม (สีของการสะท้อน วาดด้วยมือโดยอาร์ติสต์โดยตรง แทนที่จะคำนวณจาก metallic อัตโนมัติ), และ Glossiness (ตรงข้ามกับ roughness เป๊ะ ๆ: 1 คือมันวาว 0 คือขรุขระ)

METALNESS / ROUGHNESS WORKFLOW SPECULAR / GLOSSINESS WORKFLOW +----------------+ +----------------+ | Base Color | | Diffuse Color | +----------------+ +----------------+ | Metallic | | Specular Color| +----------------+ | (RGB, hand- | | Roughness | | painted) | +----------------+ +----------------+ | Glossiness | +----------------+ Both feed the exact same lighting math in the shader -- just different inputs describing the same physical surface.

ความต่างในทางปฏิบัติคือ: metalness workflow คำนวณสีของการสะท้อนอัตโนมัติจาก Base Color เมื่อ Metallic เป็น 1 ทำให้อาร์ติสต์วาดข้อมูลน้อยลง และมันถูกต้องตามหลักฟิสิกส์แทบจะโดยบังเอิญ ส่วน specular workflow ให้อาร์ติสต์ควบคุมสีการสะท้อนได้โดยตรงด้วยมือ — ยืดหยุ่นกว่าในกรณีแปลก ๆ แต่พลาดวาดวัสดุที่ผิดหลักฟิสิกส์ได้ง่ายกว่ามาก (เช่น "โลหะ" สีเทาที่มี specular tint สีแดงสด ซึ่งไม่ตรงกับวัสดุจริงชนิดไหนเลย) Substance Painter กับ Designer สามารถ bake หรือแปลงระหว่างสอง workflow นี้ให้อัตโนมัติได้ เพราะข้างใต้แล้วมันอธิบายค่าฟิสิกส์เดียวกัน แค่จัดระเบียบต่างกัน

11. เครื่องมือทำ Texture: Substance Painter, Substance Designer, และ Smart Material

Substance Designer คือเครื่องมือแบบ node-based ที่เป็น procedural — "procedural" หมายถึงผลลัพธ์ถูกสร้างจากกราฟของ node คณิตศาสตร์, noise, และลวดลายที่ต่อกัน แทนที่จะวาดพิกเซลด้วยมือทีละจุด เพราะ material ทั้งตัวจริง ๆ แล้วก็แค่กราฟของพารามิเตอร์ มันปรับแก้แบบ non-destructive ได้ (ลากตัวเลื่อน "ปริมาณสนิม" บน node เดียว แล้ว material ทั้งตัวอัปเดตทุกที่) และมันสร้างใหม่ได้ทุกความละเอียดตามต้องการ เพราะไม่มีอะไรเป็นภาพขนาดตายตัวที่ bake ไว้จนกว่าจะถึงตอน export จริง Designer มักถูกใช้สร้างวัสดุพื้นฐานที่นำกลับมาใช้ซ้ำได้ตั้งแต่ศูนย์ — "โลหะสึกหรอ", "หินมีตะไคร่", "คอนกรีตแตกร้าว" — ซึ่งจากนั้นจะถูก export เป็นชุดไฟล์ texture PBR ปกติ หรือเป็นแพ็กเกจที่ยังปรับได้อยู่ (ไฟล์ .sbsar) ที่เครื่องมืออื่นเสียบค่าพารามิเตอร์ของตัวเองเข้าไปได้

Substance Painter คือเครื่องมือวาดแบบ 3D คุณ import โมเดลที่คลี่ UV ไว้แล้ว (หัวข้อ 2) แล้ววาดตรงลงบนพื้นผิว 3D ใน viewport เลย — ไม่ใช่วาดบน UV layout แบบแบน 2D แม้ว่าคุณจะสลับไปดูมุมมอง 2D นั้นได้เหมือนกัน — โดยใช้ layer, mask, และ generator คล้าย ๆ Photoshop มาก ยกเว้นว่าแปรงหนึ่งครั้งสามารถอัปเดต PBR channel หลายตัวพร้อมกันได้ (base color, roughness, metallic, height) ในการขยับครั้งเดียว Painter ยังเข้าใจ geometry ของโมเดลเองด้วย: generator "ฝุ่นในซอก" เช่น จะใช้ความโค้งและ ambient occlusion ของโมเดลเองอัตโนมัติเพื่อตัดสินว่าคราบสกปรกควรสะสมตรงไหนตามธรรมชาติ โดยไม่ต้องให้อาร์ติสต์วาดทุกซอกด้วยสายตาเอง

smart material คือชุดพรีเซ็ตที่รวมเลเยอร์ที่วาดหรือทำแบบ procedural หลายตัวเข้าด้วยกันเป็นแพ็กเกจนำกลับมาใช้ซ้ำได้ — เช่น smart material "เหล็กสนิม" อาจรวมเลเยอร์โลหะพื้นฐาน, เลเยอร์รอยสึกตามขอบ, เลเยอร์สนิมในซอก, และเลเยอร์ฝุ่น แต่ละเลเยอร์มี smart mask ที่ตอบสนองอัตโนมัติต่อความโค้ง, ขอบที่โผล่ออกมา, และ ambient occlusion ของโมเดลนั้น ๆ เอง ลาก smart material ตัวเดียวกันนี้ไปวางบนโมเดลที่ต่างออกไปโดยสิ้นเชิง — ดาบแทนที่จะเป็นท่อ — แล้วมันจะปรับลาย wear pattern ให้เข้ากับรูปทรงของโมเดลใหม่นั้นอัตโนมัติ แทนที่อาร์ติสต์จะต้องวาดรอยขีดข่วนและคราบสนิมใหม่ด้วยมือทุกครั้ง

การ export ทั่วไปจาก Painter จะตามรูปแบบการตั้งชื่อไฟล์ที่คาดเดาได้ ซึ่งแมปตรงเข้ากับช่อง material ที่คุยไปก่อนหน้าในบทนี้:

T_Sword_BaseColor.png   -> Material._BaseMap
T_Sword_Normal.png      -> Material._BumpMap
T_Sword_MaskMap.png     -> Material._MetallicGlossMap (R=Metal, G=AO, A=Smoothness)
T_Sword_Emissive.png    -> Material._EmissionMap
Substance Designer Substance Painter (procedural graph, builds a --> (paint the built material onto a reusable "smart material") specific UV-unwrapped 3D model, using layers + smart masks) | v export the PBR map set | v import into Unity, assign textures to a Material, Material drives the Renderer
Tip smart material ช่วยประหยัดเวลาไปได้มากในช่วง 80% แรกของการทำ texture แต่มันแทบไม่เคยจับรายละเอียดที่วางด้วยมือให้อ่านง่ายและดูตั้งใจได้เท่าคนวาด — รอยขีดข่วนที่ตรงกับจุดที่ตัวละครจับอาวุธจริง ๆ, รอยไหม้ตรงจุดที่ระเบิดควรจะโดนเป๊ะ ๆ เผื่อเวลาไว้แต่งเพิ่มด้วยมือทับบน smart material ฐานเสมอ

12. Trim Sheet และ Texture Atlas

trim sheet คือ texture เดียวที่บรรจุแถบรายละเอียดเล็ก ๆ ที่นำกลับมาใช้ซ้ำได้หลายแบบ — บัว, ท่อ, สายเคเบิล, แถวสกรู, ไม้เชิงชาย — วางเรียงเป็นแถวหรือตาราง UV island หลายตัวจากหลายโมเดลในฉาก สามารถอ้างอิงแค่แถบที่ตัวเองต้องการเท่านั้น แทนที่ผนัง, กรอบประตู, และท่อทุกชิ้นจะต้องมี texture ความละเอียดเต็มเป็นของตัวเอง นี่คือวิธีมาตรฐานในงานศิลป์สภาพแวดล้อม (environment art) เพราะมันรักษาทั้งหน่วยความจำ texture และจำนวน texture เฉพาะตัวให้ต่ำ ในขณะที่ level ยังดูมีรายละเอียดสูงได้

texture atlas คือแนวคิดเดียวกันแต่ใช้กับ prop หรือไอคอนเล็ก ๆ ที่แยกจากกันแทนที่จะเป็นแถบซ้ำ ๆ: texture เล็ก ๆ หลายตัวถูกอัดรวมเข้าด้วยกันในแผ่นเดียว ทำให้ object หลายตัวใช้ material เดียวกันร่วมกันและถูกวาดพร้อมกันได้อย่างมีประสิทธิภาพมากขึ้น การใช้ texture กับ material เดียวร่วมกันในหลาย object ช่วยลดจำนวน draw call ได้ (draw call คือคำสั่งหนึ่งครั้งที่ส่งไปให้ GPU บอกให้เรนเดอร์ geometry เป็นชุด แต่ละคำสั่งมี overhead ฝั่ง CPU ดังนั้น draw call ที่น้อยลงแต่ใหญ่ขึ้นโดยทั่วไปจะเร็วกว่า draw call เล็ก ๆ จำนวนมาก)

// Pick tile (2, 1) out of a 4x4 grid atlas (each tile is 0.25 x 0.25 in UV space)
Vector2 tileSize = new Vector2(0.25f, 0.25f);
Vector2Int tileIndex = new Vector2Int(2, 1);

Material atlasMat = GetComponent<Renderer>().material;
atlasMat.mainTextureScale = tileSize;
atlasMat.mainTextureOffset = new Vector2(
    tileIndex.x * tileSize.x,
    tileIndex.y * tileSize.y
);

โค้ดนี้ย่อและเลื่อนหน้าต่างที่ shader ใช้อ่าน texture atlas ทำให้ renderer แสดงแค่ชิ้นเล็ก ๆ ขนาด 0.25 คูณ 0.25 ของแผ่นที่อยู่ตำแหน่งกริด (2, 1) เท่านั้น แทนที่จะยืด atlas ทั้งแผ่นทับ object

Texture atlas (4x4 grid of tiles, UV space 0..1 across the whole sheet) +------+------+------+------+ | 0,0 | 1,0 | 2,0 | 3,0 | +------+------+------+------+ | 0,1 | 1,1 | 2,1* | 3,1 | +------+------+------+------+ | 0,2 | 1,2 | 2,2 | 3,2 | +------+------+------+------+ * = tile (2,1), the exact tile picked by the code above Trim sheet (one texture, many reusable edge/molding strips) +--------------------------------------------------+ | molding A | pipe B | bolt row C | wood trim D | +--------------------------------------------------+ Different meshes place their UVs over just the strip they need, instead of each needing its own full unique texture.
Tip เว้นช่องว่าง (padding) เล็ก ๆ ของพิกเซลที่ไม่ได้ใช้ระหว่าง tile ของ atlas ที่อยู่ติดกัน ถ้าไม่เว้นไว้ mip level ที่อยู่ไกล ๆ (คุยต่อในหัวข้อถัดไป) อาจผสมสีจาก tile หนึ่งเข้าไปในขอบของ tile ข้าง ๆ ทำให้เกิดตะเข็บที่มองเห็นได้ ทั้ง ๆ ที่ตอนมองใกล้ ๆ ไม่มีปัญหาอะไรเลย

13. Mip-Map

mip-map คือชุด texture เดียวกันที่คำนวณล่วงหน้าไว้หลายความละเอียด ลดหลั่นกันลงมา — 1024, แล้ว 512, แล้ว 256, ไปเรื่อย ๆ จนถึงพิกเซลเดียว 1x1 — ถูกสร้างขึ้นอัตโนมัติตอน import หรือ build texture

เหตุผลที่เรื่องนี้สำคัญ: เมื่อพื้นผิวที่มี texture อยู่ไกลจากกล้องมากและกินพื้นที่แค่ไม่กี่พิกเซลบนหน้าจอ การอ่าน texture ความละเอียดเต็มจะทำให้เกิด aliasing (สัญญาณรบกวนกะพริบ ๆ) เพราะพิกเซล texture ต้นฉบับจำนวนมากถูกอัดลงมาไม่สม่ำเสมอในพิกเซลหน้าจอแค่ไม่กี่จุด และตัวไหน "ชนะ" อาจเปลี่ยนนิดหน่อยทุกเฟรมตอนกล้องขยับ แทนที่จะทำแบบนั้น GPU จะเลือก (หรือผสมแบบนุ่มนวลระหว่าง) mip level ที่มีขนาดเหมาะสมกับระยะนั้น ๆ — texture ที่เล็กกว่าทั้งอ่านเร็วกว่าและดูนิ่งกว่าทางสายตา ไม่มีอาการกะพริบนั้นเลย

Texture2D tex = myMaterial.mainTexture as Texture2D;
Debug.Log(tex.mipmapCount);
11

texture ขนาด 1024x1024 จะได้ mip level ทั้งหมด 11 ระดับ: 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1 — แต่ละระดับมีความละเอียดครึ่งหนึ่งของระดับก่อนหน้าพอดี จนกว่าจะไม่เหลืออะไรให้ย่อต่อ

Mip 0: 1024 x 1024 [ full detail -- used up close ] Mip 1: 512 x 512 Mip 2: 256 x 256 Mip 3: 128 x 128 Mip 4: 64 x 64 ... Mip 10: 1 x 1 [ one averaged color -- used very far away ]

ตรงนี้เชื่อมโยงกับ tip เรื่อง padding ของ atlas จากหัวข้อที่แล้วโดยตรง: ที่ mip level เล็ก ๆ GPU ได้ผสมพิกเซล texture ข้างเคียงเข้าด้วยกันไปแล้วเพื่อสร้างภาพเล็กนั้น ดังนั้น atlas tile สองตัวที่ชิดกันสนิทโดยไม่มี padding เลยจะเห็นสีเลอะเข้าหากันชัดเจนเมื่อ object อยู่ไกลพอที่จะใช้ mip level เล็ก ๆ พวกนั้น — ทั้ง ๆ ที่ object ตัวเดียวกันดูสะอาดสมบูรณ์แบบตอนมองใกล้ ๆ

14. Glossary

15. Exercises

Exercise 1 UV island ของ background prop ตัวหนึ่งใช้พื้นที่ 20% ของ texture sheet ที่ใช้ร่วมกันขนาด 1024x1024 (ตามแกนที่เกี่ยวข้อง) และ island นั้นห่อหุ้มพื้นผิว 4 เมตรของ prop ในเกม เป้าหมาย texel density ของโปรเจกต์คุณสำหรับ background prop คือ 256 px/m
(a) ใช้ function ComputeTexelDensity จากหัวข้อ 3 คำนวณ texel density จริงของ prop นี้
(b) มันสูงหรือต่ำกว่าเป้าหมาย และต่างกันประมาณกี่เท่า?
(c) บอกวิธีแก้สองแบบที่ต่างกัน และบอกว่าแบบไหนมักเป็นตัวเลือกที่ดีกว่า พร้อมเหตุผล
Show answer

(a) texturePixelsUsed = 1024 * 0.2 = 204.8 density = 204.8 / 4 = 51.2 px/m

(b) เป้าหมายคือ 256 px/m แต่ prop นี้วัดได้ประมาณ 51.2 px/m — ต่ำกว่าเป้าหมายประมาณ 5 เท่า มันจะดูเบลอเห็นได้ชัดเมื่อเทียบกับ background prop อื่น ๆ ที่มี density ถูกต้อง

(c) วิธีแก้ที่ 1: เพิ่มความละเอียดของ texture sheet ทั้งแผ่น (เช่นจาก 1024 ขึ้นไปเป็นขนาดใหญ่พอที่จะได้ 256 px/m ที่สัดส่วน UV 20% เท่าเดิม ซึ่งคำนวณออกมาได้ประมาณ 5120px ปัดขึ้นเป็นเลขยกกำลังสองถัดไปคือ 8192) วิธีแก้ที่ 2: ให้ UV island ของ prop นี้มีสัดส่วนที่ใหญ่ขึ้นบนแผ่นที่ใช้ร่วมกันเดิม — ย่อหรือจัดเรียง island อื่น ๆ ที่อยู่บน texture 1024 เดียวกันใหม่ หรือย้าย prop นี้ไปใช้ texture set ของตัวเองไปเลย วิธีแก้ที่ 2 มักเป็นตัวเลือกที่ดีกว่า: การดันความละเอียดทั้งแผ่นขึ้นเพื่อแก้ island ที่เล็กเกินไปแค่ตัวเดียว จะเปลืองหน่วยความจำให้กับทุก object อื่นที่ใช้แผ่นเดียวกัน ทั้ง ๆ ที่มีแค่ prop ตัวนี้ตัวเดียวที่ต้องการรายละเอียดเพิ่ม การจัดสรร UV space ใหม่ (หรือให้ prop มี texture เป็นของตัวเอง) แก้ปัญหาจริงตรงจุดโดยไม่เปลืองหน่วยความจำของสิ่งอื่นทั้งหมด

Exercise 2 เขียนโค้ด Unity C# ที่ตั้งค่า material แบบง่าย ๆ ไม่มี texture สองตัว โดยใช้แค่ค่า Metallic กับ Smoothness แบบสม่ำเสมอ (เรียบ ไม่มี texture): ถ้วยรางวัลโครเมียม กับ ยางรถยนต์ อธิบายค่าที่คุณเลือกให้แต่ละตัว โดยใช้สิ่งที่บทนี้คุยไปเรื่อง metallic map กับ roughness/smoothness map
Show answer
Material chrome = new Material(Shader.Find("Universal Render Pipeline/Lit"));
chrome.SetFloat("_Metallic", 1f);      // pure metal
chrome.SetFloat("_Smoothness", 0.9f);  // very smooth -> roughness ~0.1

Material tire = new Material(Shader.Find("Universal Render Pipeline/Lit"));
tire.SetFloat("_Metallic", 0f);        // a dielectric, not metal
tire.SetFloat("_Smoothness", 0.1f);    // very rough -> roughness ~0.9

โครเมียมเป็นโลหะเปลือยขัดเงา ดังนั้น Metallic ควรอยู่ที่ (หรือใกล้เคียงมาก ๆ กับ) 1 — ค่าเทากลาง ๆ ตรงนี้จะดูผิดหลักฟิสิกส์ ตามที่คุยไว้ในหัวข้อ 8 เรื่อง common mistake เพราะมันขัดเงา พื้นผิวระดับไมโครของมันจึงเรียบมาก ดังนั้น Smoothness จึงสูง (ใกล้ 1) ทำให้เกิดไฮไลต์เล็ก แคบ สว่าง ยางเป็น dielectric — มันไม่ย้อมสีการสะท้อนด้วย base color ของตัวเองแบบที่โลหะทำ — ดังนั้น Metallic จึงอยู่ที่ 0 พื้นผิวของยางก็ขรุขระระดับไมโครด้วย (ด้าน ไม่มันวาว) ดังนั้น Smoothness จึงต่ำ ทำให้เกิดไฮไลต์กว้าง หม่น เบลอ แทนที่จะเป็นไฮไลต์มันวาวคมชัด

Exercise 3 normal map ตัวหนึ่งถูก bake ในเครื่องมือ DCC (Digital Content Creation) โดยใช้รูปแบบ green-channel สไตล์ OpenGL แล้วถูก import เข้า engine ที่คาดหวังรูปแบบสไตล์ DirectX แทน บนหน้าจอ รอยนูนทุกจุดบนโมเดลอ่านออกมาเป็นรอยบุ๋ม และรอยบุ๋มทุกจุดอ่านออกมาเป็นรอยนูน — แสงเงาดูกลับด้าน ทั้ง ๆ ที่ geometry กับการตั้งค่า import อื่น ๆ ของ texture ถูกต้องทุกอย่าง
(a) channel RGB ตัวไหนตัวเดียวที่เป็นสาเหตุของเรื่องนี้ และทำไมต้องเป็นตัวนั้นโดยเฉพาะ ไม่ใช่อีกสองตัว?
(b) เขียนโค้ด C# สั้น ๆ ที่แก้ไข Texture2D ที่ import มาแล้วให้ถูกต้อง
Show answer

(a) channel Green ตามที่คุยไว้ในหัวข้อ 6, R เก็บทิศทาง X (tangent), G เก็บทิศทาง Y (bitangent), และ B เก็บทิศทาง Z (normal ชี้ตรงออก) normal map สไตล์ OpenGL กับสไตล์ DirectX ถูกกำหนดให้แกน bitangent (ขึ้น/ลง) ชี้กลับด้านกันพอดี — R กับ B ยังคงตรงกันในทั้งสองรูปแบบ มีแค่ G เท่านั้นที่กลับด้าน channel ตัวเดียวที่กลับด้านนี้แหละที่มากพอจะทำให้รอยนูนทุกจุดกลายเป็นรอยบุ๋มและกลับกัน

Color[] pixels = normalMap.GetPixels();
for (int i = 0; i < pixels.Length; i++)
{
    Color c = pixels[i];
    c.g = 1f - c.g; // flip the Y / bitangent axis convention
    pixels[i] = c;
}
normalMap.SetPixels(pixels);
normalMap.Apply();

(b) loop นี้อ่านทุกพิกเซล แทนที่ค่า green ด้วย 1 - g (การกลับด้านเต็ม ๆ เพราะ channel ของ normal map วิ่งตั้งแต่ 0 ถึง 1) แล้วเขียนพิกเซลกลับเข้าไป การทำแบบนี้จะสะท้อนแกน bitangent กลับด้าน แปลง map จากรูปแบบหนึ่งไปเป็นอีกรูปแบบหนึ่ง ในโปรดักชันจริง วิธีแก้ที่พบบ่อยกว่าคือ export map ใหม่จากเครื่องมือ DCC ต้นทางโดยเลือกรูปแบบที่ถูกต้อง หรือใช้ตัวเลือก "flip green channel" ที่บาง engine กับเครื่องมือจัดการภาพมีให้ใช้ตรง ๆ — แต่การทำงานข้างใต้ก็คือแบบนี้เป๊ะ ๆ: กลับด้าน G ปล่อย R กับ B ไว้เหมือนเดิม

← กลับไปหน้ารวมบท