แสงและเงา (Lights and Shadows) three.js
แสงและเงา เป็นองค์ประกอบของศิลป์ที่อยู่คู่กันแสง เมื่อส่องกระทบ กับวัตถุ จะทำให้เกิดเงา แสงและเงา เป็นตัวกำหนดระดับของค่าน้ำหนัก ความเข้มของเงาจะขึ้นอยู่กับความเข้มของเแสง ในที่ที่มีแสงสว่างมาก เงาจะเข้มขึ้น และในที่ที่มีแสงสว่างน้อย เงาจะไม่ชัดเจน ในที่ที่ไม่มีแสงสว่างจะไม่มีเงา และเงาจะอยู่ในทางตรงข้ามกับแสงเสมอ
ข้อกำหนดเบื้องต้น
ข้อกำหนดสำหรับบทความนี้คือ คุณควรได้ทำตามบทความ ภาพรวมเรขาคณิต (Overview of Geometry) มาก่อน
ในบทความที่แล้ว เราสร้างเรขาคณิตที่กำหนดเองนี้ เราสามารถกำจัดมันได้ในตอนนี้
import './App.css';
import { Canvas, useFrame, useThree, extend } from 'react-three-fiber';
import { useRef } from 'react';
import {
OrbitControls
} from 'three/examples/jsm/controls/OrbitControls';
import * as THREE from 'three'
extend({ OrbitControls });
const Orbit = () => {
const { camera, gl } = useThree();
return (
<orbitControls args={[camera, gl.domElement]} />
)
}
const Box = props => {
const ref = useRef();
useFrame(state => {
ref.current.rotation.x += 0.01;
ref.current.rotation.y += 0.01;
})
return (
<mesh ref={ref} {...props}>
<boxBufferGeometry />
<meshBasicMaterial color='blue' />
</mesh>
)
}
function App() {
return (
<div style={{ height: '100vh', width: '100vw' }}>
<Canvas
style={{ background: 'black' }}
camera={{ position: [3, 3, 3] }}
>
<Orbit />
<axesHelper args={[5]} />
<Box position={[-1, 1, 2]} />
</Canvas>
</div>
);
}
export default App;


และในตอนนี้เราจะเปลี่ยนกล่องนี้จาก meshBasicMaterial ไปเป็น meshPhysicalMaterial
MeshPhysicalMaterial
ส่วนขยายของ MeshStandardMaterial ซึ่งให้คุณสมบัติการเรนเดอร์ตามจริงขั้นสูง:
- เคลียร์โค้ท: วัสดุบางชนิด เช่น สีรถ คาร์บอนไฟเบอร์ และพื้นผิวเปียก จำเป็นต้องมีชั้นสะท้อนแสงที่ชัดเจนที่ด้านบนของอีกชั้นหนึ่งที่อาจไม่สม่ำเสมอหรือหยาบ เคลียร์โค้ทใกล้เคียงกับเอฟเฟกต์นี้ โดยไม่ต้องใช้พื้นผิวโปร่งใสแยกต่างหาก
- ความโปร่งใสตามร่างกาย: ข้อจำกัดหนึ่งของ .opacity คือวัสดุที่มีความโปร่งใสสูงจะสะท้อนแสงน้อยกว่า .transmission ตามร่างกายให้ตัวเลือกที่สมจริงยิ่งขึ้นสำหรับพื้นผิวที่บางและโปร่งใส เช่น แก้ว
- การสะท้อนแสงขั้นสูง: การสะท้อนแสงที่ยืดหยุ่นมากขึ้นสำหรับวัสดุที่ไม่ใช่โลหะ
จากคุณสมบัติการแรเงาที่ซับซ้อนเหล่านี้ MeshPhysicalMaterial จึงมีต้นทุนต่อพิกเซลที่มีประสิทธิภาพสูงกว่าวัสดุอื่น ๆ ของ three.js เอฟเฟกต์ส่วนใหญ่จะปิดใช้งานโดยค่าเริ่มต้น เพราะจะเพิ่มพลังในการประมวลผลเมื่อเปิดใช้งาน และเพื่อผลลัพธ์ที่ดีที่สุด ให้ระบุแผนที่สภาพแวดล้อมเสมอเมื่อใช้วัสดุนี้
จากคุณสมบัติการแสดงผลตาม เช่น โค้ดที่ชัดเจน ความโปร่งใส และการสะท้อนกลับ และสิ่งสำคัญคือต้องสังเกตว่าด้วยคุณสมบัติที่ยอดเยี่ยมเหล่านี้มาพร้อมกับต้นทุนด้านประสิทธิภาพ ดังนั้น พึงระลึกไว้เสมอว่ายิ่งวัสดุมีความซับซ้อนมากเท่าใด พลังในการประมวลผลก็จะยิ่งมากขึ้นเท่านั้น
แก้โค้ด เปลี่ยนจาก meshBasicMaterial เป็น meshPhysicalMaterial
<meshPhysicalMaterial color='blue' />

ดังนั้นกล่องของเราก็อยู่ที่นั่น แต่คุณเห็นว่ามันมืดสนิท และนั่นเป็นเพราะว่าไม่มีแสง

AmbientLight
ไปที่ docs เว็บไซต์ https://threejs.org/ และค้นหาแสง Light -> AmbientLight

มาเพิ่มแสง ambient light ให้กับ canvas
<ambientLight />

แสงนี้ส่องวัตถุทั้งหมดในฉากอย่างเท่าเทียมกัน และแสงนี้ใช้ในการสร้างเงาไม่ได้เนื่องจากไม่มีทิศทาง

เงา (shadows)
ambientLight มันทำให้ทุกอย่างสว่างขึ้น ตัวสร้างแสงโดยรอบใช้สีและความเข้ม ดูเหมือนว่าสีเริ่มต้นจะเป็นสีขาว และเราจะปรับความเข้ม
<ambientLight intensity={0.5}/>
แต่เราจะเพิ่มแสงอีกสักนาทีก่อนที่จะเพิ่มแสง มาสร้างพื้น (Floor) กันเถอะ
ดังนั้นพื้นของเราจึงเกือบจะเหมือนกับกล่องของเรา ยกเว้นว่ามันจะไม่หมุนและมันจะ มีมิติที่แตกต่างกันบ้าง
สร้างพื้น
const Floor = props => {
return (
<mesh {...props}>
<boxBufferGeometry args={[10,1,10]}/>
<meshPhysicalMaterial />
</mesh>
)
}

เอาล่ะ มาวางพื้นในฉาก (scene) ของเรากันเถอะ
<Floor />


เช่นเดียวกัน ที่มี กว้าง (width), ความสูง (height) และ depth (ความลึก) เราจะรักษาความสูงของพื้นไว้ แต่ต้องการยืดมันตามแกน X และ Y มิติเหล่านี้จะเข้าสู่คอนสตรัคเตอร์ ดังนั้นเราจึงส่งผ่าน ARGs และทำให้กว้าง 10 หน่วย สูง 1 หน่วย ให้ความลึก 10 หน่วย
<boxBufferGeometry args={[10,1,10]}/>


เหมือนกับที่เราทำกับกล่อง ฉันจะใส่อุปกรณ์จากพื้น (floor) ลงไปในตาข่าย (mesh )แล้วผมจะขยับตำแหน่งพื้นลงเล็กน้อยเพื่อให้ช่วยนักแสดง (actor’s ) นั่ง (sitting)
ด้านบนของมัน ตอนนี้พื้นหนาหนึ่งหน่วย ครึ่งหนึ่งอยู่เหนือตัวช่วยแกนและอีกครึ่งหนึ่งอยู่ต่ำกว่าเพราะพื้นนี้อยู่กึ่งกลางที่ ที่มา ถ้าเราลื่อนลงไปครึ่งหน่วย พื้นผิวด้านบนควรอยู่ที่จุดเริ่มต้น
<Floor position={[0, -0.5, 0]} />

ได้เลย สมบูรณ์แบบ

แล้วเราจะยืดพื้นออกไปเล็กน้อยตามแกน x ลองให้ความกว้างเป็น 20.
<boxBufferGeometry args={[20,1,10]}/>


เอาล่ะ ลดแสงรอบข้างลงหน่อย และเมื่อถึงจุดปลายที่ Origin เส้นชี้ใช้สีและความเข้ม นอกจากนี้ยังต้องใช้ระยะทางและการสลายตัว ซึ่งเป็นตัวกำหนดว่าแสงจะไปถึงไกลแค่ไหน
<ambientLight intensity={0.2} />
<pointLight />

ตกลง ตอนนี้ประเด็นของเราคือที่มา แต่คุณไม่เห็นมันเป็นเรื่องปกติ เมื่อคุณเพิ่มแสงให้กับฉากนั้น คุณจะไม่เห็นแสงนั้นเอง คุณสามารถเห็นเอฟเฟกต์ของแสงเพื่อให้เห็นภาพตำแหน่งของแสงจุดนี้เท่านั้น

ลองเพิ่มตาข่ายทรงกลม (spherical mesh)
เราจะสร้างส่วนประกอบใหม่ที่เรียกว่า bulb และกำลังจะสร้างตาข่ายทรงกลม ด้วยเรขาคณิตทรงกลม sphereBufferGeometry และเลือกวัสดุ meshPhongMaterial เพราะมันมีคุณสมบัติขนาดใหญ่ที่จะทำให้ดูเหมือนเปล่งแสง
const Bulb = props => {
return (
<mesh>
<sphereBufferGeometry />
<meshPhongMaterial />
</mesh>
)
}

แสง (Light)
มาเพิ่มหลอดไฟให้กับผืนผ้าใบกันเถอะ และตอนนี้ มาตั้งค่าคุณสมบัติบนวัสดุ bulb กัน สีที่เราใช้คือสีเหลือง
<meshPhongMaterial emissive='yellow' />
เพิ่ม Bulb ไปที่ Canvas

ตอนนี้ก็เลยดูเหมือนกำลังให้แสงสว่างอยู่บ้าง

ให้เอาแสงจุดนี้ออกจากผ้าใบและเพิ่มไปที่ Bulb ของเรา และจะส่งพร็อพไปที่ mesh
<mesh {...props}>

แล้วมาขยับ Bulb ขึ้นไปอีกหน่อย

Bulb ของเราค่อนข้างใหญ่ในขณะนี้ รูปทรงทรงกลมของบุชแคร์

ปรับรัศมีให้เล็กกว่าเล็กน้อย
<pointLight />
<sphereBufferGeometry args={[0.2]} />


คุณจะสังเกตเห็นว่าตอนนี้กล่องของเราไม่ได้สร้างเงา มีสิ่งที่เราต้องทำเพื่อให้ได้เงา (Shadows)
เพิ่ม shadowMap ที่ Canvas

ที่ pointLight เพิ่มคุณสมบัติ castShadow
<pointLight castShadow />

ดังนั้นบนพื้น Floor ของเรา เราจะได้รับเงา receiveShadow
<mesh {...props} receiveShadow>

ที่กล่อง Box ให้โยนเงาและรับเงา castShadow receiveShadow
<mesh ref={ref} {...props} castShadow receiveShadow >

ตอนนี้เรามีเงาแล้ว

ที่ Bulb หลอดไฟของเราเพิ่มอีกสองสามส่วนกับส่วนตอนกลางคืนเพื่อให้ดูไม่ขาดๆ หายๆ และให้ดูป็นทรงกลม แทนหลอดไฟ
<sphereBufferGeometry args={[0.2, 20, 20]} />

ผลลัพธ์การทำงาน
โค้ด
import './App.css';
import { Canvas, useFrame, useThree, extend } from 'react-three-fiber';
import { useRef } from 'react';
import {
OrbitControls
} from 'three/examples/jsm/controls/OrbitControls';
import * as THREE from 'three'
extend({ OrbitControls });
const Orbit = () => {
const { camera, gl } = useThree();
return (
<orbitControls args={[camera, gl.domElement]} />
)
}
const Box = props => {
const ref = useRef();
useFrame(state => {
ref.current.rotation.x += 0.01;
ref.current.rotation.y += 0.01;
});
return (
<mesh ref={ref}
{...props}
castShadow
receiveShadow
>
<boxBufferGeometry />
<meshPhysicalMaterial color='blue' />
</mesh>
)
}
const Floor = props => {
return (
<mesh {...props} receiveShadow>
<boxBufferGeometry args={[20, 1, 10]} />
<meshPhysicalMaterial />
</mesh>
)
}
const Bulb = props => {
return (
<mesh {...props}>
<pointLight castShadow />
<sphereBufferGeometry args={[0.2, 20, 20]} />
<meshPhongMaterial emissive='yellow' />
</mesh>
)
}
function App() {
return (
<div style={{ height: '100vh', width: '100vw' }}>
<Canvas
shadowMap
style={{ background: 'black' }}
camera={{ position: [3, 3, 3] }}
>
<ambientLight intensity={0.2} />
<Bulb position={[0, 3, 0]} />
<Orbit />
<axesHelper args={[5]} />
<Box position={[-1, 1, 2]} />
<Floor position={[0, -0.5, 0]} />
</Canvas>
</div>
);
}
export default App;
ดูผ่านเว็บไซต์ได้ที่