บทนำ
"Find Your Way to Oz" เป็นเวอร์ชันเว็บของ Google Chrome Experiments ใหม่จาก Disney ซึ่งจะช่วยให้คุณได้ผจญภัยแบบอินเทอร์แอกทีฟผ่านคณะละครสัตว์ในรัฐแคนซัส ซึ่งจะนำคุณไปยังดินแดนออซหลังจากที่พายุใหญ่พัดคุณไป
เป้าหมายของเราคือการผสมผสานความสมบูรณ์แบบของภาพยนตร์เข้ากับความสามารถทางเทคนิคของเบราว์เซอร์เพื่อสร้างประสบการณ์การใช้งานที่สนุกสนานและสมจริงซึ่งผู้ใช้จะรู้สึกเชื่อมโยงกับเนื้อหาได้
งานนี้มีขนาดใหญ่เกินกว่าที่จะอธิบายได้ทั้งหมดในบทความนี้ เราจึงเจาะลึกและดึงบทบางส่วนของเรื่องราวเทคโนโลยีที่เราคิดว่าน่าสนใจมาเล่าให้ฟัง ในระหว่างนี้ เราได้แยกบทแนะนําที่เน้นหัวข้อต่างๆ ที่มีความยากขึ้นเรื่อยๆ
เรามีทีมงานจำนวนมากที่ทุ่มเททำงานเพื่อให้ประสบการณ์นี้เกิดขึ้นได้ มากจนไม่สามารถระบุรายชื่อได้ทั้งหมด โปรดไปที่เว็บไซต์เพื่อดูหน้าเครดิตในส่วนเมนูเพื่ออ่านเรื่องราวฉบับเต็ม
ส่องเบื้องหลังการทำงาน
เกมหาทางไปออซบนเดสก์ท็อปเป็นโลกที่สมจริงและน่าดึงดูด เราใช้เอฟเฟกต์ 3 มิติและเอฟเฟกต์ที่ได้รับแรงบันดาลใจจากภาพยนตร์แบบดั้งเดิมหลายเลเยอร์ซึ่งรวมกันเพื่อสร้างฉากที่ดูสมจริง เทคโนโลยีที่โดดเด่นที่สุดคือ WebGL ที่มี Three.js, ชิเดอร์ที่สร้างขึ้นเอง และองค์ประกอบ DOM แบบเคลื่อนไหวโดยใช้ฟีเจอร์ CSS3 นอกจากนี้ getUserMedia API (WebRTC) สำหรับประสบการณ์แบบอินเทอร์แอกทีฟยังช่วยให้ผู้ใช้เพิ่มรูปภาพจากเว็บแคมได้โดยตรง รวมถึงใช้ WebAudio เพื่อเสียง 3 มิติ
แต่ความมหัศจรรย์ของประสบการณ์เทคโนโลยีเช่นนี้คือการผสมผสานองค์ประกอบต่างๆ เข้าด้วยกัน ซึ่งนี่เป็นหนึ่งในความท้าทายหลักด้วยเช่นกัน นั่นคือการผสมผสานเอฟเฟกต์ภาพและองค์ประกอบแบบอินเทอร์แอกทีฟเข้าด้วยกันในฉากเดียวเพื่อสร้างภาพรวมที่สอดคล้องกัน ความซับซ้อนของภาพนี้ทำให้จัดการได้ยาก ทำให้บอกได้ยากว่าเรากำลังอยู่ในขั้นตอนใดของการพัฒนา
ในการแก้ปัญหาเกี่ยวกับเอฟเฟกต์ภาพและการเพิ่มประสิทธิภาพที่เชื่อมโยงกัน เราใช้แผงควบคุมอย่างหนักเพื่อบันทึกการตั้งค่าที่เกี่ยวข้องทั้งหมดที่เรากำลังตรวจสอบ ณ ขณะนั้น ฉากสามารถปรับแบบเรียลไทม์ในเบราว์เซอร์ได้ ตั้งแต่ความสว่าง ระยะชัดลึก แกมมา ฯลฯ ทุกคนสามารถลองปรับค่าพารามิเตอร์ที่สำคัญในประสบการณ์การใช้งานและมีส่วนร่วมในการค้นหาสิ่งที่ได้ผลดีที่สุด
ก่อนจะแชร์เคล็ดลับ เราขอเตือนคุณว่าอุปกรณ์อาจขัดข้อง เช่นเดียวกับการที่คุณไปซนในเครื่องยนต์รถยนต์ ตรวจสอบว่าคุณไม่ได้เปิดสิ่งสําคัญใดๆ ไว้ แล้วไปที่ URL หลักของเว็บไซต์ แล้วเพิ่ม ?debug=on ต่อท้ายที่อยู่ รอให้เว็บไซต์โหลด และเมื่อคุณเข้าไปแล้ว ให้กดแป้น Ctrl-I
แล้วคุณจะเห็นเมนูแบบเลื่อนลงปรากฏขึ้นทางด้านขวามือ หากยกเลิกการเลือกตัวเลือก "ออกจากเส้นทางของกล้อง" คุณจะใช้แป้น A, W, S, D และเม้าส์เพื่อไปยังส่วนต่างๆ ของพื้นที่ได้อย่างอิสระ

เราจะไม่อธิบายการตั้งค่าทั้งหมดที่นี่ แต่ขอแนะนำให้คุณลองใช้คีย์ต่างๆ เพื่อดูการตั้งค่าที่แตกต่างกันในฉากต่างๆ ในลำดับพายุสุดท้ายจะมีคีย์เพิ่มเติม Ctrl-A
ซึ่งคุณสามารถสลับการเล่นภาพเคลื่อนไหวและบินไปรอบๆ ได้ ในฉากนี้ หากคุณกด Esc
(เพื่อออกจากฟังก์ชันการล็อกเมาส์) แล้วกด Ctrl-I
อีกครั้ง คุณจะเข้าถึงการตั้งค่าสำหรับฉากพายุโดยเฉพาะได้ มองไปรอบๆ และถ่ายภาพวิวสวยๆ สักภาพสองภาพ เช่นภาพด้านล่าง

เราใช้ไลบรารีที่ยอดเยี่ยมชื่อ dat.gui (ดูที่นี่สำหรับบทแนะนำที่ผ่านมาเกี่ยวกับวิธีใช้) เพื่อให้การดำเนินการนี้เกิดขึ้นและมีความยืดหยุ่นเพียงพอต่อความต้องการของเรา ซึ่งช่วยให้เราเปลี่ยนการตั้งค่าที่แสดงต่อผู้เข้าชมเว็บไซต์ได้อย่างรวดเร็ว
คล้ายกับภาพพื้นหลัง
ในภาพยนตร์และภาพเคลื่อนไหวคลาสสิกหลายเรื่องจากดิสนีย์ การสร้างฉากหมายถึงการรวมเลเยอร์ต่างๆ มีทั้งเลเยอร์ของภาพคนแสดง ภาพเคลื่อนไหวแบบเซล ฉากจริง และเลเยอร์ด้านบนที่สร้างโดยการวาดภาพบนกระจก ซึ่งเป็นเทคนิคที่เรียกว่า "ภาพพื้นหลัง"
โครงสร้างของประสบการณ์ที่เราสร้างขึ้นมีความคล้ายคลึงกันหลายประการ แม้ว่า "เลเยอร์" บางรายการจะไม่ใช่ภาพนิ่ง แต่จริงๆ แล้ว การตั้งค่าเหล่านี้ส่งผลต่อลักษณะที่ปรากฏตามการคำนวณที่ซับซ้อนมากขึ้น อย่างไรก็ตาม อย่างน้อยที่สุดในระดับภาพรวม เรากำลังจัดการกับยอดดูที่ซ้อนทับกัน คุณจะเห็นเลเยอร์ UI ที่ด้านบน โดยมีฉาก 3 มิติอยู่ด้านล่าง ซึ่งประกอบไปด้วยคอมโพเนนต์ฉากต่างๆ
เลเยอร์อินเทอร์เฟซด้านบนสร้างขึ้นโดยใช้ DOM และ CSS 3 ซึ่งหมายความว่าการแก้ไขการโต้ตอบสามารถทำได้หลายวิธีโดยอิสระจากประสบการณ์ 3 มิติด้วยการสื่อสารระหว่าง 2 เลเยอร์ตามรายการเหตุการณ์ที่เลือก การสื่อสารนี้ใช้ Backbone Router + เหตุการณ์ onHashChange ของ HTML5 ซึ่งควบคุมว่าควรแสดงภาพเคลื่อนไหวของส่วนใด (ซอร์สโค้ดของโปรเจ็กต์: /develop/coffee/router/Router.coffee)
บทแนะนำ: การรองรับ Sprite Sheet และ Retina
เทคนิคการเพิ่มประสิทธิภาพที่น่าสนใจอย่างหนึ่งที่เราใช้กับอินเทอร์เฟซคือการรวมรูปภาพการวางซ้อนอินเทอร์เฟซจำนวนมากไว้ในไฟล์ PNG ไฟล์เดียวเพื่อลดคําขอเซิร์ฟเวอร์ ในโปรเจ็กต์นี้ อินเทอร์เฟซประกอบด้วยรูปภาพกว่า 70 ภาพ (ไม่รวมพื้นผิว 3 มิติ) ที่โหลดทั้งหมดล่วงหน้าเพื่อลดเวลาในการตอบสนองของเว็บไซต์ คุณดูชีตสไปรท์เวอร์ชันที่ใช้อยู่ได้ที่นี่
จอแสดงผลปกติ - http://findyourwaytooz.com/img/home/interface_1x.png จอแสดงผล Retina - http://findyourwaytooz.com/img/home/interface_2x.png
ต่อไปนี้เป็นเคล็ดลับบางส่วนเกี่ยวกับวิธีที่เราใช้ประโยชน์จาก Sprite Sheet และวิธีใช้ Sprite Sheet สำหรับอุปกรณ์ Retina เพื่อให้อินเทอร์เฟซคมชัดและเรียบร้อยมากที่สุด
การสร้าง Spritesheet
เราใช้ TexturePacker เพื่อสร้าง SpriteSheet ซึ่งจะแสดงผลในรูปแบบใดก็ได้ตามต้องการ ในกรณีนี้เราได้ส่งออกเป็น EaselJS ซึ่งเรียบร้อยมากและอาจใช้สร้างสไปรท์ที่เคลื่อนไหวได้ด้วย
การใช้สไปรท์ชีตที่สร้างขึ้น
เมื่อสร้าง Sprite Sheet แล้ว คุณควรเห็นไฟล์ JSON ดังต่อไปนี้
{
"images": ["interface_2x.png"],
"frames": [
[2, 1837, 88, 130],
[2, 2, 1472, 112],
[1008, 774, 70, 68],
[562, 1960, 86, 86],
[473, 1960, 86, 86]
],
"animations": {
"allow_web":[0],
"bottomheader":[1],
"button_close":[2],
"button_facebook":[3],
"button_google":[4]
},
}
สถานที่:
- image หมายถึง URL ของสไปรท์ชีต
- frames คือพิกัดของเอลิเมนต์ UI แต่ละรายการ [x, y, width, height]
- ภาพเคลื่อนไหวคือชื่อของชิ้นงานแต่ละรายการ
โปรดทราบว่าเราใช้รูปภาพที่มีความหนาแน่นสูงเพื่อสร้างภาพต่อเรียง จากนั้นจึงสร้างเวอร์ชันปกติโดยปรับขนาดให้เหลือครึ่งหนึ่งของขนาดเดิม
สรุป
ตอนนี้เราพร้อมแล้ว เพียงต้องมีข้อมูลโค้ด JavaScript เพื่อใช้งาน
var SSAsset = function (asset, div) {
var css, x, y, w, h;
// Divide the coordinates by 2 as retina devices have 2x density
x = Math.round(asset.x / 2);
y = Math.round(asset.y / 2);
w = Math.round(asset.width / 2);
h = Math.round(asset.height / 2);
// Create an Object to store CSS attributes
css = {
width : w,
height : h,
'background-image' : "url(" + asset.image_1x_url + ")",
'background-size' : "" + asset.fullSize[0] + "px " + asset.fullSize[1] + "px",
'background-position': "-" + x + "px -" + y + "px"
};
// If retina devices
if (window.devicePixelRatio === 2) {
/*
set -webkit-image-set
for 1x and 2x
All the calculations of X, Y, WIDTH and HEIGHT is taken care by the browser
*/
css['background-image'] = "-webkit-image-set(url(" + asset.image_1x_url + ") 1x,";
css['background-image'] += "url(" + asset.image_2x_url + ") 2x)";
}
// Set the CSS to the DIV
div.css(css);
};
วิธีใช้มีดังนี้
logo = new SSAsset(
{
fullSize : [1024, 1024], // image 1x dimensions Array [x,y]
x : 1790, // asset x coordinate on SpriteSheet
y : 603, // asset y coordinate on SpriteSheet
width : 122, // asset width
height : 150, // asset height
image_1x_url : 'img/spritesheet_1x.png', // background image 1x URL
image_2x_url : 'img/spritesheet_2x.png' // background image 2x URL
},$('#logo'));
หากต้องการทำความเข้าใจเพิ่มเติมเกี่ยวกับความหนาแน่นของพิกเซลแบบปรับได้ โปรดอ่านบทความนี้โดย Boris Smus
ไปป์ไลน์เนื้อหา 3 มิติ
ประสบการณ์การใช้งานสภาพแวดล้อมได้รับการตั้งค่าในเลเยอร์ WebGL เมื่อพูดถึงฉาก 3 มิติ คำถามที่ยากที่สุดอย่างหนึ่งคือคุณจะแน่ใจได้อย่างไรว่าสร้างเนื้อหาที่แสดงออกได้สูงสุดจากด้านโมเดล อนิเมชัน และเอฟเฟกต์ ปัญหานี้เกี่ยวข้องกับไปป์ไลน์เนื้อหา ซึ่งเป็นกระบวนการที่ตกลงกันไว้สำหรับสร้างเนื้อหาสำหรับฉาก 3 มิติ
เราต้องการสร้างโลกที่น่าตื่นตาตื่นใจ จึงต้องมีขั้นตอนที่มั่นคงซึ่งจะช่วยให้ศิลปิน 3 มิติสร้างโลกดังกล่าวได้ ศิลปินจะต้องได้รับอิสระในการแสดงออกมากที่สุดในซอฟต์แวร์การสร้างโมเดล 3 มิติและภาพเคลื่อนไหว และเราจะต้องแสดงผลบนหน้าจอผ่านโค้ด
เราพยายามแก้ปัญหาประเภทนี้มาระยะหนึ่งแล้ว เนื่องจากทุกครั้งที่เราสร้างเว็บไซต์ 3 มิติในอดีต เราพบข้อจำกัดในเครื่องมือที่ใช้ได้ เราจึงสร้างเครื่องมือนี้ขึ้นมาชื่อ 3D Librarian ซึ่งเป็นเครื่องมือวิจัยภายใน และพร้อมที่จะนำไปใช้กับงานจริงแล้ว
เครื่องมือนี้มีประวัติความเป็นมา โดยเดิมทีมีไว้สำหรับ Flash และช่วยให้คุณนําฉาก Maya ขนาดใหญ่เข้ามาเป็นไฟล์บีบอัดไฟล์เดียวที่ได้รับการเพิ่มประสิทธิภาพเพื่อแกะไฟล์รันไทม์ เหตุผลที่มีประสิทธิภาพสูงสุดคือสามารถแพ็กฉากได้อย่างมีประสิทธิภาพในโครงสร้างข้อมูลเดียวกันกับที่ใช้จัดการระหว่างการแสดงผลและภาพเคลื่อนไหว ไฟล์ต้องแยกวิเคราะห์เพียงเล็กน้อยเมื่อโหลด การแตกไฟล์ใน Flash นั้นรวดเร็วมากเนื่องจากไฟล์อยู่ในรูปแบบ AMF ซึ่ง Flash แตกไฟล์ได้โดยตรง การใช้รูปแบบเดียวกันใน WebGL จะต้องใช้ CPU มากขึ้นเล็กน้อย เราจำเป็นต้องสร้างเลเยอร์โค้ด JavaScript สำหรับการแยกข้อมูลอีกครั้ง ซึ่งจะทำหน้าที่หลักในการแตกไฟล์เหล่านั้นและสร้างโครงสร้างข้อมูลที่จําเป็นสําหรับให้ WebGL ทํางาน การแตกไฟล์ฉาก 3 มิติทั้งฉากเป็นการดำเนินการที่ต้องใช้ CPU ในระดับปานกลาง การแตกไฟล์ฉากที่ 1 ใน Find Your Way To Oz ใช้เวลาประมาณ 2 วินาทีบนเครื่องระดับกลางถึงระดับสูง ดังนั้นจึงทําโดยใช้เทคโนโลยี Web Workers ในเวลา "การตั้งค่าฉาก" (ก่อนที่ฉากจะเปิดขึ้นจริง) เพื่อไม่ให้ประสบการณ์ของผู้ใช้หยุดชะงัก
เครื่องมือที่มีประโยชน์นี้สามารถนําเข้าฉาก 3 มิติส่วนใหญ่ เช่น โมเดล พื้นผิว ภาพเคลื่อนไหวของกระดูก คุณสร้างไฟล์ไลบรารีไฟล์เดียว จากนั้นเครื่องมือ 3 มิติจะโหลดไฟล์ได้ คุณสามารถใส่โมเดลทั้งหมดที่ต้องการไว้ในฉากภายในคลังนี้ แล้ววางโมเดลเหล่านั้นในฉาก
แต่ปัญหาที่เราพบคือตอนนี้เรากำลังจัดการกับ WebGL ซึ่งเป็นเทคโนโลยีใหม่ นี่เป็นโปรเจ็กต์ที่ยากมาก เพราะเป็นการกำหนดมาตรฐานสำหรับประสบการณ์ 3 มิติบนเบราว์เซอร์ เราจึงสร้างเลเยอร์ JavaScript เฉพาะกิจที่จะนำไฟล์ฉาก 3 มิติที่บีบอัดโดย 3D Librarian และแปลไฟล์เหล่านั้นเป็นรูปแบบที่ WebGL จะเข้าใจอย่างถูกต้อง
บทแนะนำ: Let There Be Wind
ธีมที่ปรากฏซ้ำๆ ใน "Find Your Way To Oz" คือลม เค้าโครงของเนื้อเรื่องมีโครงสร้างให้เสียงลมดังขึ้นเรื่อยๆ
ฉากแรกของงานรื่นเริงค่อนข้างสงบ และขณะดูฉากต่างๆ ผู้ใช้จะสัมผัสได้ว่าลมแรงขึ้นเรื่อยๆ จนมาถึงฉากสุดท้ายซึ่งเป็นพายุ
ดังนั้น การสร้างเอฟเฟกต์เสียงลมที่สมจริงจึงเป็นเรื่องสำคัญ
ในการสร้างภาพนี้ เราใส่วัตถุที่อ่อนนุ่มลงในฉากงานคาร์นิวัล 3 ฉาก ซึ่งควรจะได้รับผลกระทบจากลม เช่น เต็นท์ พื้นผิวของตู้ถ่าย และบอลลูน

เกมบนเดสก์ท็อปในปัจจุบันมักจะสร้างขึ้นจากเอ็นจิ้นฟิสิกส์หลัก ดังนั้นเมื่อต้องจำลองวัตถุอ่อนในโลก 3 มิติ ระบบจะเรียกใช้การจำลองฟิสิกส์อย่างเต็มรูปแบบเพื่อสร้างลักษณะการทํางานของวัตถุอ่อนที่สมจริง
ใน WebGL / JavaScript เรายังไม่มีความหรูหราที่จะเรียกใช้การจำลองฟิสิกส์อย่างเต็มรูปแบบ เราจึงต้องหาวิธีสร้างเอฟเฟกต์ลมในออสเตรเลียโดยไม่ต้องจำลองลมจริงๆ
เราได้ฝังข้อมูล "ความไวต่อลม" ของวัตถุแต่ละชิ้นไว้ในโมเดล 3 มิติ แต่ละจุดยอดของโมเดล 3 มิติมี "แอตทริบิวต์ลม" ที่ระบุระดับที่จุดยอดนั้นควรได้รับผลกระทบจากลม ดังนั้น นี่เป็นค่าความไวต่อลมที่ระบุสำหรับวัตถุ 3 มิติ จากนั้นเราต้องสร้างลม
ซึ่งเราทําได้โดยการสร้างรูปภาพที่มีPerlin Noise รูปภาพนี้มีไว้เพื่อครอบคลุม "พื้นที่ลม" บางพื้นที่ ดังนั้นวิธีที่ดีในการคิดถึงเรื่องนี้คือจินตนาการถึงรูปภาพเมฆที่เหมือนเสียงรบกวนวางซ้อนกันเหนือพื้นที่สี่เหลี่ยมผืนผ้าบางพื้นที่ของฉาก 3 มิติ พิกเซลแต่ละพิกเซลซึ่งเป็นค่าระดับสีเทาของรูปภาพนี้ระบุความแรงของลม ณ ขณะหนึ่งในพื้นที่ 3 มิติ "รอบๆ นั้น"
ระบบจะสร้างเอฟเฟกต์ลมโดยย้ายรูปภาพตามเวลาด้วยความเร็วคงที่ในทิศทางหนึ่งๆ ซึ่งเป็นทิศทางของลม และเพื่อให้แน่ใจว่า "พื้นที่ที่มีลมแรง" จะไม่ส่งผลต่อทุกสิ่งในฉาก เราจึงตัดรูปภาพลมให้อยู่รอบๆ ขอบโดยจำกัดให้อยู่ในบริเวณที่มีเอฟเฟกต์
บทแนะนำง่ายๆ เกี่ยวกับลม 3 มิติ
ตอนนี้มาสร้างเอฟเฟกต์ลมในฉาก 3 มิติง่ายๆ ใน Three.js กัน
เราจะสร้างลมใน "ทุ่งหญ้าแบบเป็นขั้นตอน" ง่ายๆ
มาสร้างฉากกันก่อน เราจะสร้างภูมิประเทศแบบเรียบง่ายที่มีพื้นผิว จากนั้นหญ้าแต่ละเส้นจะแสดงด้วยกรวย 3 มิติที่คว่ำลง

วิธีสร้างฉากง่ายๆ นี้ใน Three.js โดยใช้ CoffeeScript มีดังนี้
ก่อนอื่นเราจะตั้งค่า Three.js และเชื่อมต่อกับกล้อง ตัวควบคุมเมาส์ และแสงต่างๆ ดังนี้
constructor: ->
@clock = new THREE.Clock()
@container = document.createElement( 'div' );
document.body.appendChild( @container );
@renderer = new THREE.WebGLRenderer();
@renderer.setSize( window.innerWidth, window.innerHeight );
@renderer.setClearColorHex( 0x808080, 1 )
@container.appendChild(@renderer.domElement);
@camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 1, 5000 );
@camera.position.x = 5;
@camera.position.y = 10;
@camera.position.z = 40;
@controls = new THREE.OrbitControls( @camera, @renderer.domElement );
@controls.enabled = true
@scene = new THREE.Scene();
@scene.add( new THREE.AmbientLight 0xFFFFFF )
directional = new THREE.DirectionalLight 0xFFFFFF
directional.position.set( 10,10,10)
@scene.add( directional )
# Demo data
@grassTex = THREE.ImageUtils.loadTexture("textures/grass.png");
@initGrass()
@initTerrain()
# Stats
@stats = new Stats();
@stats.domElement.style.position = 'absolute';
@stats.domElement.style.top = '0px';
@container.appendChild( @stats.domElement );
window.addEventListener( 'resize', @onWindowResize, false );
@animate()
การเรียกใช้ฟังก์ชัน initGrass และ initTerrain จะสร้างหญ้าและภูมิประเทศในฉากตามลำดับ
initGrass:->
mat = new THREE.MeshPhongMaterial( { map: @grassTex } )
NUM = 15
for i in [0..NUM] by 1
for j in [0..NUM] by 1
x = ((i/NUM) - 0.5) * 50 + THREE.Math.randFloat(-1,1)
y = ((j/NUM) - 0.5) * 50 + THREE.Math.randFloat(-1,1)
@scene.add( @instanceGrass( x, 2.5, y, 5.0, mat ) )
instanceGrass:(x,y,z,height,mat)->
geometry = new THREE.CylinderGeometry( 0.9, 0.0, height, 3, 5 )
mesh = new THREE.Mesh( geometry, mat )
mesh.position.set( x, y, z )
return mesh
ในที่นี้เราจะสร้างตารางกริดหญ้าขนาด 15 x 15 บิต เราเพิ่มการสุ่มตำแหน่งหญ้าแต่ละตำแหน่งเล็กน้อย เพื่อไม่ให้หญ้าเรียงกันเป็นแถวเหมือนทหาร ซึ่งจะดูแปลกๆ
ภูมิประเทศนี้เป็นเพียงระนาบแนวนอนที่วางไว้ที่ฐานของหญ้า (y = 2.5)
initTerrain:->
@plane = new THREE.Mesh( new THREE.PlaneGeometry(60, 60, 2, 2), new THREE.MeshPhongMaterial({ map: @grassTex }))
@plane.rotation.x = -Math.PI/2
@scene.add( @plane )
สิ่งที่เราทํามาจนถึงตอนนี้คือสร้างฉาก Three.js และเพิ่มหญ้าเล็กน้อยซึ่งสร้างจากกรวยย้อนกลับที่สร้างขึ้นตามกระบวนการ และภูมิประเทศแบบง่าย
ยังไม่มีอะไรที่ซับซ้อน
ตอนนี้ก็ถึงเวลาเริ่มเพิ่มลมแล้ว สิ่งแรกที่เราต้องการคือฝังข้อมูลความไวต่อลมลงในโมเดลหญ้า 3 มิติ
เราจะฝังข้อมูลนี้เป็นแอตทริบิวต์ที่กำหนดเองสำหรับจุดยอดแต่ละจุดของโมเดลหญ้า 3 มิติ และเราจะใช้กฎที่ว่าปลายด้านล่างของโมเดลหญ้า (ปลายกรวย) มีความไวเป็น 0 เนื่องจากติดอยู่กับพื้น ส่วนด้านบนของโมเดลหญ้า (ฐานของกรวย) มีความไวต่อลมสูงสุด เนื่องจากเป็นส่วนที่อยู่ห่างจากพื้นดินมากที่สุด
ต่อไปนี้เป็นวิธีเขียนโค้ดฟังก์ชัน instanceGrass ใหม่เพื่อเพิ่มการตอบสนองต่อลมเป็นแอตทริบิวต์ที่กำหนดเองสำหรับโมเดลหญ้า 3 มิติ
instanceGrass:(x,y,z,height)->
geometry = new THREE.CylinderGeometry( 0.9, 0.0, height, 3, 5 )
for i in [0..geometry.vertices.length-1] by 1
v = geometry.vertices[i]
r = (v.y / height) + 0.5
@windMaterial.attributes.windFactor.value[i] = r * r * r
# Create mesh
mesh = new THREE.Mesh( geometry, @windMaterial )
mesh.position.set( x, y, z )
return mesh
ตอนนี้เราใช้วัสดุที่กําหนดเองอย่าง windMaterial แทน MeshPhongMaterial ที่ใช้ก่อนหน้านี้ WindMaterial ใส่ WindMeshShader ไว้ด้วย ซึ่งเราจะได้เห็นในอีกสักครู่
ดังนั้น โค้ดใน instanceGrass จะวนผ่านจุดยอดทั้งหมดของโมเดลหญ้า และเพิ่มแอตทริบิวต์จุดยอดที่กำหนดเองชื่อ windFactor ให้กับจุดยอดแต่ละจุด windFactor นี้ตั้งค่าเป็น 0 สำหรับส่วนล่างของโมเดลหญ้า (จุดที่หญ้าควรจะสัมผัสกับพื้น) และมีค่าเป็น 1 สำหรับส่วนบนของโมเดลหญ้า
ส่วนผสมอีกอย่างที่เราต้องใช้คือการเพิ่มลมจริงลงในฉาก ตามที่ได้พูดคุยกัน เราจะใช้ Perlin Noise สำหรับงานนี้ เราจะสร้างพื้นผิวแบบ Noise ของ Perlin โดยอิงตามขั้นตอน
เราจะกำหนดพื้นผิวนี้ให้กับภูมิประเทศแทนพื้นผิวสีเขียวก่อนหน้านี้เพื่อความชัดเจน วิธีนี้จะช่วยให้คุณทราบสถานการณ์ของลมได้ง่ายขึ้น
ดังนั้น พื้นผิวที่เป็นจุดแบบ Perlin นี้จะครอบคลุมพื้นที่ของภูมิประเทศ และแต่ละพิกเซลของพื้นผิวจะระบุความรุนแรงของลมในพื้นที่ภูมิประเทศที่พิกัดนั้นอยู่ สี่เหลี่ยมผืนผ้าของภูมิประเทศจะเป็น "พื้นที่ลม"
เสียงรบกวน Perlin สร้างขึ้นแบบเป็นขั้นตอนผ่านโปรแกรมเปลี่ยนสีที่เรียกว่า NoiseShader โปรแกรมเปลี่ยนรูปแบบนี้ใช้อัลกอริทึมสัญญาณรบกวน Simplex 3 มิติจาก https://github.com/ashima/webgl-noise เวอร์ชัน WebGL นี้นำมาจากตัวอย่าง Three.js ของ MrDoob โดยตรงที่ http://mrdoob.github.com/three.js/examples/webgl_terrain_dynamic.html
NoiseShader ใช้เวลา สเกล และชุดพารามิเตอร์ออฟเซตเป็นค่าคงที่ และแสดงผลการกระจาย 2 มิติของ Perlin Noise ที่สวยงาม
class NoiseShader
uniforms:
"fTime" : { type: "f", value: 1 }
"vScale" : { type: "v2", value: new THREE.Vector2(1,1) }
"vOffset" : { type: "v2", value: new THREE.Vector2(1,1) }
...
เราจะใช้ Shader นี้เพื่อแสดงผล Perlin Noise เป็นพื้นผิว ซึ่งดำเนินการในฟังก์ชัน initNoiseShader
initNoiseShader:->
@noiseMap = new THREE.WebGLRenderTarget( 256, 256, { minFilter: THREE.LinearMipmapLinearFilter, magFilter: THREE.LinearFilter, format: THREE.RGBFormat } );
@noiseShader = new NoiseShader()
@noiseShader.uniforms.vScale.value.set(0.3,0.3)
@noiseScene = new THREE.Scene()
@noiseCameraOrtho = new THREE.OrthographicCamera( window.innerWidth / - 2, window.innerWidth / 2, window.innerHeight / 2, window.innerHeight / - 2, -10000, 10000 );
@noiseCameraOrtho.position.z = 100
@noiseScene.add( @noiseCameraOrtho )
@noiseMaterial = new THREE.ShaderMaterial
fragmentShader: @noiseShader.fragmentShader
vertexShader: @noiseShader.vertexShader
uniforms: @noiseShader.uniforms
lights:false
@noiseQuadTarget = new THREE.Mesh( new THREE.PlaneGeometry(window.innerWidth,window.innerHeight,100,100), @noiseMaterial )
@noiseQuadTarget.position.z = -500
@noiseScene.add( @noiseQuadTarget )
สิ่งที่โค้ดข้างต้นทําคือตั้งค่า noiseMap เป็นเป้าหมายการเรนเดอร์ Three.js, ติดตั้ง NoiseShader แล้วเรนเดอร์ด้วยกล้องออร์โธกราฟิกเพื่อหลีกเลี่ยงการบิดเบือนตามมุมมอง
ตามที่ได้พูดคุยกัน ตอนนี้เราจะใช้พื้นผิวนี้เป็นการแรเงาพื้นผิวหลักสำหรับภูมิประเทศด้วย ซึ่งไม่จำเป็นต่อการทำให้เอฟเฟกต์ลมทำงาน แต่การมีภาพก็ดี เพื่อให้เราเข้าใจสิ่งที่เกิดขึ้นกับการผลิตพลังงานลมได้ดียิ่งขึ้น
ฟังก์ชัน initTerrain ที่ปรับปรุงใหม่โดยใช้ noiseMap เป็นพื้นผิวมีดังนี้
initTerrain:->
@plane = new THREE.Mesh( new THREE.PlaneGeometry(60, 60, 2, 2), new THREE.MeshPhongMaterial( { map: @noiseMap, lights: false } ) )
@plane.rotation.x = -Math.PI/2
@scene.add( @plane )
เมื่อเรามีพื้นผิวลมแล้ว เรามาดู WindMeshShader ซึ่งมีหน้าที่ทำให้โมเดลหญ้าบิดเบี้ยวตามลม
ในการสร้างเชดเดอร์นี้ เราเริ่มจากเชดเดอร์ MeshPhongMaterial มาตรฐานของ Three.js แล้วทำการแก้ไข วิธีนี้เป็นวิธีที่รวดเร็วและได้ผลดีในการเริ่มต้นใช้งาน Shader ที่ใช้งานได้โดยไม่ต้องเริ่มตั้งแต่ต้น
เราจะไม่คัดลอกโค้ด Shader ทั้งหมดที่นี่ (คุณสามารถดูได้ในไฟล์ซอร์สโค้ด) เนื่องจากส่วนใหญ่จะเป็นโค้ดที่คัดลอกมาจาก Shader ของ MeshPhongMaterial แต่เรามาดูส่วนที่เกี่ยวข้องกับลมซึ่งมีการแก้ไขใน Vertex Shader
vec4 wpos = modelMatrix * vec4( position, 1.0 );
vec4 wpos = modelMatrix * vec4( position, 1.0 );
wpos.z = -wpos.z;
vec2 totPos = wpos.xz - windMin;
vec2 windUV = totPos / windSize;
vWindForce = texture2D(tWindForce,windUV).x;
float windMod = ((1.0 - vWindForce)* windFactor ) * windScale;
vec4 pos = vec4(position , 1.0);
pos.x += windMod * windDirection.x;
pos.y += windMod * windDirection.y;
pos.z += windMod * windDirection.z;
mvPosition = modelViewMatrix * pos;
ดังนั้นสิ่งที่โปรแกรมเปลี่ยนรูปแบบนี้จะทําคือคํานวณพิกัดการค้นหาพื้นผิว windUV ก่อน โดยอิงตามตําแหน่ง 2 มิติ xz (แนวนอน) ของจุดยอด พิกัด UV นี้ใช้เพื่อค้นหาแรงลม vWindForce จากพื้นผิวลมแบบ Perlin Noise
ค่า vWindForce นี้รวมกับ windFactor เฉพาะเวิร์กเท็กซ์ ซึ่งเป็นแอตทริบิวต์ที่กำหนดเองที่กล่าวถึงข้างต้น เพื่อคำนวณปริมาณการเปลี่ยนรูปที่เวิร์กเท็กซ์ต้องการ นอกจากนี้ เรายังมีพารามิเตอร์ windScale ระดับกลางที่ใช้ควบคุมความแรงของลมโดยรวม และเวกเตอร์ windDirection ที่ระบุทิศทางที่ลมควรเปลี่ยนรูป
ด้วยเหตุนี้ หญ้าจึงบิดเบี้ยวตามลม แต่เรายังไม่หยุดอยู่แค่นี้ ขณะนี้ การเปลี่ยนรูปนี้เป็นแบบคงที่และจะไม่สื่อถึงลักษณะของพื้นที่ที่มีลมแรง
ตามที่ได้แจ้งไปก่อนหน้านี้ เราจะต้องเลื่อนพื้นผิวที่เป็นจุดทั่วทั้งพื้นที่ที่มีลมเพื่อให้กระจกไหวตามลม
ซึ่งทำได้โดยการเลื่อนค่า vOffset ที่เป็นค่าคงที่ซึ่งส่งไปยัง NoiseShader เมื่อเวลาผ่านไป นี่คือพารามิเตอร์ vec2 ซึ่งจะช่วยให้เราระบุค่าออฟเซ็ตของเสียงรบกวนตามทิศทางหนึ่งๆ (ทิศทางลม) ได้
เราทําเช่นนี้ในฟังก์ชัน render ซึ่งเรียกใช้ในทุกเฟรม
render: =>
delta = @clock.getDelta()
if @windDirection
@noiseShader.uniforms[ "fTime" ].value += delta * @noiseSpeed
@noiseShader.uniforms[ "vOffset" ].value.x -= (delta * @noiseOffsetSpeed) * @windDirection.x
@noiseShader.uniforms[ "vOffset" ].value.y += (delta * @noiseOffsetSpeed) * @windDirection.z
...
เท่านี้ก็เรียบร้อย เราเพิ่งสร้างฉากที่มี "หญ้าแบบเป็นขั้นตอน" ซึ่งได้รับผลกระทบจากลม
การเพิ่มฝุ่นลงในส่วนผสม
มาเพิ่มสีสันให้ฉากกัน มาเพิ่มฝุ่นละอองให้ลอยไปมาเพื่อทำให้ฉากน่าสนใจยิ่งขึ้นกัน

ท้ายที่สุดแล้ว ฝุ่นก็ควรได้รับผลกระทบจากลม ดังนั้นจึงเหมาะอย่างยิ่งที่จะมีฝุ่นปลิวไปมาในฉากที่มีลม
ฝุ่นได้รับการตั้งค่าในฟังก์ชัน initDust เป็นระบบอนุภาค
initDust:->
for i in [0...5] by 1
shader = new WindParticleShader()
params = {}
params.fragmentShader = shader.fragmentShader
params.vertexShader = shader.vertexShader
params.uniforms = shader.uniforms
params.attributes = { speed: { type: 'f', value: [] } }
mat = new THREE.ShaderMaterial(params)
mat.map = shader.uniforms["map"].value = THREE.ImageUtils.loadCompressedTexture("textures/dust#{i}.dds")
mat.size = shader.uniforms["size"].value = Math.random()
mat.scale = shader.uniforms["scale"].value = 300.0
mat.transparent = true
mat.sizeAttenuation = true
mat.blending = THREE.AdditiveBlending
shader.uniforms["tWindForce"].value = @noiseMap
shader.uniforms[ "windMin" ].value = new THREE.Vector2(-30,-30 )
shader.uniforms[ "windSize" ].value = new THREE.Vector2( 60, 60 )
shader.uniforms[ "windDirection" ].value = @windDirection
geom = new THREE.Geometry()
geom.vertices = []
num = 130
for k in [0...num] by 1
setting = {}
vert = new THREE.Vector3
vert.x = setting.startX = THREE.Math.randFloat(@dustSystemMinX,@dustSystemMaxX)
vert.y = setting.startY = THREE.Math.randFloat(@dustSystemMinY,@dustSystemMaxY)
vert.z = setting.startZ = THREE.Math.randFloat(@dustSystemMinZ,@dustSystemMaxZ)
setting.speed = params.attributes.speed.value[k] = 1 + Math.random() * 10
setting.sinX = Math.random()
setting.sinXR = if Math.random() < 0.5 then 1 else -1
setting.sinY = Math.random()
setting.sinYR = if Math.random() < 0.5 then 1 else -1
setting.sinZ = Math.random()
setting.sinZR = if Math.random() < 0.5 then 1 else -1
setting.rangeX = Math.random() * 5
setting.rangeY = Math.random() * 5
setting.rangeZ = Math.random() * 5
setting.vert = vert
geom.vertices.push vert
@dustSettings.push setting
particlesystem = new THREE.ParticleSystem( geom , mat )
@dustSystems.push particlesystem
@scene.add particlesystem
ที่นี่จะมีการสร้างอนุภาคฝุ่น 130 อนุภาค และโปรดทราบว่าแต่ละรายการจะมี WindParticleShader พิเศษ
ตอนนี้ในแต่ละเฟรม เราจะย้ายอนุภาคไปรอบๆ เล็กน้อยโดยใช้ CoffeeScript โดยแยกจากลม รหัสมีดังนี้
moveDust:(delta)->
for setting in @dustSettings
vert = setting.vert
setting.sinX = setting.sinX + (( 0.002 * setting.speed) * setting.sinXR)
setting.sinY = setting.sinY + (( 0.002 * setting.speed) * setting.sinYR)
setting.sinZ = setting.sinZ + (( 0.002 * setting.speed) * setting.sinZR)
vert.x = setting.startX + ( Math.sin(setting.sinX) * setting.rangeX )
vert.y = setting.startY + ( Math.sin(setting.sinY) * setting.rangeY )
vert.z = setting.startZ + ( Math.sin(setting.sinZ) * setting.rangeZ )
นอกจากนี้ เราจะเลื่อนตำแหน่งของอนุภาคแต่ละอนุภาคตามลม ซึ่งดำเนินการใน WindParticleShader โดยเฉพาะในเวิร์กเทกซ์ Shader
โค้ดสำหรับโปรแกรมเปลี่ยนสีนี้คือ ParticleMaterial เวอร์ชันแก้ไขของ Three.js และนี่คือลักษณะของโค้ดหลัก
vec4 mvPosition;
vec4 wpos = modelMatrix * vec4( position, 1.0 );
wpos.z = -wpos.z;
vec2 totPos = wpos.xz - windMin;
vec2 windUV = totPos / windSize;
float vWindForce = texture2D(tWindForce,windUV).x;
float windMod = (1.0 - vWindForce) * windScale;
vec4 pos = vec4(position , 1.0);
pos.x += windMod * windDirection.x;
pos.y += windMod * windDirection.y;
pos.z += windMod * windDirection.z;
mvPosition = modelViewMatrix * pos;
fSpeed = speed;
float fSize = size * (1.0 + sin(time * speed));
#ifdef USE_SIZEATTENUATION
gl_PointSize = fSize * ( scale / length( mvPosition.xyz ) );
#else,
gl_PointSize = fSize;
#endif
gl_Position = projectionMatrix * mvPosition;
เวิร์กเทกซ์เชดเดอร์นี้ไม่แตกต่างจากที่เรามีสำหรับหญ้าที่บิดเบี้ยวตามลมมากนัก โดยจะใช้พื้นผิวที่เป็นจุดแบบ Perlin เป็นอินพุต และค้นหาค่า vWindForce ในพื้นผิวที่เป็นจุด โดยขึ้นอยู่กับตำแหน่งของฝุ่นในโลก จากนั้นจะใช้ค่านี้เพื่อแก้ไขตำแหน่งของอนุภาคฝุ่น
Riders On The Storm
ฉาก WebGL ที่ผจญภัยที่สุดน่าจะเป็นฉากสุดท้าย ซึ่งคุณจะเห็นได้หากคลิกผ่านบอลลูนเข้าไปในศูนย์กลางของพายุทอร์นาโดเพื่อไปยังจุดสิ้นสุดของการเดินทางในเว็บไซต์ และวิดีโอสุดพิเศษของรุ่นที่กำลังจะเปิดตัว

เมื่อสร้างฉากนี้ เราทราบดีว่าต้องมีฟีเจอร์หลักที่ทำให้เกิดประสบการณ์ที่มีประสิทธิภาพ พายุทอร์นาโดที่ปั่นจะทำหน้าที่เป็นจุดศูนย์กลาง และเลเยอร์ของเนื้อหาอื่นๆ จะปรับเปลี่ยนฟีเจอร์นี้ให้เข้ากับตำแหน่งเพื่อสร้างเอฟเฟกต์ที่น่าทึ่ง ด้วยเหตุนี้ เราจึงสร้างสิ่งที่เทียบเท่ากับฉากในสตูดิโอภาพยนตร์รอบๆ ชิดเดอร์แปลกๆ นี้
เราใช้วิธีการแบบผสมผสานเพื่อสร้างภาพคอมโพสิตที่สมจริง บางรายการเป็นเทคนิคภาพ เช่น รูปร่างของแสงเพื่อสร้างเอฟเฟกต์แสงแฟลร์ หรือหยดน้ำฝนที่เป็นภาพเคลื่อนไหวเป็นเลเยอร์บนฉากที่คุณเห็น ในกรณีอื่นๆ เราได้วาดพื้นผิวเรียบให้ดูเหมือนเคลื่อนไหวไปรอบๆ เช่น เลเยอร์เมฆที่บินในระดับต่ำซึ่งเคลื่อนไหวตามโค้ดระบบอนุภาค ส่วนเศษซากที่หมุนรอบพายุทอร์นาโดเป็นเลเยอร์ในฉาก 3 มิติที่จัดเรียงให้เคลื่อนไหวอยู่หน้าและหลังพายุทอร์นาโด
เหตุผลหลักที่เราสร้างฉากด้วยวิธีนี้คือเพื่อให้แน่ใจว่าเรามี GPU เพียงพอที่จะจัดการกับเชดเดอร์พายุทอร์นาโดให้สมดุลกับเอฟเฟกต์อื่นๆ ที่เราใช้ ตอนแรกเรามีปัญหาเรื่องการปรับสมดุล GPU อย่างมาก แต่ต่อมาฉากนี้ได้รับการเพิ่มประสิทธิภาพและเบากว่าฉากหลัก
บทแนะนำ: Shader พายุ
ในการสร้างลำดับพายุสุดท้ายนี้ เราใช้เทคนิคต่างๆ ร่วมกัน แต่หัวใจหลักของผลงานนี้คือ Shader GLSL ที่กําหนดเองซึ่งดูเหมือนพายุทอร์นาโด เราได้ลองใช้เทคนิคต่างๆ มากมาย ตั้งแต่ Vertex Shader เพื่อสร้างวังน้ำวนเรขาคณิตที่น่าสนใจ ไปจนถึงภาพเคลื่อนไหวแบบอนุภาค และแม้แต่ภาพเคลื่อนไหว 3 มิติของรูปทรงเรขาคณิตที่บิดเบี้ยว ไม่มีเอฟเฟกต์ใดที่ดูเหมือนจะสร้างความรู้สึกของพายุทอร์นาโดขึ้นมาใหม่หรือต้องใช้การประมวลผลมากเกินไป
ในที่สุดโปรเจ็กต์ที่แตกต่างออกไปโดยสิ้นเชิงก็ให้คำตอบแก่เราได้ โปรเจ็กต์คู่ขนานที่เกี่ยวข้องกับเกมเพื่อวิทยาศาสตร์เพื่อแมปสมองของหนูจากสถาบัน Max Planck (brainflight.org) ได้สร้างเอฟเฟกต์ภาพที่น่าสนใจ เราสร้างภาพยนตร์เกี่ยวกับภายในเซลล์ประสาทของหนูได้โดยใช้ชิดเดอร์แบบปริมาตรที่กําหนดเอง

เราพบว่าภายในเซลล์สมองมีลักษณะคล้ายกับกรวยของพายุทอร์นาโด และเนื่องจากเราใช้เทคนิคภาพปริมาตร เราจึงรู้ว่าสามารถดูชิเดอร์นี้จากทุกทิศทางในอวกาศ เราอาจตั้งค่าการแสดงผลของโปรแกรมจำลองแสงให้รวมกับฉากพายุ โดยเฉพาะในกรณีที่มีเมฆหลายชั้นอยู่ตรงกลางและอยู่เหนือพื้นหลังที่สมจริง
เทคนิคนี้ใช้เทคนิคพื้นฐานซึ่งใช้โปรแกรมเปลี่ยนรูปแบบ GLSL รายการเดียวเพื่อแสดงผลวัตถุทั้งชิ้นด้วยอัลกอริทึมการแสดงผลแบบง่ายที่เรียกว่าการแสดงผลด้วย Ray Marching ที่มีฟิลด์ระยะทาง ในเทคนิคนี้ ระบบจะสร้างพิกเซลเชดเดอร์ซึ่งประมาณระยะทางที่ใกล้ที่สุดกับพื้นผิวสำหรับแต่ละจุดบนหน้าจอ
คุณสามารถดูข้อมูลอ้างอิงที่ดีเกี่ยวกับอัลกอริทึมนี้ในภาพรวมโดย iq: การแสดงผลโลกด้วยรูปสามเหลี่ยม 2 รูป - Iñigo Quilez นอกจากนี้ คุณยังดูแกลเลอรีของ Shader ใน glsl.heroku.com ได้ด้วย ซึ่งจะมีตัวอย่างเทคนิคนี้มากมายให้ทดลองใช้
หัวใจสำคัญของโปรแกรมเปลี่ยนรูปแบบเริ่มต้นด้วยฟังก์ชันหลัก ซึ่งจะตั้งค่าการเปลี่ยนรูปแบบของกล้องและเข้าสู่ลูปที่ประเมินระยะทางไปยังพื้นผิวซ้ำๆ การเรียก RaytraceFoggy( direction_vector, max_iterations, color, color_multiplier ) คือจุดที่เกิดการคํานวณหลักของ Ray Marching
for(int i=0;i < number_of_steps;i++) // run the ray marching loop
{
old_d=d;
float shape_value=Shape(q); // find out the approximate distance to or density of the tornado cone
float density=-shape_value;
d=max(shape_value*step_scaling,0.0);// The max function clamps values smaller than 0 to 0
float step_dist=d+extra_step; // The point is advanced by larger steps outside the tornado,
// allowing us to skip empty space quicker.
if (density>0.0) { // When density is positive, we are inside the cloud
float brightness=exp(-0.6*density); // Brightness decays exponentially inside the cloud
// This function combines density layers to create a translucent fog
FogStep(step_dist*0.2,clamp(density, 0.0,1.0)*vec3(1,1,1), vec3(1)*brightness, colour, multiplier);
}
if(dist>max_dist || multiplier.x < 0.01) { return; } // if we've gone too far stop, we are done
dist+=step_dist; // add a new step in distance
q=org+dist*dir; // trace its direction according to the ray casted
}
แนวคิดคือเมื่อเราเข้าสู่รูปร่างของพายุทอร์นาโด เราจะเพิ่มการมีส่วนร่วมของสีลงในค่าสีสุดท้ายของพิกเซลเป็นประจำ รวมถึงการมีส่วนร่วมกับความทึบแสงตามแนวรัศมี วิธีนี้จะทำให้พื้นผิวของพายุทอร์นาโดดูนุ่มนวลเป็นชั้นๆ
ส่วนสําคัญถัดไปของพายุทอร์นาโดคือรูปร่างจริงซึ่งสร้างขึ้นโดยการคอมโพสิชันฟังก์ชันหลายรายการ เริ่มจากรูปกรวยที่ประกอบไปด้วยสัญญาณรบกวนเพื่อสร้างขอบขรุขระแบบออร์แกนิก จากนั้นบิดตามแกนหลักและบิดตามเวลา
mat2 Spin(float angle){
return mat2(cos(angle),-sin(angle),sin(angle),cos(angle)); // a rotation matrix
}
// This takes noise function and makes ridges at the points where that function crosses zero
float ridged(float f){
return 1.0-2.0*abs(f);
}
// the isosurface shape function, the surface is at o(q)=0
float Shape(vec3 q)
{
float t=time;
if(q.z < 0.0) return length(q);
vec3 spin_pos=vec3(Spin(t-sqrt(q.z))*q.xy,q.z-t*5.0); // spin the coordinates in time
float zcurve=pow(q.z,1.5)*0.03; // a density function dependent on z-depth
// the basic cloud of a cone is perturbed with a distortion that is dependent on its spin
float v=length(q.xy)-1.5-zcurve-clamp(zcurve*0.2,0.1,1.0)*snoise(spin_pos*vec3(0.1,0.1,0.1))*5.0;
// create ridges on the tornado
v=v-ridged(snoise(vec3(Spin(t*1.5+0.1*q.z)*q.xy,q.z-t*4.0)*0.3))*1.2;
return v;
}
การสร้างโปรแกรมเปลี่ยนสีประเภทนี้เป็นเรื่องยาก นอกจากปัญหาที่เกี่ยวข้องกับการแยกการดำเนินการที่คุณกำลังสร้างแล้ว ยังมีปัญหาด้านการเพิ่มประสิทธิภาพและความเข้ากันได้ข้ามแพลตฟอร์มที่รุนแรงซึ่งคุณต้องติดตามและแก้ไขก่อนจึงจะใช้งานในเวอร์ชันที่ใช้งานจริงได้
ส่วนแรกของปัญหาคือการเพิ่มประสิทธิภาพเชดเดอร์นี้สำหรับฉากของเรา ในการแก้ปัญหานี้ เราต้องใช้แนวทางที่ "ปลอดภัย" ในกรณีที่ชิลด์จะหนักเกินไป โดยเราใช้เทคนิคนี้ในการคอมโพสิทชิดเดอร์พายุทอร์นาโดที่ความละเอียดที่ต่างกันจากการสุ่มตัวอย่างฉากส่วนที่เหลือ ข้อมูลนี้มาจากไฟล์ stormTest.coffee (ใช่ นี่เป็นการทดสอบ)
เราเริ่มต้นด้วยเรนเดอร์เป้าหมายที่ตรงกับความกว้างและความสูงของฉากเพื่อให้ความละเอียดของเชดเดอร์พายุทอร์นาโดกับฉากเป็นอิสระต่อกัน จากนั้นเราจะตัดสินใจลดความละเอียดของการปรับแต่งเมฆพายุแบบไดนามิกโดยอิงตามอัตราเฟรมที่เราได้รับ
...
Line 1383
@tornadoRT = new THREE.WebGLRenderTarget( @SCENE_WIDTH, @SCENE_HEIGHT, paramsN )
...
Line 1403
# Change settings based on FPS
if @fpsCount > 0
if @fpsCur < 20
@tornadoSamples = Math.min( @tornadoSamples + 1, @MAX_SAMPLES )
if @fpsCur > 25
@tornadoSamples = Math.max( @tornadoSamples - 1, @MIN_SAMPLES )
@tornadoW = @SCENE_WIDTH / @tornadoSamples // decide tornado resWt
@tornadoH = @SCENE_HEIGHT / @tornadoSamples // decide tornado resHt
สุดท้าย เราแสดงผลพายุทอร์นาโดบนหน้าจอโดยใช้อัลกอริทึม sal2x แบบง่าย (เพื่อหลีกเลี่ยงลักษณะที่ไม่เป็นระเบียบ) @บรรทัด 1107 ใน stormTest.coffee ซึ่งหมายความว่าในกรณีที่แย่ที่สุด เราจะได้พายุทอร์นาโดที่เบลอมากขึ้น แต่อย่างน้อยก็ใช้งานได้โดยที่ผู้ใช้ยังคงมีสิทธิ์ควบคุม
ขั้นตอนการเพิ่มประสิทธิภาพถัดไปต้องเจาะลึกอัลกอริทึม ปัจจัยการประมวลผลที่ขับเคลื่อนในโปรแกรมเปลี่ยนสีคือการทำซ้ำที่ดำเนินการกับแต่ละพิกเซลเพื่อพยายามประมาณระยะทางของฟังก์ชันพื้นผิว: จำนวนการทำซ้ำของลูปการแรเงาด้วยรังสี เมื่อใช้ขนาดขั้นที่ใหญ่ขึ้น เราจะได้รับค่าประมาณพื้นผิวของพายุทอร์นาโดด้วยการวนซ้ำน้อยลงขณะที่เราอยู่นอกพื้นผิวที่มีเมฆ เมื่ออยู่ด้านใน เราจะลดขนาดการแบ่งขั้นเพื่อเพิ่มความแม่นยำและเพื่อผสมค่าต่างๆ เพื่อสร้างเอฟเฟกต์หมอก นอกจากนี้ การสร้างทรงกระบอกที่กําหนดขอบเขตเพื่อรับค่าประมาณความลึกของรังสีที่ฉายยังช่วยเพิ่มความเร็วได้อีกด้วย
ส่วนถัดไปของปัญหาคือการตรวจสอบว่า shader นี้จะทำงานบนการ์ดวิดีโอต่างๆ ได้ เราทำการทดสอบทุกครั้งและเริ่มสร้างความเข้าใจเกี่ยวกับประเภทปัญหาความเข้ากันได้ที่อาจพบ สาเหตุที่เราไม่สามารถแก้ปัญหาได้ดีไปกว่าการคาดเดาคือเราไม่สามารถรับข้อมูลการแก้ไขข้อบกพร่องที่ดีเกี่ยวกับข้อผิดพลาดได้เสมอไป สถานการณ์ทั่วไปคือข้อผิดพลาดของ GPU ที่อาจเกิดขึ้นอีกหรือแม้แต่ระบบอาจขัดข้อง
ปัญหาความเข้ากันได้ของบอร์ดวิดีโอข้ามรุ่นมีวิธีแก้ไขคล้ายกัน นั่นคือตรวจสอบว่าได้ป้อนค่าคงที่แบบคงที่ของประเภทข้อมูลที่แน่นอนตามที่กําหนด เช่น 0.0 สําหรับ float และ 0 สําหรับ int โปรดระมัดระวังเมื่อเขียนฟังก์ชันที่ยาวขึ้น ควรแบ่งออกเป็นฟังก์ชันที่ง่ายขึ้นหลายรายการและตัวแปรชั่วคราว เนื่องจากคอมไพเลอร์ดูเหมือนจะจัดการกับบางกรณีไม่ถูกต้อง ตรวจสอบว่าพื้นผิวทั้งหมดเป็นจำนวนที่ยกกำลัง 2 มีขนาดไม่ใหญ่เกินไป และในทุกกรณีให้ใช้ "ความระมัดระวัง" เมื่อค้นหาข้อมูลพื้นผิวในลูป
ปัญหาใหญ่ที่สุดที่เราพบเกี่ยวกับความเข้ากันได้คือเอฟเฟกต์แสงของพายุ เราใช้พื้นผิวที่สร้างไว้ล่วงหน้าซึ่งพันรอบพายุหมุนเพื่อให้เราระบายสีเศษฝุ่นของพายุได้ นี่เป็นเอฟเฟกต์ที่ยอดเยี่ยมและทำให้ผสานพายุหมุนเข้ากับสีของฉากได้ง่าย แต่ใช้เวลานานในการพยายามทำให้ทำงานบนแพลตฟอร์มอื่นๆ ได้

เว็บไซต์เวอร์ชันอุปกรณ์เคลื่อนที่
ประสบการณ์การใช้งานบนอุปกรณ์เคลื่อนที่ไม่สามารถแปลมาจากเวอร์ชันเดสก์ท็อปได้โดยตรง เนื่องจากข้อกำหนดด้านเทคโนโลยีและการประมวลผลมีความละเอียดอ่อนเกินไป เราจึงต้องสร้างสิ่งใหม่ๆ ที่กําหนดเป้าหมายไปยังผู้ใช้อุปกรณ์เคลื่อนที่โดยเฉพาะ
เราคิดว่าการมีตู้ถ่ายภาพงานเทศกาลจากเดสก์ท็อปเป็นแอปพลิเคชันเว็บบนอุปกรณ์เคลื่อนที่ซึ่งจะใช้กล้องของอุปกรณ์เคลื่อนที่ของผู้ใช้นั้นเจ๋งดี เป็นสิ่งที่เราไม่เคยเห็นมาก่อน
เราเขียนโค้ดการเปลี่ยนรูปแบบ 3 มิติใน CSS3 เพื่อเพิ่มสีสัน การลิงก์กับไจโรสโคปและเครื่องวัดความเร่งช่วยให้เราเพิ่มประสบการณ์การใช้งานที่สมจริงได้ เว็บไซต์จะตอบสนองต่อลักษณะที่คุณถือ เลื่อน และมองโทรศัพท์
ขณะเขียนบทความนี้ เราคิดว่าควรให้คำแนะนำบางอย่างเกี่ยวกับวิธีทําให้กระบวนการพัฒนาแอปบนอุปกรณ์เคลื่อนที่ราบรื่น มาดูกันเลย ไปดูกันเลยว่าคุณจะเรียนรู้อะไรจากรายงานนี้บ้าง
เคล็ดลับและคำแนะนำเกี่ยวกับอุปกรณ์เคลื่อนที่
โปรแกรมโหลดล่วงหน้าเป็นสิ่งที่จำเป็น ไม่ใช่สิ่งที่ควรหลีกเลี่ยง เราทราบดีว่าบางครั้งอาจเกิดกรณีหลังขึ้น สาเหตุหลักคือคุณต้องดูแลรักษารายการที่คุณโหลดล่วงหน้าอยู่เสมอเมื่อโปรเจ็กต์เติบโตขึ้น ที่แย่กว่านั้นคือ ยังไม่มีความชัดเจนมากนักว่าคุณควรคำนวณความคืบหน้าในการโหลดอย่างไรหากดึงทรัพยากรที่แตกต่างกันและดึงหลายรายการพร้อมกัน คลาสนามธรรม "Task" ที่กําหนดเองและทั่วไปมากของเราจึงมีประโยชน์ในสถานการณ์นี้ แนวคิดหลักคืออนุญาตให้มีโครงสร้างที่ฝังอยู่อย่างไม่มีที่สิ้นสุด โดยที่งานหนึ่งๆ อาจมีงานย่อยของตัวเอง ซึ่งอาจมีงานย่อยของตัวเองอีกต่อๆ ไป นอกจากนี้ งานแต่ละรายการจะคํานวณความคืบหน้าโดยอิงตามความคืบหน้าของงานย่อย (แต่ไม่ใช่ความคืบหน้าของงานหลัก) เราได้สร้างโครงสร้างที่มีลักษณะดังนี้เพื่อให้ MainPreloadTask, AssetPreloadTask และ TemplatePreFetchTask ทั้งหมดมาจาก Task

แนวทางนี้และคลาส Task ช่วยให้เราทราบความคืบหน้าโดยรวม (MainPreloadTask) หรือแค่ความคืบหน้าของชิ้นงาน (AssetPreloadTask) หรือความคืบหน้าของการโหลดเทมเพลต (TemplatePreFetchTask) ได้อย่างง่ายดาย ความคืบหน้าที่เท่าๆ กันของไฟล์หนึ่งๆ หากต้องการดูวิธีดำเนินการ ให้ดูคลาส Task ที่ /m/javascripts/raw/util/Task.js และการใช้งานจริงของงานได้ที่ /m/javascripts/preloading/task ต่อไปนี้เป็นตัวอย่างที่ตัดมาจากวิธีที่เราตั้งค่าคลาส /m/javascripts/preloading/task/MainPreloadTask.js ซึ่งเป็น Wrapper สำหรับการโหลดล่วงหน้าขั้นสูงสุด
Package('preloading.task', [
Import('util.Task'),
...
Class('public MainPreloadTask extends Task', {
_public: {
MainPreloadTask : function() {
var subtasks = [
new AssetPreloadTask([
{name: 'cutout/cutout-overlay-1', ext: 'png', type: ImagePreloader.TYPE_BACKGROUND, responsive: true},
{name: 'journey/scene1', ext: 'jpg', type: ImagePreloader.TYPE_IMG, responsive: false}, ...
...
]),
new TemplatePreFetchTask([
'page.HomePage',
'page.CutoutPage',
'page.JourneyToOzPage1', ...
...
])
];
this._super(subtasks);
}
}
})
]);
ในคลาส /m/javascripts/preloading/task/subtask/AssetPreloadTask.js นอกจากการสังเกตวิธีที่คลาสนี้สื่อสารกับ MainPreloadTask (ผ่านการใช้งาน Task ที่แชร์) แล้ว คุณควรสังเกตวิธีที่เราโหลดชิ้นงานที่ขึ้นอยู่กับแพลตฟอร์มด้วย โดยพื้นฐานแล้ว เรามีรูปภาพ 4 ประเภท มาตรฐานสำหรับอุปกรณ์เคลื่อนที่ (.ext โดยที่ ext คือนามสกุลไฟล์ ซึ่งมักจะเป็น .png หรือ .jpg), ความละเอียด Retina สำหรับอุปกรณ์เคลื่อนที่ (-2x.ext), มาตรฐานสำหรับแท็บเล็ต (-tab.ext) และความละเอียด Retina สำหรับแท็บเล็ต (-tab-2x.ext) แทนที่จะทำการตรวจจับใน MainPreloadTask และกำหนดอาร์เรย์ชิ้นงาน 4 รายการแบบฮาร์ดโค้ด เราเพียงแค่ระบุชื่อและนามสกุลของชิ้นงานที่จะใช้โหลดล่วงหน้า และระบุว่าชิ้นงานนั้นขึ้นอยู่กับแพลตฟอร์มหรือไม่ (responsive = true / false) จากนั้น AssetPreloadTask จะสร้างชื่อไฟล์ให้เรา
resolveAssetUrl : function(assetName, extension, responsive) {
return AssetPreloadTask.ASSETS_ROOT + assetName + (responsive === true ? ((Detection.getInstance().tablet ? '-tab' : '') + (Detection.getInstance().retina ? '-2x' : '')) : '') + '.' + extension;
}
โค้ดจริงที่ดำเนินการโหลดชิ้นงานล่วงหน้าจะมีลักษณะดังนี้ (/m/javascripts/raw/util/ImagePreloader.js)
loadUrl : function(url, type, completeHandler) {
if(type === ImagePreloader.TYPE_BACKGROUND) {
var $bg = $('<div>').hide().css('background-image', 'url(' + url + ')');
this.$preloadContainer.append($bg);
} else {
var $img= $('<img />').attr('src', url).hide();
this.$preloadContainer.append($img);
}
var image = new Image();
this.cache[this.generateKey(url)] = image;
image.onload = completeHandler;
image.src = url;
}
generateKey : function(url) {
return encodeURIComponent(url);
}
บทแนะนำ: ตู้ถ่ายรูป HTML5 (iOS6/Android)
เมื่อพัฒนา OZ บนอุปกรณ์เคลื่อนที่ เราพบว่าตัวเองใช้เวลาส่วนใหญ่ไปกับการเล่นเกมตู้ถ่ายรูปแทนที่จะทำงาน :D นั่นเป็นเพราะเกมนี้สนุกมาก เราจึงสร้างเดโมให้คุณลองใช้

คุณดูการสาธิตเวอร์ชันที่ใช้จริงได้ที่นี่ (เรียกใช้บน iPhone หรือโทรศัพท์ Android)
http://u9html5rocks.appspot.com/demos/mobile_photo_booth
หากต้องการตั้งค่า คุณต้องมีอินสแตนซ์แอปพลิเคชัน Google App Engine ฟรีที่คุณสามารถเรียกใช้แบ็กเอนด์ได้ โค้ดส่วนหน้าไม่ซับซ้อน แต่อาจมีปัญหาบางอย่าง มาดูกันทีละข้อ
- ประเภทไฟล์รูปภาพที่อนุญาต
เราต้องการให้ผู้ใช้อัปโหลดรูปภาพได้เท่านั้น (เนื่องจากเป็นตู้ถ่ายรูปภาพ ไม่ใช่ตู้ถ่ายวิดีโอ) ในทางทฤษฎี คุณระบุตัวกรองใน HTML ได้ ดังนี้
input id="fileInput" class="fileInput" type="file" name="file" accept="image/*"
แต่ดูเหมือนว่าวิธีนี้จะใช้ได้ใน iOS เท่านั้น เราจึงต้องเพิ่มการตรวจสอบ RegExp เพิ่มเติมเมื่อเลือกไฟล์แล้ว ดังนี้
this.$fileInput.fileupload({
dataType: 'json',
autoUpload : true,
add : function(e, data) {
if(!data.files[0].name.match(/(\.|\/)(gif|jpe?g|png)$/i)) {
return self.onFileTypeNotSupported();
}
}
});
- การยกเลิกการอัปโหลดหรือการเลือกไฟล์ ความไม่สอดคล้องอีกอย่างหนึ่งที่เราสังเกตเห็นระหว่างกระบวนการพัฒนาคือวิธีที่อุปกรณ์ต่างๆ แจ้งการเลือกไฟล์ที่ยกเลิก โทรศัพท์และแท็บเล็ต iOS จะไม่ดำเนินการใดๆ เลยและไม่แจ้งเตือน ดังนั้นเราจึงไม่ต้องดำเนินการพิเศษใดๆ สำหรับกรณีนี้ อย่างไรก็ตาม โทรศัพท์ Android จะเรียกใช้ฟังก์ชัน add() อยู่ดี แม้ว่าจะไม่ได้เลือกไฟล์ใดก็ตาม วิธีจัดการกับกรณีนี้
add : function(e, data) {
if(data.files.length === 0 || (data.files[0].size === 0 && data.files[0].name === "" && data.files[0].fileName === "")) {
return self.onNoFileSelected();
} else if(data.files.length > 1) {
return self.onMultipleFilesSelected();
}
}
ส่วนที่เหลือทำงานได้ค่อนข้างราบรื่นในแพลตฟอร์มต่างๆ สนุกกับการสร้างสรรค์
บทสรุป
บทความนี้กล่าวถึงแนวทางเพียงไม่กี่วิธีที่เราใช้เนื่องจาก Find Your Way To Oz มีขนาดใหญ่มากและมีเทคโนโลยีต่างๆ ที่เกี่ยวข้องมากมาย
หากอยากดูโค้ดแหล่งที่มาทั้งหมดของ Find Your Way To Oz โปรดดูที่ลิงก์นี้
เครดิต
คลิกที่นี่เพื่อดูรายชื่อเครดิตทั้งหมด
ข้อมูลอ้างอิง
- CoffeeScript - http://coffeescript.org/
- Backbone.js - http://backbonejs.org/
- Three.js - http://mrdoob.github.com/three.js/
- Max Planck Institute (brainflight.org) - http://brainflight.org/