Physics Engine part 1 : มาทำความรู้จักกับ physics engine

chayanin
SAIG-KMITL
Published in
4 min readApr 16, 2019
Brackeys channel

เคยสงสัยกันว่าวีดีโอเกมที่เราเล่นกันทำไมมันถึงถูกสร้างมาสมจริงนัก ตัวละครที่เราเล่นกระโดด ตกลงมาจากที่สูง หรือวิ่ง อะไรเป็นสิ่งที่อยู่เบื้องหลังการทำงานเหล่านี้

ย้อนไปตอนสมัยที่ผมเริ่มหัดเขียนโปรแกรมใหม่ตอนอยู่ปี 1 วิชา computer programming อาจารย์ให้การบ้านตอนท้ายเทอมโดยให้เขียนเกมมาชิ้นนึง โดยใช้ภาษา C ตอนนั้นผมก็ไม่รู้จะเริ่มยังไงดีความรู้ที่มีอยู่แค่ปริ้นตัวอักษรขึ้นบนคอนโซล 80×60 คิดได้แค่ว่าวิธีเช็คการชนก็คือ เช็คตำแหน่งว่าอยู่ที่เดียวกันหรือป่าว มันก็เลยจะเกิดปัญหาตรงที่ ถ้ามี game object ที่มีขนาดมากกว่า 1 ช่องนั้นจะเช็คยังไง (ตอนนั้นหาวิธีนี้ไม่ได้ ก็เลยไม่มี object ที่โดนชนได้ มีขนาดมากกว่า 1 ช่อง) ซึ่งปัญหาตรงนี้เราสามารถใช้ physics engine เข้ามาช่วยแก้ปัญหานี้ได้

แต่ว่าประโยชน์ของ physics engine ไม่ได้มีเพียงแค่เช็คการชน มันสามารถจำลองกฎทางฟิสิกส์อื่นๆ ได้อีก เช่น แรงโน้มถ่วง แรงเสียดทาน แรง ทอร์ค อย่างที่ gif ด้านล่างได้แสดงเอาไว้ ที่จำลองการตกลงมาของวัตถุที่มีความซับซ้อนอยู่พอสมควร จะเห็นได้ว่าในขณะที่มันกำลังตกลงมาก็จะเกิดการหมุน เนื่องจากความไม่สมดุลของวัตถุ และก็จะสังเกตได้อีกอย่างว่าวัตถุตกลงมาเร็วขึ้นเรื่อยๆ เนื่องจากความเร่งแรงโน้มถ่วง

ถ้าพูดถึง Physics engine ใน game engine แล้ว Physics ก็เหมือน engine ตัวนึงนอกจาก Graphic engine , I/O engine , sound engine และอื่นๆอีก

คำตอบก็คือ Physics Engine ครับ Physics Engine คือ เครื่องมือจำลองกฎทางฟิสิกส์ของนิวตัน เช่น การเคลื่อนที่(motion) ความเร็ว(velocity) ความเร่ง(Acceleration) การตรวจจับการชน(collision detection)

อธิบายแบบนี้คงจะยังนึกภาพไม่ออก มาดูตัวอย่างกันดีกว่า

สมมติว่าเราสร้างกล่องไว้บนอากาศ ในขณะที่เวลาหยุดอยู่(Timescale = 0) จากนั้นปล่อยให้เวลาผ่านไป Physics Engine ก็จะทำหน้าที่ของมันโดยจำลองแรงโน้มถ่วงขึ้นมาดึงให้กล่องตกลงมา

matter.js Demo Air Friction

หลักๆแล้ว Physics Engine ประกอบไปด้วย

  1. World หรือโลกๆนึงที่ Physics Engine สร้างขึ้นมาเพื่อจำลองสถานการณ์ต่างๆที่ใช้ร่วมกัน เช่น เราต้องการจำลองแรงโน้มถ่วงให้กล่องๆนึง แต่ตามกฎของฟิสิกส์แล้ว ถ้าเราจำลองแรงโน้มถ่วงให้กล่องแค่กล่องเดียวคงเป็นเรื่องแปลก เราจึงสร้างกฎที่ต้องใช้ได้กับ object ทุกตัวร่วมกัน
  2. Body เหมือนเป็น Object นึงที่เราสร้างขึ้นมาเพื่อที่จะจำลองฟิสิกส์กับวัตถุนั้น เช่นเราออกแรงกระทำต่อวัตถุนึงด้วยค่าๆนึง วัตถุนั้นก็จะเคลื่อนที่ด้วยความเร่งค่านึง หรือ เมื่อเราต้องการสร้างลูกบอลขึ้นมาสองลูก เราจำเป็นต้องกำหนดขอบเขตการชนให้กับลูกบอล เพื่อเช็คว่าลูกบอลสองลูกนั้นชนกันรึปล่าว
  3. Joints หรือข้อต่อ มันมีประโยชน์ในการฟิสิกส์สำหรับวัตถุที่ต่อกัน วัตถุทางฟิสิกส์นั้นไม่ได้มีแค่วัตถุแข็งเกร็ง ( Rigidbody ) เพียงอย่างเดียว เราจำเป็นต้องจำลองวัตถุที่เปลี่ยนแปลงรูปร่างได้ เช่น โซ่ เสื้อผ้า หรือวัตถุที่มีองค์ประกอบเล็กๆ

Box2D

การอธิบายด้วยทฤษฎีอาจทำให้เห็นภาพได้ยาก ผมยกตัวอย่างไลบราลี่นึงมาแสดงให้ดู

Box2d เป็น open-source physics engine เขียนด้วยภาษา C++ มีเกมหลายเกมที่ใช้งาน เช่น angry bird

#include <Box2D/Box2D.h>
#include <cstdio>
int main()
{
// Define the gravity vector.
b2Vec2 gravity(0.0f, -10.0f);

// Construct a world object, which will hold and simulate the rigid bodies.
// Allow bodies to sleep.
b2World world(gravity, true);

// Define the ground body.
b2BodyDef groundBodyDef;
groundBodyDef.position.Set(0.0f, -10.0f);
b2Body* groundBody = world.CreateBody(&groundBodyDef);

// Define the ground box shape.
b2PolygonShape groundBox;
groundBox.SetAsBox(50.0f, 10.0f);

// Add the ground fixture to the ground body.
groundBody->CreateFixture(&groundBox, 0.0f);

// Define the dynamic body. Set its position and call the body factory.
b2BodyDef bodyDef;
bodyDef.type = b2_dynamicBody;
bodyDef.position.Set(0.0f, 4.0f);
bodyDef.linearVelocity.Set(5.0f, 5.0f);
bodyDef.angle = 0.25f * b2_pi;

b2Body* body = world.CreateBody(&bodyDef);

// Define another box shape for your dynamic body.
b2PolygonShape dynamicBox;
dynamicBox.SetAsBox(1.0f, 1.0f);

// Define the dynamic body fixture.
b2FixtureDef fixtureDef;
fixtureDef.shape = &dynamicBox;
fixtureDef.density = 1.0f;
fixtureDef.friction = 0.3f;

// Add the shape to the body.
body->CreateFixture(&fixtureDef);

float32 timeStep = 1.0f / 60.0f;
int32 velocityIterations = 6;
int32 positionIterations = 2;

do {
world.Step(timeStep, velocityIterations, positionIterations);
world.ClearForces();

b2Vec2 position = body->GetPosition();
float32 angle = body->GetAngle();

printf(“%4.2f %4.2f %4.2f\n”, position.x, position.y, angle);

} while (body->IsAwake());

return 0;
}

จากโค้ดข้างต้น ขั้นแรกเราต้องสร้างโลก ( world )ขึ้นมาก่อนโดยกำหนดค่าแรงโน้มถ่วงจากตัวแปร b2Vec2 แล้วสร้างโลกจากแรงโน้มถ่วงนั้น

b2Vec2 gravity(0.0f, -10.0f);
b2World world(gravity, true);

จากนั้นก็สร้างพื้นขึ้นมา โดยที่พื้นนี้จะไม่สามารถเคลื่อนย้าย และชนกับวัตถุอื่นได้

b2BodyDef groundBodyDef;
groundBodyDef.position.Set(0.0f, -10.0f);
b2Body* groundBody = world.CreateBody(&groundBodyDef);

แล้วก็สร้างรูปร่าง ( shape ) ให้กับพื้น

b2PolygonShape groundBox;
groundBox.SetAsBox(50.0f, 10.0f);

เมื่อเรามี body และ shape ของพื้นขึ้นมาแล้ว เราก็จะเชื่อมสองอย่างนี้เข้าด้วยกันโดยใช้ fixture

groundBody->CreateFixture(&groundBox, 0.0f);

พอเราสร้างพื้นขึ้นมาเสร็จ เราก็จะสร้างวัตถุที่เคลื่อนที่ได้ (Dynamic Body )ขึ้นมา แต่โดย default แล้วชนิดของ body จะเป็น static เราจึงต้องตั้งให้มันเป็น dynamic

 b2BodyDef bodyDef;
bodyDef.type = b2_dynamicBody;
bodyDef.position.Set(0.0f, 4.0f);
bodyDef.linearVelocity.Set(5.0f, 5.0f);
bodyDef.angle = 0.25f * b2_pi;

b2Body* body = world.CreateBody(&bodyDef);

เหมือนกับที่เราสร้างพื้นขึ้น คือเราจะสร้าง shape ให้กับวัตถุนี้แล้ว ใช้ fixture เชื่อมมันแต่คราวนี้ เราจะกำหนดค่าให้กับ fixture โดยตัว fixture จะเก็บค่า density , friction , restitution ไว้ด้วย และ Body นึงอาจมี fixture ได้หลายตัว แต่ fixture นึง มี shape ได้แค่ตัวเดียว

 // Define another box shape for your dynamic body.
b2PolygonShape dynamicBox;
dynamicBox.SetAsBox(1.0f, 1.0f);

// Define the dynamic body fixture.
b2FixtureDef fixtureDef;
fixtureDef.shape = &dynamicBox;
fixtureDef.density = 1.0f;
fixtureDef.friction = 0.3f;

// Add the shape to the body.
body->CreateFixture(&fixtureDef);

เนื่องจาก Box2D เทคนิคที่เรียกว่า Integrators ซึ่งมันจะจำลองกฎทางฟิสิกส์ ณ ขณะเวลาหนึ่ง ( Time step ) เราจึงต้องกำหนด เวลาให้มันทำงาน

float32 timeStep = 1.0f / 60.0f;

โดยที่การคำนวณนั้นจะแบ่งออกเป็น 2 ช่วง คือ velocity phase และ position phase เราจึงต้องกำหนดค่านี้ด้วย

int32 velocityIterations = 6;
int32 positionIterations = 2;

สุดท้ายเราสร้างลูปการคำนวณ โดยที่ไม่มีการแสดงกราฟฟิกขึ้นมาให้เห็น

do {world.Step(timeStep, velocityIterations, positionIterations);
world.ClearForces();

b2Vec2 position = body->GetPosition();
float32 angle = body->GetAngle();

printf(“%4.2f %4.2f %4.2f\n”, position.x, position.y, angle);

} while (body->IsAwake());

อ่านเพิ่มเติม สำหรับ Box2D : https://box2d.org/

Matter.js

matter.js เป็นไลบราลี่ตัวนึงที่ใช้ภาษา javascript และรันบนเว็บบราวเซอร์ได้

http://brm.io/matter-js/demo/#doublePendulum

matter.js นั้นถูกสร้างมาเพื่อให้ง่ายต่อการใช้งานมากกว่า Box2D เพื่อความเหมาะสมกับภาษา javascript และการใช้งานบนเว็บบราวเซอร์

var Engine = Matter.Engine,
Render = Matter.Render,
World = Matter.World,
Bodies = Matter.Bodies;

var engine = Engine.create();

var render = Render.create({
element: document.body,
engine: engine,
options: {
width: 800,
height: 400,
wireframes: false
}
});

var boxA = Bodies.rectangle(400, 200, 80, 80);
var ballA = Bodies.circle(380, 100, 40, 10);
var ballB = Bodies.circle(460, 10, 40, 10);
var ground = Bodies.rectangle(400, 380, 810, 60,{isStatic: true });

World.add(engine.world, [ boxA, ballA, ballB, ground ]);

Engine.run(engine);
Render.run(render);

ขั้นแรกที่เราจะทำก็ตั้ง alias หรือชื่อเล่นให้กับคลาสที่เราจะใช้ จากชื่อคลาสที่เราจะใช้ เห็นได้ว่า matter.js เหมือนกับ Box2D คือมันจะสร้าง world กับ body ขึ้นมา แล้วส่ง instance เหล่านั้นไปให้ Matter.Engine ทำงานต่อ

var Engine = Matter.Engine,
Render = Matter.Render,
World = Matter.World,
Bodies = Matter.Bodies;

สร้าง Engine ขึ้นมา

var engine = Engine.create();

นอกนั้นจากนั้น matter.js ยังมี built in Renderer ซึ่งเราจะใช้วาดรูปทรงออกมา

var render = Render.create({
element: document.body,
engine: engine,
options: {
width: 800,
height: 400,
wireframes: false
}
});

ต่อมาเราสร้าง instance ของรูปทรงที่เราอยากจะใส่เข้าไปในโลก ผ่านทางคลาส Matter.Bodies ซึ่งทำตัวเหมือน factory

var boxA = Bodies.rectangle(400, 200, 80, 80);
var ballA = Bodies.circle(380, 100, 40, 10);
var ballB = Bodies.circle(460, 10, 40, 10);
var ground = Bodies.rectangle(400, 380, 810, 60,{isStatic: true });

แล้วก็ใส่ไปใน world ที่สร้างขึ้นมา

World.add(engine.world, [ boxA, ballA, ballB, ground ]);

แล้วก็สั่งให้ engine กับ renderer ทำงาน ดูผลการรันได้จากข้างล่าง

อ่านเพิ่มเติม สำหรับ matter.js : http://brm.io/matter-js/

สรุปแล้วหลักการทำงานของ physics engine ก็คือการสร้างกฎการทำงานหรือ engine ขึ้นมาและสร้างโลกที่ใช้กฎนั้นขึ้นมา แล้วก็ใส่วัตถุที่มีรูปร่างต่างๆลงไป จากนั้นก็เป็นหน้าที่ของ engine ให้มันทำงานของมัน แล้วถ้าเราอยาก interact กับโลกใบนั้นก็คือส่งค่าไปให้ engine ว่าเกิดอะไรขึ้นกับวัตถุในนั้น engine ก็จะส่งค่ากลับคืนมา

ทั้งหมดที่ผ่านมานั้น ยกตัวอย่างให้ดูแค่ตัวอย่างเล็กๆของ physics engine แต่ที่ game engine ใช้อยู่ปัจจุบันมีฟีเจอร์ล้ำๆเพิ่มเติมขึ้นมาอีกมากมาย เช่น ใน Unity มี Continuous collision detection (CCD) ซึ่งช่วยแก้ปัญหาที่วัตถุเคลื่อนที่เร็วมากๆ มันจะเคลื่อนที่ผ่าน collider โดยที่ไม่เกิดการชนขึ้น หรืออีกตัวอย่างเช่น Cloth physics ช่วยจำลองให้ผ้าเคลื่อนไหวพริ้ว ได้อย่างสมจริงยิ่งขึ้น

--

--