ผมตื่นเต้นมากเมื่อทีม Google Data Arts ติดต่อ Moniker และผมเกี่ยวกับการทำงานร่วมกันเพื่อสำรวจความเป็นไปได้ต่างๆ ที่ WebVR นำมาใช้ เราติดตามผลงานของทีมมาตลอดหลายปีที่ผ่านมา และโปรเจ็กต์ของทีมนี้ตรงใจเราเสมอ การทำงานร่วมกันของเราจึงทำให้เกิดDance Tonite ซึ่งเป็นประสบการณ์การเต้นใน VR ที่เปลี่ยนแปลงอยู่ตลอดเวลาร่วมกับ LCD Soundsystem และแฟนๆ มาดูวิธีที่เราทำกัน
แนวคิด
เราเริ่มต้นด้วยการพัฒนาชุดโปรโตไทป์โดยใช้ WebVR ซึ่งเป็นมาตรฐานแบบเปิดที่ช่วยให้เข้าสู่ VR ได้ด้วยการเข้าชมเว็บไซต์โดยใช้เบราว์เซอร์ เป้าหมายก็คือการทำให้ทุกคนเข้าสู่ประสบการณ์ VR ได้ง่ายขึ้น ไม่ว่าจะใช้อุปกรณ์ใด
เราให้ความสำคัญกับสิ่งนี้ ไม่ว่าเราจะสร้างอะไรขึ้นมา วิดีโอเหล่านั้นก็ควรใช้งานได้กับ VR ทุกประเภท ตั้งแต่ชุดหูฟัง VR ที่ใช้งานได้กับโทรศัพท์มือถือ เช่น Daydream View, Cardboard ของ Google และ Gear VR ของ Samsung ไปจนถึงระบบแบบห้องเสมือนจริง เช่น HTC VIVE และ Oculus Rift ซึ่งจะแสดงการเคลื่อนไหวของร่างกายคุณในสภาพแวดล้อมเสมือนจริง บางทีที่สำคัญที่สุด เราคิดว่ามันคงเป็นจิตวิญญาณของเว็บ ที่จะสร้างบางสิ่งที่เหมาะสำหรับทุกคนที่ไม่ได้เป็นเจ้าของอุปกรณ์ VR
1. การจับภาพภาพเคลื่อนไหว DIY
เนื่องจากเราต้องการดึงดูดผู้ใช้ให้มีส่วนร่วมอย่างสร้างสรรค์ เราจึงเริ่มมองหาโอกาสในการมีส่วนร่วมและการแสดงออกด้วยตนเองโดยใช้ VR เราประทับใจในความละเอียดที่คุณเคลื่อนไหวและมองไปรอบๆ ใน VR ได้ รวมถึงความสมจริงของภาพ ข้อมูลนี้ทำให้เรามีไอเดีย แทนที่จะให้ผู้ใช้ดูหรือสร้างสิ่งใดสิ่งหนึ่ง คุณลองบันทึกการเคลื่อนไหวของผู้ใช้ดูไหม
เราสร้างต้นแบบโดยบันทึกตำแหน่งของแว่น VR และตัวควบคุมขณะเต้น เราแทนที่ตำแหน่งที่บันทึกไว้ด้วยรูปร่างนามธรรมและประหลาดใจกับผลลัพธ์ที่ได้ ผลลัพธ์ที่ได้มีความเป็นมนุษย์และแสดงถึงบุคลิกภาพ เราจึงตระหนักได้อย่างรวดเร็วว่าสามารถใช้ WebVR ในการจับภาพการเคลื่อนไหวแบบประหยัดที่บ้าน
เมื่อใช้ WebVR นักพัฒนาซอฟต์แวร์จะมีสิทธิ์เข้าถึงตำแหน่งศีรษะและการวางแนวของผู้ใช้ผ่านออบเจ็กต์ VRPose ค่านี้จะอัปเดตทุกเฟรมโดยฮาร์ดแวร์ VR เพื่อให้โค้ดแสดงเฟรมใหม่จากมุมมองที่ถูกต้องได้ ผ่าน GamePad API กับ WebVR เรายังเข้าถึงตำแหน่ง/การวางแนวของคอนโทรลเลอร์ของผู้ใช้ผ่านออบเจ็กต์ GamepadPose ได้อีกด้วย เราเพียงแค่จัดเก็บค่าตำแหน่งและการวางแนวทั้งหมดเหล่านี้ในทุกเฟรม เพื่อสร้าง "ไฟล์บันทึก" การเคลื่อนไหวของผู้ใช้
2. ลัทธิมินิมอลและเครื่องแต่งกาย
อุปกรณ์ VR สำหรับขนาดห้องในปัจจุบันช่วยให้เราติดตามการเคลื่อนไหวร่างกายของผู้ใช้ได้ 3 จุด ได้แก่ ศีรษะและมือทั้ง 2 ข้าง ใน Dance Tonite เราต้องการมุ่งเน้นไปที่ความเป็นมนุษย์ในการเคลื่อนไหวของจุดทั้ง 3 จุดนี้ในอวกาศ เราจึงใช้องค์ประกอบให้น้อยที่สุดเพื่อเน้นการเคลื่อนไหว เราชอบแนวคิดของการทำให้สมองของผู้คนทำงานได้
วิดีโอนี้แสดงผลงานของ Gunnar Johansson นักจิตวิทยาชาวสวีเดนเป็นหนึ่งในตัวอย่างที่เราใช้อ้างอิงเมื่อพิจารณาการลดทอนสิ่งต่างๆ ให้เหลือน้อยที่สุด วิดีโอนี้แสดงให้เห็นว่าจุดสีขาวที่ลอยอยู่สามารถจดจำได้ทันทีว่าเป็นวัตถุเมื่อเห็นเป็นภาพเคลื่อนไหว
เราได้แรงบันดาลใจจากห้องสีและเครื่องแต่งกายเรขาคณิตในการบันทึกเหตุการณ์ภาพยนตร์ Triadic Ballet ของ Oskar Schlemmer’s Triadic Ballet ในปี 1970 ของ Margarete Hastings
ในขณะที่เหตุผลที่ Schlemmer เลือกเครื่องแต่งกายเรขาคณิตนามธรรมเพื่อจำกัดการเคลื่อนไหวของนักเต้นให้เหมือนกับหุ่นเชิดและตุ๊กตาเชิด แต่เรากลับมีเป้าหมายตรงข้ามสำหรับ Dance Tonite
สุดท้ายเราจะใช้รูปร่างต่างๆ ที่เรามีให้เลือก โดยดูจากปริมาณข้อมูลที่สื่อเหล่านั้นถ่ายทอดด้วยการหมุน ลูกกลมจะมีลักษณะเหมือนกันไม่ว่าจะหมุนไปทางไหน แต่กรวยจะชี้ไปในทิศทางที่มองอยู่จริงๆ และมีลักษณะแตกต่างจากด้านหน้าและด้านหลัง
3. แป้นเหยียบแบบวนซ้ำสำหรับการเคลื่อนไหว
เราต้องการแสดงภาพกลุ่มคนจำนวนมากที่บันทึกไว้กำลังเต้นและเคลื่อนไหวไปด้วยกัน การทำแบบนั้นแบบเรียลไทม์นั้นไม่สามารถทำได้ เนื่องจากอุปกรณ์ VR มีจำนวนไม่มากพอ แต่เรายังคงต้องการกลุ่มคนที่แสดงความรู้สึกต่อกันและกันผ่านการเคลื่อนไหว เรานึกถึงการแสดงแบบซ้ำซ้อนของ Norman McClaren ในวิดีโอชื่อ "Canon" เมื่อปี 1964
การแสดงของ McClaren แสดงให้เห็นการเคลื่อนไหวด้วยท่าเต้นที่ เริ่มโต้ตอบกันเมื่อเล่นวนซ้ำไปเรื่อยๆ เช่นเดียวกับ Loop Pedal ในดนตรีที่นักดนตรีเล่นดนตรีด้วยตนเองโดยวางเลเยอร์เพลงสดที่แตกต่างกัน เราอยากรู้ว่าจะสร้างสภาพแวดล้อมที่ผู้ใช้สามารถด้นสดการแสดงในเวอร์ชันที่อิสระขึ้นได้อย่างอิสระได้ไหม
4. ห้องที่เชื่อมถึงกัน
แทร็กต่างๆ ของ LCD Soundsystem สร้างขึ้นโดยใช้มาตรการที่วัดเวลาอย่างแม่นยำเช่นเดียวกับเพลงอื่นๆ จำนวนมาก แทร็ก Tonite ของศิลปินดังกล่าวซึ่งใช้ในโปรเจ็กต์ของเรามีมาตรการที่มีความยาว 8 วินาทีพอดี เราอยากให้ผู้ใช้เพิ่มประสิทธิภาพ ต่อทุกๆ 8 วินาทีในแทร็ก แม้ว่าจังหวะของท่อนเหล่านี้จะไม่เปลี่ยนแปลง แต่เนื้อหาทางดนตรีจะเปลี่ยนแปลง เมื่อเพลงดำเนินไปเรื่อยๆ จะมีช่วงที่มีเครื่องดนตรีและเสียงร้องที่แตกต่างกัน ซึ่งผู้แสดงสามารถตอบสนองได้หลายวิธี แต่ละวิธีจะแสดงเป็นห้องซึ่งผู้คนสามารถสร้างประสิทธิภาพที่เหมาะกับห้องได้
การเพิ่มประสิทธิภาพเพื่อรักษาเฟรม: ไม่ลดเฟรม
การสร้างประสบการณ์ VR หลายแพลตฟอร์มที่ทำงานบนฐานโค้ดเดียวโดยให้ประสิทธิภาพที่ดีที่สุดสำหรับอุปกรณ์หรือแพลตฟอร์มแต่ละประเภทนั้นไม่ใช่เรื่องง่าย
เมื่ออยู่ใน VR สิ่งที่น่ารังเกียจที่สุดอย่างหนึ่งที่คุณสัมผัสได้จะเกิดจากอัตราเฟรมที่ไม่ตรงกับการเคลื่อนไหวของคุณ หากคุณหันศีรษะแต่ภาพในตาที่เห็นไม่ตรงกับการเคลื่อนไหวที่หูชั้นในรู้สึก ก็จะทำให้ท้องไส้ปั่นป่วนทันที ด้วยเหตุนี้ เราจึงต้องหลีกเลี่ยงความล่าช้าของอัตราเฟรมที่มาก ต่อไปนี้คือการเพิ่มประสิทธิภาพบางส่วนที่เรานำมาใช้
1. เรขาคณิตบัฟเฟอร์แบบอินสแตนซ์
เนื่องจากโปรเจ็กต์ทั้งหมดของเราใช้วัตถุ 3 มิติเพียงไม่กี่ชิ้น เราจึงได้รับประสิทธิภาพที่เพิ่มขึ้นอย่างมากโดยใช้ Instanced Buffer Geometry โดยพื้นฐานแล้ว ฟีเจอร์นี้จะช่วยให้คุณอัปโหลดออบเจ็กต์ไปยัง GPU ได้เพียงครั้งเดียวและวาด "อินสแตนซ์" ของออบเจ็กต์นั้นๆ ได้มากเท่าที่ต้องการในการเรียกใช้การวาดเพียงครั้งเดียว ใน Dance Tonite เรามีวัตถุที่แตกต่างกันเพียง 3 ชิ้น (กรวย ทรงกระบอก และห้องที่มีรู) แต่วัตถุเหล่านั้นอาจมีหลายร้อยสำเนา เรขาคณิตบัฟเฟอร์ของอินสแตนซ์เป็นส่วนหนึ่งของ ThreeJS แต่เราใช้ Dusan Bosnjak ในการทดลองและ Fork ที่กำลังดำเนินการอยู่ซึ่งจะนำไปใช้THREE.InstanceMesh
ซึ่งทำให้การทำงานกับ Insistent Buffer Geometry ง่ายยิ่งขึ้น
2. การหลีกเลี่ยง Garbage Collector
JavaScript จะเพิ่มหน่วยความจำโดยอัตโนมัติเช่นเดียวกับภาษาสคริปต์อื่นๆ หลายภาษา โดยค้นหาออบเจ็กต์ที่สงวนไว้ซึ่งไม่ได้ใช้งานแล้ว กระบวนการนี้เรียกว่า การเก็บขยะ
นักพัฒนาแอปไม่สามารถควบคุมได้ว่าเมื่อใดที่ระบบจะดำเนินการนี้ ผู้เก็บขยะอาจมาปรากฏตัวที่ประตูของเราได้ทุกเมื่อและเริ่มยกขยะออก ส่งผลให้เฟรมตกขณะที่ใช้เวลานาน
วิธีแก้ปัญหานี้คือผลิตขยะให้น้อยที่สุดโดยการรีไซเคิลวัตถุของเรา เราได้ทําเครื่องหมายออบเจ็กต์สแครชไว้เพื่อใช้ซ้ำแทนการสร้างออบเจ็กต์เวกเตอร์ใหม่สําหรับการคํานวณแต่ละครั้ง เนื่องจากเรายึดถือโดยการย้ายการอ้างอิงไปยังเนื้อหาที่อยู่นอกขอบเขตของเรา จึงไม่ถูกทำเครื่องหมายให้นำออก
ตัวอย่างเช่น นี่เป็นโค้ดของเราในการแปลงเมทริกซ์ตำแหน่งของศีรษะและมือของผู้ใช้ให้เป็นอาร์เรย์ของค่าตำแหน่ง/การหมุนที่เราจัดเก็บไว้ในแต่ละเฟรม การใช้ SERIALIZE_POSITION
, SERIALIZE_ROTATION
และ SERIALIZE_SCALE
ซ้ำจะช่วยหลีกเลี่ยงการจัดสรรหน่วยความจำและการรวบรวมขยะที่จะเกิดขึ้นหากเราสร้างออบเจ็กต์ใหม่ทุกครั้งที่เรียกใช้ฟังก์ชัน
const SERIALIZE_POSITION = new THREE.Vector3();
const SERIALIZE_ROTATION = new THREE.Quaternion();
const SERIALIZE_SCALE = new THREE.Vector3();
export const serializeMatrix = (matrix) => {
matrix.decompose(SERIALIZE_POSITION, SERIALIZE_ROTATION, SERIALIZE_SCALE);
return SERIALIZE_POSITION.toArray()
.concat(SERIALIZE_ROTATION.toArray())
.map(compressNumber);
};
3. การจัดเรียงลําดับการเคลื่อนไหวและการเล่นแบบเป็นขั้นเป็นตอน
ในการจับการเคลื่อนไหวของผู้ใช้ใน VR เราจำเป็นต้องจัดเรียงตำแหน่งและการหมุนของชุดหูฟังและตัวควบคุม แล้วอัปโหลดข้อมูลนี้ไปยังเซิร์ฟเวอร์ของเรา เราเริ่มจากการบันทึกเมทริกซ์การแปลงทั้งหมด สำหรับทุกเฟรม การดำเนินการนี้มีประสิทธิภาพดี แต่ตัวเลข 16 รายการคูณด้วยตำแหน่งละ 3 ตำแหน่งที่ 90 เฟรมต่อวินาทีทำให้ไฟล์มีขนาดใหญ่มาก จึงต้องรอนานขณะอัปโหลดและดาวน์โหลดข้อมูล การแยกเฉพาะข้อมูลตำแหน่งและการหมุนเวียนจากเมทริกซ์การเปลี่ยนรูปแบบช่วยให้เราลดค่าเหล่านี้ได้จาก 16 ถึง 7
เนื่องจากผู้เข้าชมบนเว็บมักจะคลิกลิงก์โดยที่ไม่รู้ว่าจะได้พบกับอะไร เราจึงต้องแสดงเนื้อหาที่เป็นภาพอย่างรวดเร็ว ไม่เช่นนั้นผู้เข้าชมจะออกจากเว็บไซต์ภายในไม่กี่วินาที
ด้วยเหตุนี้ เราจึงต้องการตรวจสอบว่าโปรเจ็กต์ของเราจะเริ่มเล่นได้เร็วที่สุด ตอนแรกเราใช้ JSON เป็นรูปแบบในการโหลดข้อมูลการเคลื่อนไหว ปัญหาคือเราต้องโหลดไฟล์ JSON ที่สมบูรณ์ก่อนจึงจะแยกวิเคราะห์ได้ ไม่ค่อยก้าวหน้า
เบราว์เซอร์มีเวลาเพียงเล็กน้อยในแต่ละเฟรมสําหรับการคํานวณ JavaScript เพื่อให้โปรเจ็กต์อย่าง Dance Tonite แสดงที่อัตราเฟรมสูงสุดได้ หากใช้เวลานานเกินไป ภาพเคลื่อนไหวจะเริ่มกระตุก ตอนแรกเราพบปัญหาการกระตุกเมื่อเบราว์เซอร์ถอดรหัสไฟล์ JSON ขนาดใหญ่เหล่านี้
เราพบรูปแบบข้อมูลสตรีมมิงที่สะดวกซึ่งเรียกว่า NDJSON หรือ JSON ที่แบ่งด้วยบรรทัดใหม่ เคล็ดลับในการสร้างไฟล์นี้คือการสร้างไฟล์ที่มีชุดสตริง JSON ที่ถูกต้อง โดยแต่ละรายการจะอยู่คนละบรรทัดกัน ซึ่งจะช่วยให้คุณแยกวิเคราะห์ไฟล์ได้ขณะที่ระบบกำลังโหลด ทำให้เราแสดงประสิทธิภาพได้ก่อนที่จะโหลดเสร็จ
นี่คือลักษณะของส่วนหนึ่งในการบันทึกของเรา
{"fps":15,"count":1,"loopIndex":"1","hideHead":false}
[-464,17111,-6568,-235,-315,-44,9992,-3509,7823,-7074, ... ]
[-583,17146,-6574,-215,-361,-38,9991,-3743,7821,-7092, ... ]
[-693,17158,-6580,-117,-341,64,9993,-3977,7874,-7171, ... ]
[-772,17134,-6591,-93,-273,205,9994,-4125,7889,-7319, ... ]
[-814,17135,-6620,-123,-248,408,9988,-4196,7882,-7376, ... ]
[-840,17125,-6644,-173,-227,530,9982,-4174,7815,-7356, ... ]
[-868,17120,-6670,-148,-183,564,9981,-4069,7732,-7366, ... ]
...
การใช้ NDJSON จะช่วยให้เราแสดงข้อมูลของแต่ละเฟรมของการแสดงเป็นสตริงได้ เราสามารถรอจนกว่าจะถึงเวลาที่จำเป็น จากนั้นจะถอดรหัสข้อมูลเหล่านั้นเป็นข้อมูลตำแหน่ง เพื่อกระจายการประมวลผลที่จำเป็นต่อไป
4. การเคลื่อนไหวแบบอินเตอร์โพเลชัน
เนื่องจากเราหวังว่าจะแสดงประสิทธิภาพที่ทำงานพร้อมกันได้ 30-60 รายการ เราจึงต้องลดอัตราการส่งข้อมูลให้ต่ำลงกว่าเดิม ทีมศิลปะจากข้อมูลได้แก้ปัญหาเดียวกันนี้ในโปรเจ็กต์ Virtual Art Sessions ซึ่งจะเล่นไฟล์บันทึกวิดีโอของศิลปินที่วาดภาพใน VR โดยใช้ Tilt Brush โดยแก้ปัญหานี้ด้วยการทำให้ข้อมูลผู้ใช้เป็นเวอร์ชันกลางที่มีอัตราเฟรมต่ำลง และทำการประมาณระหว่างเฟรมขณะเล่นซ้ำ เราประหลาดใจที่ได้พบว่าแทบไม่เห็นความแตกต่างระหว่างวิดีโอที่บันทึกแบบปัดเศษที่ 15 FPS กับวิดีโอต้นฉบับที่บันทึกที่ 90 FPS
หากต้องการดูด้วยตนเอง คุณสามารถบังคับให้ Dance Tonite เล่นข้อมูลในอัตราต่างๆ ได้โดยใช้สตริงการค้นหา ?dataRate=
คุณสามารถใช้ฟีเจอร์นี้เพื่อเปรียบเทียบการเคลื่อนไหวที่บันทึกไว้ที่ 90 เฟรมต่อวินาที 45 เฟรมต่อวินาที หรือ 15 เฟรมต่อวินาที
สำหรับตำแหน่ง เราจะใช้การหาค่าเฉลี่ยเชิงเส้นระหว่างคีย์เฟรมก่อนหน้ากับคีย์เฟรมถัดไป โดยอิงตามช่วงเวลาระหว่างคีย์เฟรม (อัตราส่วน)
const { x: x1, y: y1, z: z1 } = getPosition(previous, performanceIndex, limbIndex);
const { x: x2, y: y2, z: z2 } = getPosition(next, performanceIndex, limbIndex);
interpolatedPosition = new THREE.Vector3();
interpolatedPosition.set(
x1 + (x2 - x1) * ratio,
y1 + (y2 - y1) * ratio,
z1 + (z2 - z1) * ratio
);
สําหรับการวางแนว เราจะใช้การหาค่าเฉลี่ยเชิงเส้นแบบทรงกลม (slerp) ระหว่างคีย์เฟรม ระบบจะจัดเก็บการวางแนวเป็น Quaternion
const quaternion = getQuaternion(previous, performanceIndex, limbIndex);
quaternion.slerp(
getQuaternion(next, performanceIndex, limbIndex),
ratio
);
5. การซิงค์การเคลื่อนไหวกับเพลง
เราจำเป็นต้องทราบเวลาปัจจุบันของเพลงแบบละเอียดถึงระดับมิลลิวินาทีเพื่อที่จะทราบว่าจะให้เล่นเฟรมใดของภาพเคลื่อนไหวที่บันทึกไว้ ปรากฏว่าแม้ว่าองค์ประกอบ HTML Audio จะเหมาะอย่างยิ่งสำหรับการโหลดและเล่นเสียงแบบเป็นขั้นเป็นตอน แต่พร็อพเพอร์ตี้เวลาที่มีให้จะไม่เปลี่ยนแปลงตามเฟรมที่วนซ้ำของเบราว์เซอร์ ข้อมูลจะช้ากว่าเสมอ บางครั้งช้ากว่าเพียงเศษเสี้ยวของมิลลิวินาที บางครั้งช้ากว่านานกว่านั้น
วิธีนี้ทำให้เกิดการกระตุกขณะบันทึกการเต้นที่สวยๆ ของเรา ซึ่งเราต้องการหลีกเลี่ยงค่าใช้จ่ายทั้งหมด เราจึงแก้ปัญหานี้ด้วยการใช้ตัวจับเวลาของเราเองใน JavaScript วิธีนี้ช่วยให้เรามั่นใจได้ว่าระยะเวลาที่เปลี่ยนแปลงระหว่างเฟรมคือระยะเวลาที่ผ่านไปนับตั้งแต่เฟรมสุดท้าย เมื่อใดก็ตามที่ตัวจับเวลาของเราไม่ตรงกับเพลงมากกว่า 10 มิลลิวินาที เราจะซิงค์ตัวจับเวลาอีกครั้ง
6. การตัดและหมอก
ทุกเรื่องราวต้องมีตอนจบที่ดี และเราอยากทำสิ่งที่น่าประหลาดใจให้กับผู้ใช้ที่มาถึงตอนจบของประสบการณ์การใช้งาน เมื่อออกจากห้องสุดท้ายแล้ว คุณจะได้พบกับภูมิทัศน์ที่เงียบสงบของกรวยและทรงกระบอก คุณอาจสงสัยว่า "นี่จะเป็นจุดจบไหม" เมื่อคุณเคลื่อนที่เข้าไปในสนาม โทนเสียงดนตรีก็ทำให้กลุ่มกรวยและทรงกระบอกต่างๆ ก่อตัวเป็นนักเต้น คุณพบว่าตัวเองอยู่ในงานปาร์ตี้ขนาดใหญ่ แล้วเสียงเพลงหยุดลงทันที แล้วทุกอย่างก็ร่วงหล่นลงมา
แม้ว่าผู้ชมจะรู้สึกดีกับฟีเจอร์นี้ แต่เราก็พบปัญหาด้านประสิทธิภาพบางอย่างที่ต้องแก้ไข อุปกรณ์ VR แบบห้องและอุปกรณ์เล่นเกมระดับสูงทำงานได้อย่างสมบูรณ์แบบสำหรับการแสดงเพิ่มเติมกว่า 40 ครั้งที่จำเป็นสำหรับฉากจบใหม่ แต่อัตราเฟรมในอุปกรณ์เคลื่อนที่บางรุ่นลดลงครึ่งหนึ่ง
เราได้นำหมอกมาใช้เพื่อรับมือกับปัญหานี้ หลังจากระยะหนึ่ง ทุกอย่างจะค่อยๆ เปลี่ยนเป็นสีดํา เนื่องจากเราไม่จำเป็นต้องคำนวณหรือวาดสิ่งที่มองไม่เห็น เราจึงตัดการแสดงผลในห้องที่มองไม่เห็น ซึ่งช่วยให้เราประหยัดทั้ง CPU และ GPU ได้ แต่ควรเลือกระยะห่างอย่างไร
อุปกรณ์บางเครื่องรองรับทุกสิ่งที่คุณโยนใส่ แต่อุปกรณ์บางเครื่องก็มีข้อจำกัดมากกว่า เราจึงเลือกใช้รูปแบบการปรับราคาตามรายได้ การวัดจํานวนเฟรมต่อวินาทีอย่างต่อเนื่องช่วยให้เราปรับระยะห่างของหมอกได้ตามความเหมาะสม ตราบใดที่อัตราเฟรมทำงานได้อย่างราบรื่น เราจะพยายามเพิ่มงานเรนเดอร์ด้วยการลดหมอก หากอัตราเฟรมทำงานไม่ราบรื่นพอ เราจะเพิ่มระดับหมอกให้ใกล้ขึ้นเพื่อข้ามการแสดงผลในที่มืด
// this is called every frame
// the FPS calculation is based on stats.js by @mrdoob
tick: (interval = 3000) => {
frames++;
const time = (performance || Date).now();
if (prevTime == null) prevTime = time;
if (time > prevTime + interval) {
fps = Math.round((frames * 1000) / (time - prevTime));
frames = 0;
prevTime = time;
const lastCullDistance = settings.cullDistance;
// if the fps is lower than 52 reduce the cull distance
if (fps <= 52) {
settings.cullDistance = Math.max(
settings.minCullDistance,
settings.cullDistance - settings.roomDepth
);
}
// if the FPS is higher than 56, increase the cull distance
else if (fps > 56) {
settings.cullDistance = Math.min(
settings.maxCullDistance,
settings.cullDistance + settings.roomDepth
);
}
}
// gradually increase the cull distance to the new setting
cullDistance = cullDistance * 0.95 + settings.cullDistance * 0.05;
// mask the edge of the cull distance with fog
viewer.fog.near = cullDistance - settings.roomDepth;
viewer.fog.far = cullDistance;
}
การสร้าง VR สำหรับเว็บเพื่อตอบสนองความต้องการที่หลากหลาย
การออกแบบและพัฒนาประสบการณ์การใช้งานแบบหลายแพลตฟอร์มที่ไม่เหมือนกันหมายความว่าต้องคำนึงถึงความต้องการของผู้ใช้แต่ละรายตามอุปกรณ์ที่ใช้ และในทุกการตัดสินใจด้านการออกแบบ เราจำเป็นต้องดูว่าการตัดสินใจนั้นอาจส่งผลต่อผู้ใช้รายอื่นๆ อย่างไร คุณจะแน่ใจได้อย่างไรว่าสิ่งที่เห็นใน VR จะน่าตื่นเต้นไม่แพ้กัน และเมื่อไม่ได้ใช้ VR และการขอวีซ่า
1. ลูกบอลสีเหลือง
ผู้ใช้ VR แบบห้องสเกลจะแสดงผลงาน แต่ผู้ใช้อุปกรณ์ VR บนอุปกรณ์เคลื่อนที่ (เช่น Cardboard, Daydream View หรือ Samsung Gear) จะใช้งานโปรเจ็กต์นี้อย่างไร สำหรับสิ่งนี้ เราได้แนะนำองค์ประกอบใหม่ ในสภาพแวดล้อมของเรา นั่นคือลูกโลกสีเหลือง
เมื่อดูโปรเจ็กต์ใน VR คุณจะดูจากมุมมองของลูกบอลสีเหลือง เมื่อคุณลอยตัวจากห้องหนึ่งไปยังอีกห้องหนึ่ง นักเต้นจะตอบสนองต่อการปรากฏตัวของคุณ ตัวละครจะโบกมือให้คุณ เต้นรอบๆ คุณ ทำท่าทางตลกๆ อยู่ข้างหลังคุณ และหลบทางอย่างรวดเร็วเพื่อไม่ให้ชนคุณ ลูกบอลสีเหลืองเป็นจุดสนใจเสมอ
สาเหตุคือขณะบันทึกการแสดง ลูกบอลสีเหลืองจะเคลื่อนไหวผ่านตรงกลางห้องตามจังหวะเพลงและวนกลับมาซ้ำ ตำแหน่งของลูกโลกทำให้ผู้แสดงแนวคิดได้ว่าตนอยู่ในจุดไหนแล้ว และใช้เวลานานเท่าใด เป็นจุดโฟกัสที่เป็นธรรมชาติ สำหรับพวกเขาในการสร้างประสิทธิภาพ
2. มุมมองอื่น
เราไม่อยากให้ผู้ใช้ที่ไม่มี VR รู้สึกถูกทอดทิ้ง โดยเฉพาะเมื่อผู้ใช้กลุ่มนี้น่าจะเป็นกลุ่มผู้ชมจำนวนมากที่สุดของเรา แทนที่จะสร้างประสบการณ์ VR จำลอง เราต้องการให้อุปกรณ์ ใช้งานบนหน้าจอเป็นของตัวเอง เราอยากแสดงประสิทธิภาพจากด้านบนในมุมมองภาพ Isometric มุมมองนี้มีประวัติอันยาวนานในเกมคอมพิวเตอร์ เกมนี้ใช้ครั้งแรกใน Zaxxon ซึ่งเป็นเกมยิงในอวกาศจากปี 1982 ขณะที่ผู้ใช้ VR จะรู้สึกทึ่ง แต่มุมมองภาพสามมิติจะให้มุมมองที่เหมือนกับเทพเจ้าในการเคลื่อนไหว เราเลือกที่จะปรับขนาดโมเดลให้ใหญ่ขึ้นเล็กน้อยเพื่อให้ดูมีสไตล์แบบบ้านตุ๊กตา
3. เงา: แต่งตัวให้เหมือนโกหก
เราพบว่าผู้ใช้บางรายประสบปัญหาในการมองเห็นความลึกในมุมมองภาพ Isometric เราค่อนข้างมั่นใจว่าด้วยเหตุนี้ Zaxxon จึงเป็นหนึ่งในเกมคอมพิวเตอร์เกมแรกๆ ในประวัติศาสตร์ที่ฉายเงาแบบไดนามิกใต้วัตถุที่บินได้
แต่การสร้างเงาในแบบ 3 มิตินั้นยาก โดยเฉพาะอย่างยิ่งสำหรับอุปกรณ์ที่มีพื้นที่จำกัด เช่น โทรศัพท์มือถือ ตอนแรกเราต้องตัดสินใจอย่างยากลำบากที่จะตัดฟีเจอร์นี้ออก แต่หลังจากขอคำแนะนำจากผู้เขียน Three.js และMr doob ซึ่งเป็นแฮ็กเกอร์เดโมที่มีประสบการณ์ เขาก็ได้ไอเดียแปลกใหม่ขึ้นมาว่า… ให้ใช้ฟีเจอร์จำลอง
แทนที่จะต้องคำนวณว่าวัตถุที่ลอยอยู่แต่ละวัตถุบดบังแสงของเราอย่างไร เราจึงวาดเงาของรูปทรงต่างๆ กัน เราวาดรูปพื้นผิวเบลอเป็นวงกลมวงเดียวกันไว้ใต้วัตถุเหล่านั้น เนื่องจากภาพของเราไม่ได้พยายามเลียนแบบความเป็นจริงตั้งแต่แรก เราจึงพบว่าสามารถแก้ไขให้ผ่านเกณฑ์ได้โดยง่ายด้วยการปรับเปลี่ยนเพียงไม่กี่อย่าง เมื่อวัตถุเข้าใกล้พื้นมากขึ้น เราจะทำให้พื้นผิวมีสีเข้มขึ้นและเล็กลง เมื่อผู้ใช้เลื่อนขึ้น เราจะทําให้พื้นผิวโปร่งใสขึ้นและใหญ่ขึ้น
เราสร้างขึ้นมาโดยใช้พื้นผิวนี้โดยไล่ระดับสีขาวที่นุ่มนวลไปจนถึงสีดำ (ไม่มีความโปร่งใสของอัลฟ่า) เราตั้งค่าวัสดุเป็นโปร่งใสและใช้การผสมแบบลบ ซึ่งจะช่วยให้ภาพผสมผสานกันได้ดีเมื่อซ้อนทับกัน
function createShadow() {
const texture = new THREE.TextureLoader().load(shadowTextureUrl);
const material = new THREE.MeshLambertMaterial({
map: texture,
transparent: true,
side: THREE.BackSide,
depthWrite: false,
blending: THREE.SubtractiveBlending,
});
const geometry = new THREE.PlaneBufferGeometry(0.5, 0.5, 1, 1);
const plane = new THREE.Mesh(geometry, material);
return plane;
}
4. การเข้าร่วม
เมื่อคลิกที่หัวของผู้แสดง ผู้เข้าชมที่ไม่มี VR จะดูสิ่งต่างๆ จากมุมมองของผู้เต้นได้ มุมนี้ทำให้เห็นรายละเอียดเล็กๆ น้อยๆ จำนวนมาก นักเต้นมองหน้ากันอย่างรวดเร็วเพื่อพยายามทำการแสดงให้สอดคล้องกัน เมื่อลูกบอลเข้ามาในห้อง คุณจะเห็นพวกเขามองไปที่ลูกบอลอย่างกังวล แม้ว่าในฐานะผู้ชม คุณจะไม่สามารถควบคุมการเคลื่อนไหวเหล่านี้ได้ แต่ฟีเจอร์นี้ก็สื่อให้เห็นถึงความรู้สึกสมจริงได้อย่างน่าทึ่ง เราขอย้ำอีกครั้งว่าวิธีนี้ดีกว่าการนำเสนอเวอร์ชัน VR จําลองที่ควบคุมด้วยเมาส์ซึ่งดูจืดชืด
5. การแชร์ไฟล์บันทึกเสียง
เราทราบดีว่าคุณภูมิใจเพียงใดเมื่อถ่ายทำวิดีโอที่มีการออกแบบท่าเต้นอย่างประณีตและบันทึกการแสดงของนักแสดง 20 ชั้นที่โต้ตอบกัน เรารู้ว่าผู้ใช้ของเรา น่าจะอยากดูรูปนี้ให้เพื่อนดู แต่ภาพนิ่งของการแสดงนี้สื่อไม่ชัดเจนพอ แต่เราต้องการอนุญาตให้ผู้ใช้แชร์วิดีโอการแสดงของตนแทน จริงๆ แล้วทำไมต้องเป็น GIF ภาพเคลื่อนไหวของเรามีแรเงาแบบแบนราบ ซึ่งเหมาะ กับชุดสีที่มีรูปแบบจำกัด
เราหันมาใช้ GIF.js ซึ่งเป็นไลบรารี JavaScript ที่ให้คุณเข้ารหัส GIF แบบเคลื่อนไหวได้จากในเบราว์เซอร์ โดยจะลดภาระการแปลงไฟล์เฟรมไปให้Web Worker ซึ่งสามารถทำงานในเบื้องหลังเป็นกระบวนการแยกต่างหาก จึงใช้ประโยชน์จากโปรเซสเซอร์หลายตัวที่ทำงานร่วมกันได้
ขออภัย กระบวนการเข้ารหัสยังคงช้าเกินไปสำหรับจำนวนเฟรมที่เราต้องใช้สำหรับภาพเคลื่อนไหว GIF สามารถสร้างไฟล์ขนาดเล็กได้โดยใช้ชุดสีที่จํากัด เราพบว่าเราใช้เวลาส่วนใหญ่ไปกับการค้นหาสี ที่ใกล้เคียงที่สุดของแต่ละพิกเซล เราเพิ่มประสิทธิภาพกระบวนการนี้ได้ 10 เท่าด้วยการแฮ็กทางลัดเล็กๆ น้อยๆ เช่น หากสีของพิกเซลเหมือนกับพิกเซลก่อนหน้า ให้ใช้สีเดียวกันจากจานสีเหมือนที่ผ่านมา
ตอนนี้เรามีการเข้ารหัสอย่างรวดเร็ว แต่ไฟล์ GIF ที่ได้นั้นมีขนาดใหญ่เกินไป รูปแบบ GIF ช่วยให้คุณระบุได้ว่าจะแสดงแต่ละเฟรมอย่างไรที่ด้านบนของเฟรมสุดท้าย โดยกำหนดวิธีการกำจัดของเฟรมนั้น เราจะอัปเดตเฉพาะพิกเซลที่มีการเปลี่ยนแปลงเท่านั้น แทนที่จะอัปเดตพิกเซลแต่ละพิกเซลในแต่ละเฟรม เพื่อให้ไฟล์มีขนาดเล็กลง แม้ว่าจะทําให้ขั้นตอนการเข้ารหัสช้าลงอีก แต่วิธีนี้ช่วยลดขนาดไฟล์ได้อย่างมาก
6. ฐานที่มั่นคง: Google Cloud และ Firebase
แบ็กเอนด์ของเว็บไซต์ "เนื้อหาที่ผู้ใช้สร้างขึ้น" มักมีความซับซ้อนและเปราะบาง แต่เราสร้างระบบที่เรียบง่ายและมีประสิทธิภาพได้โดยใช้ Google Cloud และ Firebase เมื่อผู้แสดงอัปโหลดการเต้นใหม่ลงในระบบ ระบบจะตรวจสอบสิทธิ์แบบไม่ระบุตัวตนโดยการตรวจสอบสิทธิ์ Firebase โดยจะได้รับสิทธิ์อัปโหลดไฟล์บันทึกเสียงไปยังพื้นที่ทำงานชั่วคราวโดยใช้ Cloud Storage สำหรับ Firebase เมื่อการอัปโหลดเสร็จสมบูรณ์แล้ว เครื่องไคลเอ็นต์จะเรียกใช้ทริกเกอร์ HTTP ของ Cloud Functions for Firebase โดยใช้โทเค็น Firebase ซึ่งจะทริกเกอร์กระบวนการของเซิร์ฟเวอร์ที่จะตรวจสอบข้อมูลที่ส่ง สร้างระเบียนฐานข้อมูล และย้ายไฟล์บันทึกเสียงไปยังไดเรกทอรีสาธารณะใน Google Cloud Storage
เนื้อหาสาธารณะทั้งหมดของเราจัดเก็บไว้ในชุดไฟล์เดี่ยวในที่เก็บข้อมูล Cloud Storage ซึ่งหมายความว่าข้อมูลของเราเข้าถึงได้อย่างรวดเร็วทั่วโลก และเราไม่ต้องกังวลว่าปริมาณการเข้าถึงที่สูงจะส่งผลต่อความพร้อมใช้งานของข้อมูลไม่ว่าในทางใดก็ตาม
เราใช้ Firebase Realtime Database และปลายทาง Cloud Functions เพื่อสร้างเครื่องมือการดูแล/การดูแลจัดการที่เรียบง่ายซึ่งช่วยให้เราดูเนื้อหาใหม่ที่ส่งเข้ามาแต่ละรายการใน VR และเผยแพร่เพลย์ลิสต์ใหม่จากอุปกรณ์ใดก็ได้
7. Service Worker
Service Worker เป็นนวัตกรรมที่เพิ่งเกิดขึ้นซึ่งช่วยจัดการการแคชเนื้อหาของเว็บไซต์ ในกรณีของเรา โปรแกรมทำงานของบริการโหลดเนื้อหาของเราอย่างรวดเร็วสำหรับผู้เข้าชมที่กลับมา และยังทำให้เว็บไซต์ทำงานแบบออฟไลน์ได้ด้วย ฟีเจอร์เหล่านี้เป็นฟีเจอร์ที่สำคัญเนื่องจากผู้เข้าชมจำนวนมากจะใช้การเชื่อมต่อบนอุปกรณ์เคลื่อนที่ในคุณภาพที่หลากหลาย
การเพิ่ม Service Worker ลงในโปรเจ็กต์นั้นง่ายมากด้วยปลั๊กอิน webpack ที่มีประโยชน์ซึ่งจัดการงานหนักส่วนใหญ่ให้คุณ ในการกำหนดค่าด้านล่างนี้ เราสร้าง Service Worker ที่จะแคชไฟล์คงที่ทั้งหมดโดยอัตโนมัติ โดยจะดึงไฟล์เพลย์ลิสต์ล่าสุดจากเครือข่าย (หากมี) เนื่องจากเพลย์ลิสต์จะอัปเดตอยู่ตลอดเวลา คุณควรดึงไฟล์ JSON ที่บันทึกทั้งหมดจากแคช หากมี เนื่องจากไฟล์เหล่านี้จะไม่เปลี่ยนแปลง
const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin');
config.plugins.push(
new SWPrecacheWebpackPlugin({
dontCacheBustUrlsMatching: /\.\w{8}\./,
filename: 'service-worker.js',
minify: true,
navigateFallback: 'index.html',
staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/],
runtimeCaching: [{
urlPattern: /playlist\.json$/,
handler: 'networkFirst',
}, {
urlPattern: /\/recordings\//,
handler: 'cacheFirst',
options: {
cache: {
maxEntries: 120,
name: 'recordings',
},
},
}],
})
);
ปัจจุบันปลั๊กอินไม่สามารถจัดการชิ้นงานสื่อที่โหลดแบบเป็นขั้นเป็นตอน เช่น ไฟล์เพลงของเรา เราจึงแก้ปัญหานี้โดยการตั้งค่าส่วนหัว Cache-Control
ของ Cloud Storage ในไฟล์เหล่านี้เป็น public, max-age=31536000
เพื่อให้เบราว์เซอร์แคชไฟล์ได้นานถึง 1 ปี
บทสรุป
เราตื่นเต้นที่จะได้เห็นว่านักแสดงจะใส่ประสบการณ์นี้ได้อย่างไรบ้างและใช้เป็นเครื่องมือสำหรับ การแสดงออกอย่างสร้างสรรค์โดยใช้การเคลื่อนไหว เราได้เผยแพร่โค้ดแบบโอเพนซอร์สทั้งหมด ซึ่งดูได้ที่ https://github.com/puckey/dance-tonite ในช่วงเริ่มต้นของ VR และ WebVR เราหวังว่าจะได้เห็นว่าสื่อรูปแบบใหม่นี้จะพัฒนาไปในทิศทางใหม่ๆ ที่สร้างสรรค์และคาดไม่ถึงอย่างไร เต้นต่อ