โปรแกรมแรก React กับ Three.js

การเขียนโปรแกรมเพื่อสร้างภาพ 3 มิติ ทั้งในรูปแบบ Desktop Application และ Web Application นิยมใช้ Library ที่สร้างขึ้นมาเพื่องานเขียนโปรแกรมกราฟิกโดยเฉพาะ ซึ่งที่นิยมใช้กันมาก คือ OpenGL
ในส่วนของ Web Application ได้นำเอา OpenGL มาปรับให้ทำงานบนเว็บได้ และตั้งชื่อว่า WebGL และใช้ภาษา JavaScript มาพัฒนาเพื่อให้ทำงานบนเว็บได้
WebGL เป็นการผสมผสานงานกราฟิกบนเว็บไซต์ โดยใช้บราวเซอร์ในการประมวลผลแปลคำสั่ง HTML กับภาษา JavaScript ซึ่งภายใน JavaScript นั้นจะต้องมี ไลบรารี ของ OpenGL รวบรวมเอาไว้เป็น API เพื่อทำให้การทำงานคอมพิวเตอร์กราฟิกง่ายขึ้น รองรับกับการเขียนโปรแกรมแบบ 2 มิติ และ 3 มิติ รวมทั้ง interactive กับผู้ใช้ผ่านคีย์บอร์ดและเมาส์ และสุดท้ายการเขียนโปรแกรมแบบแอนิเมชันด้วย เพราะ HTML5 มี Tag Canvas สำหรับสร้างกราฟิกร่วมกับภาษา JavaScript อยู่แล้ว
ต่อมาเริ่มมี Library ของกลุ่มที่พัฒนาต่อยอด เพื่อให้การพัฒนาโปรแกรมกราฟิก 3 มิติเขียนได้ง่ายและรวดเร็วขึ้น หลาย ๆ กลุ่ม แต่ที่ได้รับความนิยมมาก และเพิ่มขึ้นอย่างรวดเร็ว คือ Three.js
ส่วน React Three Fiber คือ การนำไลบรารี Three.js มาประยุกต์ให้ใช้ได้กับ React.js และทางผู้สร้างก็ยังได้อธิบายไว้ใน GitHub Repository ของ React Three Fiber จะไม่มี overhead ที่จะทำให้ช้าลงจากต้นฉบับอย่าง three.js แต่อย่างใด อีกทั้งยังอัพเดตไปพร้อม ๆ กับ three.js อีกด้วย
สร้างโปรเจค React
การสร้างโปรเจค React นั้นต้องติดตั้งโปรแกรม Visual Studio Code และ node.js มาก่อน
เราจะเริ่มต้นเขียน React ด้วยการสร้างโปรเจค React จากการเปิด Terminal ขึ้นมา ตั้งชื่อ App ในตัวอย่างนี้ชื่อว่า hello-three โดยใช้คำสั่ง
npx create-react-app hello-three


ใช้โปรแกรม Visual Studio Code เปิดโปรเจคขึ้นมา

เปิด Terminal ขึ้นมา ทดสอบการทำงานของ React ด้วยคำสั่ง
npm start

ที่เว็บบราวเซอร์ จะแสดง URL เป็น http://localhost:3000/ และมีหน้าเว็บตามรูปด้านล่าง แสดงว่า React พร้อมใช้งานแล้ว

ติดตั้ง React Three Fiber
หยูดการทำงานของ React ด้วยการ กด < Ctrl + c >
แล้วติดตั้ง three เวอร์ชั่น 0.122.0 , react-three-fiber เวอร์ชั่น 5.1.5 และ use-cannon เวอร์ชั่น 0.5.3
npm install three@0.122.0 react-three-fiber@5.1.5 use-cannon@0.5.3

ที่ไฟล์ package.json จะแสดง เวอร์ชั่นในการติดตั้ง

ทดสอบการใช้งาน three.js
เปิดไฟล์ App.js

ลบโค้ดบางส่วนออก แล้วทดสอบการทำงาน

ที่เว็บบราวเซอร์

การสร้างฉาก (scene)
เพื่อให้สามารถแสดงอะไรก็ได้ด้วย three.js เราต้องการ 3 สิ่ง คือ ฉาก-scene กล้อง-camera และตัวแสดงภาพ-renderer เพื่อให้เราสามารถแสดงฉากด้วยกล้องได้
- ฉาก (scene) หมายถึง ฉากที่ต้องการถ่ายภาพ แล้วภาพเหล่านั้นปรากฏขึ้นในภาพ ทั้งนี้เพราะว่า การเขียนโปรแกรม 3 มิติ เป็นการเขียนโปรแกรมเหมือนจริง ในโลกของความจริง คือ เราสามารถเขียนวัตถุขึ้นมา ได้มากมาย วางกระจัดกระจายไปทั่วทุกสารทิศ แต่กล้องถ่ายจะถ่ายภาพติดเฉพาะที่ซีนจะส่องกล้องไปยังตำแหน่งนั้นเท่านั้น หากเราส่องกล้องลงพื้น เราจะเห็นเพียงน้อยนิด หากเราส่องกล้องไปที่วิว ทิวทัศน์ไกล เราจะได้ภาพ (ซีน) เป็นภาพกว้างไกล ดังนั้น จึงสรุปได้ว่า ซีนเกิดขึ้นจากการจับภาพจากจุดที่เราสนใจ จับภาพไปที่ตำแหน่งที่เราจะสื่อไปยังผู้ดูภาพ
- กล้อง (camera) เป็น Object หนึ่งที่ใช้ในการสร้างภาพ ทำหน้าที่เหมือนกล้องถ่ายภาพ จึงมีการกำหนดประเภทของกล้อง และตำแหน่งที่วางของกล้อง มุมกล้อง อัตราส่วนของภาพ ภาพที่ได้ขึ้นอยู่กับคุณสมบัติของกล้องและระยะการวางกล้อง การโฟกัสตำแหน่งที่เน้นจุดใด
- เร็นเดอร์ (render) เป็นการสั่งให้ภาพที่เราเขียนโปรแกรมสร้างขึ้นมานั้น ทำให้ปรากฏเป็นภาพขึ้นมาจริง โดยกระบวนการพล็อท (plot ) จุดทีละจุดจนเกิดภาพ หรือเป็นซีนปรากฏออกมาที่จอภาพ
เขียนโค้ด Three.js
นำเข้า three.js
import * as THREE from 'three'
สร้างอินสแตนซ์ ฉาก (scene)
const scene = new THREE.Scene();
มีกล้องสองสามตัวใน three.js ในตอนนี้ เราจะลองใช้ กล้องเปอร์สเปคทีฟ (PerspectiveCamera) กัน
Perspective หรือ ทัศนมิติ คือ มิติของภาพที่เรามองเห็นผ่านเลนส์ ในกล้องใหญ่ เลนส์มุมกว้างให้ภาพที่กว้างชัดลึก ของใกล้ก็ดูเหมือนไกล และเมื่อใกล้กล้องจะดูใหญ่มาก ถ้าไกลออกไปก็จะดูเล็กลง
const camera = new THREE.PerspectiveCamera(
);
คุณลักษณะแรกคือขอบเขตการมองเห็น FOV คือขอบเขตของฉากที่ปรากฏบนหน้าจอในช่วงเวลาใดก็ตาม ค่าจะเป็นหน่วยองศา ในตัวอย่าง เป็น 75 องศา

const camera = new THREE.PerspectiveCamera(
75,
);
อันที่สองคืออัตราส่วนภาพ (aspect ratio) เกือบทุกครั้งคุณต้องการใช้ความกว้างขององค์ประกอบ (window.innerWidth) หารด้วยความสูง (window.innerHeight) มิฉะนั้น คุณจะได้ผลลัพธ์แบบเดียวกับเมื่อคุณเล่นภาพยนตร์เก่าบนทีวีจอไวด์สกรีน คือ ภาพจะดูบิดเบี้ยว

คุณลักษณะสองประการถัดไปคือระนาบการตัด ใกล้ (Near) และไกล (Far) หมายความว่า วัตถุที่อยู่ไกลจากกล้องมากกว่าค่าใกล้หรือไกลจะไม่แสดงผล
พารามิเตอร์สำหรับตัว PerspectiveCamera จากโค้ดด้านล่างหมายถึง ให้สร้างกล้องเปอร์สเปคทีฟที่มีมุมมอง 75 องศา โดยมีอัตราส่วนกว้างยาวเท่ากับหน้าต่างเบราว์เซอร์ และทำให้มองเห็นเฉพาะวัตถุทั้งหมดภายในระยะทาง 0.1 หน่วยและ 1,000 หน่วยเท่านั้น
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
ถัดมาเป็นตัวเรนเดอร์ ที่ทำให้ปรากฏเป็นภาพขึ้นมาจริง นอกจาก WebGLRenderer ที่เราใช้ที่นี่แล้ว three.js ยังมาพร้อมกับโปรแกรมอื่นๆ อีกสองสามรายการ ซึ่งมักใช้เป็นทางเลือกสำหรับผู้ใช้ที่มีเบราว์เซอร์รุ่นเก่าหรือสำหรับผู้ที่ไม่มีการสนับสนุน WebGL ด้วยเหตุผลบางประการ
const renderer = new THREE.WebGLRenderer();
นอกจากการสร้างอินสแตนซ์ตัวแสดงภาพแล้ว เรายังต้องกำหนดขนาดที่ต้องการให้แสดงที่แอปของเรา เป็นความคิดที่ดีที่จะใช้ความกว้างและความสูงของพื้นที่ แอปของเรา ในกรณีนี้คือความกว้างและความสูงของหน้าต่างเบราว์เซอร์ สำหรับแอปที่เน้นประสิทธิภาพ คุณยังสามารถกำหนดค่า setSize ที่เล็กกว่าได้ เช่น window.innerWidth/2 และ window.innerHeight/2 ซึ่งจะทำให้แอปแสดงผลที่ quarter size เป็นต้น
renderer.setSize(
window.innerWidth,
window.innerHeight
);
สุดท้ายแต่ไม่ท้ายสุด เราเพิ่มองค์ประกอบตัวแสดงภาพลงในเอกสาร HTML ของเรา นี่คือองค์ประกอบ canvas ที่โปรแกรมแสดงภาพใช้เพื่อแสดงฉากให้เราเห็น
document.body.appendChild( renderer.domElement );
โค้ดทั้งหมด ณ.ตอนนี้

import './App.css';
import * as THREE from 'three'
function App() {
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(
window.innerWidth,
window.innerHeight
);
document.body.appendChild(renderer.domElement);
return (
null
);
}
export default App;
ที่เว็บบราวเซอร์

คำสั่งแสดงข้อมูลออกสู่หน้าจอแบบ HTML โดยไม่ต้อง Refresh ด้วยคำสั่ง innerHTML
document.body.innerHTML = '';
ในการสร้างกล่องลูกบาศก์ เราจำเป็นต้องใช้ BoxGeometry (จีโอ๊เมทรี่ Geometry แปลว่า เรขาคณิต) และนี่คือวัตถุที่มีจุดทั้งหมด (vertices) และเติม (faces) ของลูกบาศก์
const geometry = new THREE.BoxGeometry();
นอกจากรูปทรงแล้ว เรายังต้องการวัสดุสำหรับระบายสี โดย Three.js มาพร้อมกับวัสดุหลายอย่าง แต่เราจะทดลองใช้วัสดุพื้นฐานคือ MeshBasicMaterial ก่อนในตอนนี้ เพื่อให้ทุกอย่างเรียบง่าย เราใส่แอตทริบิวต์สีเป็น blue ซึ่งเป็นน้ำเงินเท่านั้น
const material = new THREE.MeshBasicMaterial({
color: 'blue'
});
สิ่งที่สามที่เราต้องการคือตาข่าย (mesh) ตาข่ายเป็นวัตถุที่ใช้รูปทรงเรขาคณิต และใช้วัสดุกับมัน จากนั้นเราสามารถแทรกเข้าไปในฉากของเรา และเคลื่อนที่ไปรอบๆ ได้อย่างอิสระ
const cube = new THREE.Mesh(geometry, material);
โดยค่าเริ่มต้น เมื่อเราเรียก scene.add() สิ่งที่เราเพิ่มจะถูกเพิ่มลงในพิกัด (0,0,0) ซึ่งจะทำให้ทั้งกล้องและกล่องลูกบาศก์แสดงในจุดเดียวกัน
scene.add(cube);
เพื่อหลีกเลี่ยงปัญหานี้ เราเพียงแค่ขยับกล้องออกเล็กน้อย
camera.position.z = 5;
โค้ดทั้งหมด ณ.ตอนนี้

import './App.css';
import * as THREE from 'three'
function App() {
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(
window.innerWidth,
window.innerHeight
);
document.body.innerHTML = '';
document.body.appendChild(renderer.domElement);
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({
color: 'blue'
});
camera.position.z = 5;
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
return (
null
);
}
export default App;
ที่เว็บบราวเซอร์

แอนิเมชั่น
ที่เราสร้างไว้ก่อนหน้านี้ คุณจะไม่เห็นอะไรเลย เนื่องจากเรายังไม่ได้แสดงอะไรจริงๆ เพื่อที่เราต้องการสิ่งที่เรียกว่าการเรนเดอร์หรือแอนิเมชั่นลูป
การดำเนินการนี้จะสร้างลูปที่ทำให้ตัวแสดงภาพวาดฉากทุกครั้งที่รีเฟรชหน้าจอ (ในหน้าจอทั่วไป หมายถึง 60 ครั้งต่อวินาที) หากคุณเพิ่งเริ่มเขียนเกมในเบราว์เซอร์ คุณอาจพูดว่า “ทำไมเราไม่สร้าง setInterval ล่ะ ?” สิ่งนี้คือ – เราทำได้ แต่ requestAnimationFrame มีข้อดีหลายประการ บางทีสิ่งที่สำคัญที่สุดคือการหยุดชั่วคราวเมื่อผู้ใช้นำทางไปยังแท็บเบราว์เซอร์อื่น จึงไม่เปลืองพลังการประมวลผลอันมีค่า
function animate() {
requestAnimationFrame( animate );
renderer.render( scene, camera );
}
animate();
โค้ดทั้งหมด ณ.ตอนนี้

import './App.css';
import * as THREE from 'three'
function App() {
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(
window.innerWidth,
window.innerHeight
);
document.body.innerHTML = '';
document.body.appendChild(renderer.domElement);
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({
color: 'blue'
});
camera.position.z = 5;
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
return (
null
);
}
export default App;
ที่เว็บบราวเซอร์ คุณจะเห็นกล่องสีน้ำเงิน

มาทำให้ทั้งหมดน่าสนใจยิ่งขึ้นด้วยการหมุนมัน เพิ่มโค้ดต่อไปนี้ที่ด้านบน renderer.render ในฟังก์ชัน animate ของคุณ:
การดำเนินการนี้จะดำเนินการทุกเฟรม (ปกติ 60 ครั้งต่อวินาที) และทำให้ลูกบาศก์มีภาพเคลื่อนไหวการหมุนที่ดี
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
โค้ดทั้งหมด ณ.ตอนนี้

import './App.css';
import * as THREE from 'three'
function App() {
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(
window.innerWidth,
window.innerHeight
);
document.body.innerHTML = '';
document.body.appendChild(renderer.domElement);
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({
color: 'blue'
});
camera.position.z = 5;
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
function animate() {
requestAnimationFrame(animate);
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
}
animate();
return (
null
);
}
export default App;
ที่เว็บบราวเซอร์ คุณจะเห็นกล่องลูกบาศก์สีน้ำเงินเคลื่อนไหว แต่เมื่อขยายขนาดของ เว็บบราวเซอร์ กล่องลูกบาศก์สีน้ำเงินจะไม่ได้อยู่กึ่งกลาง ของจอภาพ และ ขนาดของฉากไม่ได้ขยายตาม

การอัปเดต Camera View ในการปรับขนาด
ในการไม่อัปเดต Camera View เมื่อปรับขนาดคืออะไรให้ลองปรับขนาดหน้าต่างเบราว์เซอร์เมื่อแท็บโครงการของคุณเปิดอยู่ คุณจะเห็นพื้นที่สีขาวในขณะที่คุณทำวิธีแก้ปัญหานั้นได้ด้วยการรีเฟรช แต่คุณไม่สามารถรีเฟรชได้ทุกครั้งที่คุณปรับขนาดหน้าต่างเบราว์เซอร์ นั่นคือจุดเริ่มต้นของการอัปเดตใน Resize โชคดีที่ Three.js มีโค้ดไม่กี่บรรทัดเพื่อแก้ไขปัญหาดังกล่าว ตามที่แสดงในด้านล่าง
เพิ่มโค้ดดังนี้
window.addEventListener('resize',
() => {
renderer.setSize(window.innerWidth, window.innerHeight)
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
}
);

import './App.css';
import * as THREE from 'three'
function App() {
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(
window.innerWidth,
window.innerHeight
);
document.body.innerHTML = '';
document.body.appendChild(renderer.domElement);
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({
color: 'blue'
});
camera.position.z = 5;
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
function animate() {
requestAnimationFrame(animate);
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize',
() => {
renderer.setSize(window.innerWidth, window.innerHeight)
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
}
);
return (
null
);
}
export default App;
ผลลัพธ์การทำงาน
ดูผ่านเว็บไซต์ได้ที่
ที่เว็บบราวเซอร์ คุณจะเห็นกล่องลูกบาศก์สีน้ำเงินเคลื่อนไหว และเมื่อขยายขนาดของ เว็บบราวเซอร์ กล่องลูกบาศก์สีน้ำเงินจะอยู่กึ่งกลางของจอภาพโดยอัตโนมัติ และ ขนาดของฉากสีดำก็ขยายตามขนาดของจอภาพโดยอัตโนมัติ เช่นกัน
