กรณีศึกษา - ค้นหาหนทางสู่ออซ

เกริ่นนำ

"ค้นหาทางสู่ออซ" เป็นการทดลองใหม่ของ Google Chrome ที่เผยแพร่โดย 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 กลุ่มนี้ตามรายการเหตุการณ์ที่คัดสรรมาแล้ว การสื่อสารนี้ใช้เหตุการณ์ HTML5 ของ Backbone Router + onHashChange ที่ควบคุมพื้นที่ที่ควรเคลื่อนไหวเข้า/ออก (แหล่งที่มาของโปรเจ็กต์: /develop/coffee/router/Router.coffee)

บทแนะนำ: การรองรับภาพต่อเรียงและจอประสาทตา

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

จอแสดงผลปกติ - http://findyourwaytooz.com/img/home/interface_1x.png จอแสดงผลแบบ Retina - http://findyourwaytooz.com/img/home/interface_2x.png

มาดูเคล็ดลับบางส่วนเกี่ยวกับวิธีที่เราใช้ประโยชน์จากภาพต่อเรียงและวิธีการนำไปใช้กับอุปกรณ์ที่มีหน้าจอเรตินา รวมทั้งออกแบบอินเทอร์เฟซให้คมชัดและเป็นระเบียบมากที่สุด

การสร้างภาพต่อเรียง

ในการสร้างภาพต่อเรียง เราใช้ TexturePacker ซึ่งจะเอาต์พุตในรูปแบบใดก็ได้ที่คุณต้องการ ในกรณีนี้ เราส่งออกเป็น EaselJS ซึ่งสะอาดมากและสามารถใช้สร้างสไปรท์แบบเคลื่อนไหวได้ด้วย

การใช้ภาพต่อเรียงที่สร้างขึ้น

เมื่อสร้างภาพต่อเรียงแล้ว คุณจะเห็นไฟล์ 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]
   },
}

โดยที่

  • รูปภาพหมายถึง URL ของสไปรท์ชีต
  • เฟรมคือพิกัดขององค์ประกอบ UI แต่ละรายการ [x, y, ความกว้าง, ความสูง]
  • ภาพเคลื่อนไหวคือชื่อของเนื้อหาแต่ละรายการ

โปรดทราบว่าเราใช้รูปภาพความหนาแน่นสูงเพื่อสร้างภาพต่อเรียง จากนั้นเราได้สร้างเวอร์ชันปกติโดยปรับขนาดเป็นภาพครึ่งหนึ่ง

สรุป

เท่านี้ก็เรียบร้อย แค่มีข้อมูลโค้ด 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 เท่านั้น และช่วยให้คุณสามารถสร้างฉากของมายาขนาดใหญ่เป็นไฟล์บีบอัดเดียวที่เพิ่มประสิทธิภาพสำหรับการคลายการแพคข้อมูลรันไทม์ เหตุผลที่ดีที่สุดก็เพราะฉากนั้นมีประสิทธิภาพในโครงสร้างข้อมูลแบบเดียวกับที่มีการดัดแปลงระหว่างการแสดงภาพและภาพเคลื่อนไหว มีการแยกวิเคราะห์ที่ต้องทำกับไฟล์น้อยมากเมื่อโหลด การคลายแพ็กในรูปแบบ Flash ทำได้ค่อนข้างเร็วเนื่องจากไฟล์อยู่ในรูปแบบ AMF ซึ่ง Flash นั้นสามารถคลายแพ็กได้ทันที การใช้รูปแบบเดียวกันใน WebGL จะต้องใช้ CPU มากขึ้น อันที่จริงแล้วเราต้องสร้างเลเยอร์ JavaScript ที่คลายการแพคข้อมูลขึ้นใหม่ โดยหลักๆ แล้วจะคลายการบีบอัดไฟล์เหล่านั้นและสร้างโครงสร้างข้อมูลที่จำเป็นต่อการทำให้ WebGL ใช้งานได้ใหม่ การคลายแพ็กฉาก 3 มิติทั้งภาพเป็นการดำเนินการที่ต้องอาศัย CPU เล็กน้อย การคลายแพ็กฉาก 1 ในค้นหาทางสู่ออซจะใช้เวลาประมาณ 2 วินาทีในเครื่องระดับปานกลางถึงระดับสูง ดังนั้นจึงดำเนินการโดยใช้เทคโนโลยี Web Workers ใน "การตั้งค่าฉาก" (ก่อนที่จะเริ่มฉากจริง) เพื่อไม่ให้ประสบการณ์ของผู้ใช้หยุดชะงัก

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

แต่ปัญหาที่เราเจอก็คือตอนนี้เราต้องรับมือกับ WebGL ซึ่งเป็นเด็กหน้าใหม่ เป็นเด็กที่รับมือยากมาก นั่นเป็นการตั้งค่ามาตรฐานสำหรับประสบการณ์ 3 มิติบนเบราว์เซอร์ เราจึงสร้างเลเยอร์ JavaScript เฉพาะกิจซึ่งจะนำไฟล์ฉาก 3 มิติของบรรณารักษ์ 3 มิติที่บีบอัดมา มาแปลให้อยู่ในรูปแบบที่ WebGL เข้าใจอย่างเหมาะสม

บทแนะนำ: ขอให้มีลมแรง

"ค้นหาทางสู่ออซ" เป็นธีมที่เกิดซ้ำ เส้นเรื่องของเนื้อเรื่องมีโครงสร้างให้ไล่ระดับเป็นสายลม

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

ด้วยเหตุนี้คุณจึงควรสร้างเอฟเฟกต์ลมที่สมจริง

เราสร้างฉากคาร์นิวัล 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 และ initSurfacein จะเติมข้อมูลฉากด้วยหญ้าและภูมิประเทศตามลำดับ ดังนี้

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 สำหรับกรณีนี้ เราจะสร้างพื้นผิวสัญญาณรบกวนแบบ Perlin ตามกระบวนการ

เพื่อความชัดเจน เราจะกำหนดพื้นผิวนี้ให้กับภูมิประเทศ แทนพื้นผิวสีเขียวที่มีอยู่ก่อนหน้า ซึ่งจะช่วยให้คุณรับรู้ถึงสิ่งที่เกิดขึ้นกับลมได้ง่ายขึ้น

ดังนั้นพื้นผิวนอยส์ Perlin นี้จะครอบคลุมส่วนขยายของภูมิประเทศของเราในเชิงพื้นที่ และพื้นผิวแต่ละพิกเซลจะระบุความเข้มของลมในพื้นที่ภูมิประเทศที่พิกเซลนั้นตกลงมา รูปสี่เหลี่ยมผืนผ้าของภูมิประเทศจะเป็น "พื้นที่แห่งลม"

นอยส์ Perlin จะสร้างขึ้นผ่านตัวปรับแสงเงาที่เรียกว่า NoiseShader ตัวปรับแสงเงานี้ใช้อัลกอริทึมนอยส์ด้านเดียวแบบ 3 มิติจาก https://github.com/ashima/webgl-noise เวอร์ชัน WebGL ของเวอร์ชันนี้นำแบบคำต่อคำมาจากตัวอย่าง Three.js ของ MrDoob ที่ http://mrdoob.github.com/three.js/examples/webgl_terrain_dynamic.html

NoiseShader ใช้เวลา สเกล และชุดพารามิเตอร์ออฟเซ็ต เป็นแบบเดียวกัน และให้การกระจายเสียง Perlin แบบ 2 มิติที่ดี

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 ที่จะเปลี่ยนโฉมโมเดลพื้นหญ้าตามลม

ในการสร้างตัวปรับแสงเงานี้ เราเริ่มต้นจากตัวปรับแสงเงา Three.js MeshPhongMaterial แบบมาตรฐานและนำมาปรับแต่ง นี่เป็นวิธีที่ดีรวดเร็วและสกปรกในการเริ่มต้นใช้งานด้วยเฉดสีที่ใช้ได้ผล โดยไม่ต้องเริ่มใหม่ตั้งแต่ต้น

เราจะไม่คัดลอกโค้ดให้ตัวปรับแสงเงาทั้งหมดที่นี่ (โปรดดูในโค้ดต้นฉบับในไฟล์โค้ดต้นฉบับ) เนื่องจากโค้ดส่วนใหญ่จะเป็นตัวจำลองของตัวปรับแสงเงา 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 โดยอิงตามตำแหน่ง 2D, xz (แนวนอน) ของจุดยอดมุม พิกัดยูวีนี้ใช้เพื่อค้นหาแรงลม vWindForce จากพื้นผิวลมเสียง Perlin

ค่า vWindForce นี้มีการรวมเข้ากับ windFactor ที่เฉพาะเจาะจงของจุดยอดมุม ซึ่งเป็นแอตทริบิวต์ที่กำหนดเองที่กล่าวถึงข้างต้น เพื่อคำนวณหาความแปรปรวนที่ Vertex ต้องการ และเรายังมีพารามิเตอร์ windScale ระดับโลกสำหรับควบคุมความแรงโดยรวมของลม และ windDirection, เวกเตอร์ ซึ่งระบุทิศทางที่จำเป็นต้องมีการเปลี่ยนสภาพลม

ซึ่งทำให้เศษหญ้าของเราผิดรูปเนื่องจากลม แต่เรายังคงดำเนินการไม่เสร็จ แต่ในปัจจุบัน การผิดรูปนี้ยังคงเป็นไปไม่ได้ และจะไม่บ่งบอกถึงผลกระทบในพื้นที่ที่มีลมแรง

ดังที่ได้กล่าวไปแล้วว่าเราจะต้องเลื่อนพื้นผิวของเสียงรบกวนไปทั่วบริเวณที่มีลมเพื่อให้กระจกของเราโบกมือได้

ซึ่งทำโดยการเปลี่ยนเมื่อเวลาผ่านไป ให้เป็นเครื่องแบบ vOffset ที่ส่งไปยัง NoiseShader นี่คือพารามิเตอร์ vec2 ที่จะช่วยเราระบุการชดเชยเสียงรบกวนตามทิศทางที่เฉพาะเจาะจง (ทิศทางลมของเรา) ได้

เราจะทำเช่นนี้ในฟังก์ชันการแสดงผลที่มีการเรียกในทุกเฟรม

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 โดยเฉพาะในตัวปรับสี Vertex

โค้ดสำหรับตัวปรับแสงเงานี้คือ 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 ในพื้นผิวของสัญญาณรบกวน ทั้งนี้ขึ้นอยู่กับตำแหน่งของฝุ่น จากนั้นจะใช้ค่านี้เพื่อปรับตำแหน่งของอนุภาคฝุ่น

ไรเดอร์ลอยฟ้าข้ามเมือง

ประสบการณ์ที่น่าสนใจที่สุดในฉาก WebGL ของเราอาจเป็นฉากสุดท้ายที่คุณจะเห็นได้หากคุณคลิกผ่านบอลลูนไปที่ดวงตาของพายุทอร์นาโดเพื่อไปให้ถึงจุดสิ้นสุดของการเดินทางบนเว็บไซต์ และวิดีโอพิเศษของการเปิดตัวที่กำลังจะมาถึง

ฉากนั่งบอลลูน

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

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

เหตุผลหลักที่ทำให้เราต้องสร้างฉากนี้ขึ้นมาก็เพื่อให้มั่นใจว่าเรามี GPU เพียงพอที่จะจัดการกับตัวเฉดสีทอร์นาโดได้อย่างสมดุลกับเอฟเฟกต์อื่นๆ ที่เรานำมาใช้ ในช่วงแรกนั้นเรามีปัญหาเกี่ยวกับการรักษาสมดุลของ GPU อย่างมาก แต่ต่อมาก็มีการเพิ่มประสิทธิภาพให้ฉากนี้และมีน้ำหนักน้อยลงกว่าฉากหลัก

บทแนะนำ: The Storm Shader

ในการสร้างลำดับเรื่องราวของพายุขั้นสุดท้าย เราได้นำเทคนิคต่างๆ มากมายมารวมกัน แต่สิ่งที่เราให้ความสำคัญเป็นหลักคือเกม GLSL แบบกำหนดเองที่ดูเหมือนพายุทอร์นาโด เราได้ทดลองเทคนิคต่างๆ มากมายจากตัวสร้างเฉดสี Vertex เพื่อสร้างน้ำวนรูปทรงเรขาคณิตที่น่าสนใจ ไปจนถึงภาพเคลื่อนไหวที่มาจากอนุภาค และแม้แต่ภาพเคลื่อนไหว 3 มิติของรูปทรงเรขาคณิตที่บิดเบี้ยว ปรากฏว่าผลกระทบเหล่านี้ไม่ได้สร้างความรู้สึกของพายุทอร์นาโดให้ตื่นตาตื่นใจหรือจำเป็นต้องอาศัยการประมวลผลมากเกินไป

ในที่สุดแต่ละโครงการก็ทำให้เราได้คำตอบ โครงการที่ทำคู่ขนานกับเกมวิทยาศาสตร์เพื่อสร้างแผนที่สมองของหนูจาก Max Planck Institute (brainflight.org) ได้สร้างเอฟเฟกต์ภาพที่น่าสนใจ เราสามารถสร้างภาพยนตร์ที่อยู่ภายในเซลล์ประสาทของเมาส์โดยใช้ Volumetric Shaderr แบบกำหนดเอง

ภายในเซลล์ประสาทของเมาส์ที่ใช้ตัวปรับเฉดสีแบบกำหนดเอง
ภายในเซลล์ประสาทของเมาส์โดยใช้ Volumetric Shades แบบกำหนดเอง

เราพบว่าด้านในของเซลล์สมองมีลักษณะคล้ายกับ Funnel ของพายุทอร์นาโด และเนื่องจากเราใช้เทคนิคเชิงปริมาตร เราจึงรู้ว่าจะดูเฉดสีนี้จากทุกทิศทางในอวกาศได้ เราสามารถตั้งค่าการแสดงภาพตัวปรับเฉดสีให้กลมกลืนกับฉากพายุได้ โดยเฉพาะอย่างยิ่งหากถูกสอดประสานใต้ก้อนเมฆและอยู่เหนือพื้นหลังสุดอลังการ

เทคนิคให้เฉดสีเกี่ยวข้องกับเคล็ดลับในการใช้ตัวปรับเฉดสี GLSL เดี่ยวเพื่อแสดงผลวัตถุทั้งวัตถุด้วยอัลกอริทึมการแสดงผลแบบง่ายที่เรียกว่าการแสดงผลแบบ Ray marching กับฟิลด์ระยะทาง ในเทคนิคนี้ จะมีการสร้างตัวปรับแสงเงาพิกเซลซึ่งจะประเมินระยะทางที่ใกล้กับพื้นผิวของแต่ละจุดบนหน้าจอมากที่สุด

การอ้างอิงที่ดีเกี่ยวกับอัลกอริทึมจะอยู่ในภาพรวมโดย iq: Rendering Worlds with Two Triangles - Iñigo Quilez นอกจากนี้ คุณยังสำรวจแกลเลอรีของให้เฉดสีใน glsl.heroku.com ได้อีกด้วย มีตัวอย่างเทคนิคนี้จำนวนมากให้นำไปทดลองได้

หัวใจของตัวปรับเฉดสีเริ่มต้นด้วยฟังก์ชันหลัก โดยจะตั้งค่าการแปลงกล้องและเข้าสู่ลูป ซึ่งจะประเมินระยะทางไปยังพื้นผิวซ้ำๆ การเรียกใช้ RaytraceFoggy( Directions_ector, max_iterations, color, color_multiplier ) คือการคำนวณแกนรังสีหลัก

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;
}

งานที่เกี่ยวข้องกับการสร้างตัวปรับแสงเงาประเภทนี้มีความยุ่งยาก นอกจากปัญหาเกี่ยวกับนามธรรมของการดำเนินการที่คุณสร้างขึ้นแล้ว ยังมีการเพิ่มประสิทธิภาพที่ร้ายแรงและปัญหาความเข้ากันได้ข้ามแพลตฟอร์มที่คุณต้องติดตามและแก้ไขก่อนจึงจะนำงานดังกล่าวไปใช้จริงได้

ส่วนแรกของปัญหา: ปรับให้เฉดสีนี้เหมาะกับฉากของเรา ในการรับมือกับปัญหานี้ เราจำเป็นต้องใช้วิธี "ที่ปลอดภัย" ในกรณีที่ตัวปรับแสงเงาหนักเกินไป ในการดำเนินการนี้ เราได้ผสมตัวปรับแสงสีพายุทอร์นาโดโดยใช้ความละเอียดจากการสุ่มตัวอย่างที่แตกต่างจากส่วนที่เหลือของฉาก ไฟล์นี้มาจากไฟล์ พายุTest.coffee (ใช่แล้ว นี่เป็นการทดสอบ)

เราเริ่มด้วยRenderTarget ที่ตรงกับความกว้างและความสูงของฉาก เพื่อให้เราสามารถปรับความละเอียดของตัวเฉดสีทอร์นาโดไปยังฉากได้ จากนั้นเราจะตัดสินใจว่าจะลดการสุ่มตัวอย่างความละเอียดของตัวปรับแสงเงาพายุแบบไดนามิกโดยขึ้นอยู่กับอัตราเฟรมที่เราได้รับ

...
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 ที่เรียบง่าย (เพื่อหลีกเลี่ยงภาพทอร์นาโด) @line 1107 ใน forceTest.coffee ซึ่งหมายความว่ากรณีที่แย่กว่านั้น เราอาจเกิดพายุทอร์นาโดที่พร่ามัวมากขึ้น แต่อย่างน้อยก็ยังคงได้ผลโดยไม่ควบคุมผู้ใช้

ขั้นตอนต่อไปในการเพิ่มประสิทธิภาพ คุณจะต้องเจาะลึกอัลกอริทึม ปัจจัยในการคำนวณการขับเคลื่อนในตัวปรับแสงเงาคือ การทำซ้ำที่ดำเนินการกับแต่ละพิกเซลเพื่อพยายามประมาณระยะห่างของฟังก์ชันพื้นผิว ซึ่งเป็นจำนวนการทำซ้ำของลูปเรย์มาร์ช เมื่อใช้ระยะที่ใหญ่กว่า เราก็อาจได้รับค่าประมาณพื้นผิวของพายุทอร์นาโดโดยทำซ้ำน้อยลงในช่วงที่เราอยู่นอกพื้นผิวที่มีเมฆมาก เมื่ออยู่ภายในเราจะลดขนาดขั้นตอนเพื่อความแม่นยําและเพื่อให้ผสมค่าเพื่อสร้างเอฟเฟกต์หมอกได้ และการสร้างกระบอกล้อมรอบเพื่อหาความลึกโดยประมาณของรังสีที่แคสต์ทำให้เร่งความเร็วได้เป็นอย่างดี

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

ปัญหาความเข้ากันได้ของครอสวิดีโอมีวิธีแก้ปัญหาที่คล้ายกัน คือ ตรวจสอบให้แน่ใจว่าได้ป้อนค่าคงที่ของประเภทข้อมูลที่ถูกต้องตามที่กำหนดไว้ IE: 0.0 สำหรับทศนิยม และ 0 สำหรับ int โปรดระมัดระวังเมื่อเขียนฟังก์ชันยาวๆ ควรให้ค่าในฟังก์ชันที่ง่ายขึ้นและตัวแปรชั่วคราวหลายรายการ เนื่องจากคอมไพเลอร์ไม่สามารถจัดการบางกรณีได้อย่างถูกต้อง ตรวจสอบว่าพื้นผิวยกกำลัง 2 ทั้งหมด ไม่ใหญ่เกินไป และในกรณีใดก็ตาม ให้ใช้ "ข้อควรระวัง" เมื่อค้นหาข้อมูลพื้นผิวแบบวนซ้ำ

ปัญหาใหญ่ที่สุดที่เราพบในความเข้ากันได้คือจากเอฟเฟกต์แสงของพายุ เราใช้พื้นผิวสำเร็จรูปพันรอบพายุทอร์นาโดเพื่อใส่สีที่ฉูดฉาดของพายุ เทคนิคนี้สวยงามมาก แถมช่วยทำให้พายุทอร์นาโดกลมกลืนไปกับสีของฉากได้ง่ายๆ แต่ใช้เวลานานในการเล่นบนแพลตฟอร์มอื่น

ทอร์นาโด

เว็บไซต์บนอุปกรณ์เคลื่อนที่

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

เราคิดว่าคงจะดีไม่น้อยหากเราได้นำบูธรูปภาพงานคาร์นิวัลจากเดสก์ท็อปมาใช้เป็นเว็บแอปพลิเคชันบนอุปกรณ์เคลื่อนที่ โดยใช้กล้องมือถือของผู้ใช้ สิ่งที่เราไม่เคยเห็นมาก่อน

เราเขียนโค้ดการเปลี่ยนรูปแบบ 3 มิติไว้ใน CSS3 เพื่อเพิ่มรสชาติ หลังจากเชื่อมโยงกับเครื่องวัดการหมุนและตัวตรวจวัดความเร่งแล้ว เราก็ได้เพิ่มประสบการณ์ลงไปได้อย่างมาก เว็บไซต์ตอบสนองต่อการจับ ขยับ และมองโทรศัพท์

เมื่อเขียนบทความนี้ เราคิดว่าคุณควรมอบคำแนะนำเกี่ยวกับวิธีดำเนินกระบวนการพัฒนาอุปกรณ์เคลื่อนที่อย่างราบรื่น มาแล้ว! ลองเริ่มดูกันเลยว่าคุณจะได้เรียนรู้อะไรจากโลกนั้นบ้าง

กลเม็ดเคล็ดลับสำหรับอุปกรณ์เคลื่อนที่

ตัวโหลดล่วงหน้าเป็นสิ่งที่จำเป็นต้องใช้ ไม่ใช่สิ่งที่ควรหลีกเลี่ยง เราทราบดีว่าบางครั้งกรณีหลังเกิดขึ้น สาเหตุหลักคือคุณต้องคอยดูแลจัดการรายการต่างๆ ที่โหลดไว้ให้ล่วงหน้าเมื่อโครงการเติบโตขึ้น ที่แย่กว่านั้นคือ เรายังไม่ทราบแน่ชัดว่าคุณควรคำนวณความคืบหน้าในการโหลดอย่างไรหากคุณดึงทรัพยากรแตกต่างกัน และยังใช้ทรัพยากรจำนวนมากในเวลาเดียวกัน ซึ่งเป็นที่มาของ 'งาน' คลาสนามธรรมทั่วไปของเรา แนวคิดหลักของโปรแกรมนี้คือการอนุญาตให้มีโครงสร้างที่ซ้อนกันอย่างไม่มีที่สิ้นสุด โดยที่งานสามารถมีงานย่อยของตัวเองได้ หรืออาจมีรายการอื่นๆ... นอกจากนี้ แต่ละงานยังคำนวณความคืบหน้าของงานย่อยตามความคืบหน้าของงานย่อยด้วย (ไม่ใช่ความคืบหน้าของงานย่อย) ในการทำให้ MainPreloadTask, AssetPreloadTask และ TemplatePreFetchTask ทั้งหมดมาจากงาน เราได้สร้างโครงสร้างที่มีลักษณะดังนี้

ตัวโหลดล่วงหน้า

วิธีการและคลาสงานเหล่านี้ช่วยให้เราทราบความคืบหน้าโดยรวม (MainPreloadTask) หรือความคืบหน้าของชิ้นงาน (AssetPreloadTask) หรือความคืบหน้าของการโหลดเทมเพลต (TemplatePreFetchTask) ได้อย่างง่ายดาย ความคืบหน้าของไฟล์บางไฟล์ ดูวิธีการทำงานได้ที่คลาสงานที่ /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 (ผ่านการติดตั้งใช้งานงานที่แชร์) แล้ว คุณควรสังเกตวิธีที่เราโหลดชิ้นงานที่ขึ้นอยู่กับแพลตฟอร์มด้วย โดยพื้นฐานแล้ว เรามีรูปภาพ 4 ประเภท มาตรฐานสำหรับอุปกรณ์เคลื่อนที่ (.ext) ที่ส่วนขยายคือนามสกุลไฟล์ ซึ่งมักเป็น .png หรือ .jpg), Mobile retina (-2x.ext), มาตรฐานของแท็บเล็ต (-tab.ext) และหน้าจอเรตินาของแท็บเล็ต (-tab-2x.ext) แทนที่จะทำการตรวจหาใน MainPreloadTask และฮาร์ดโค้ดอาร์เรย์เนื้อหา 4 รายการ เราเพียงแค่บอกว่าชื่อและส่วนขยายของเนื้อหาที่จะโหลดล่วงหน้าคืออะไรและเนื้อหานั้นขึ้นอยู่กับแพลตฟอร์มหรือไม่ (ปรับเปลี่ยนตามพื้นที่โฆษณา = จริง / เท็จ) จากนั้น 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 Photo Booth (iOS6/Android)

ตอนที่พัฒนา OZ Mobile เราพบว่าเราใช้เวลามากมายไปกับการเล่นเกมในตู้ถ่ายภาพ :D เรื่องนี้สนุกมาก เราจึงสร้างเดโมให้คุณลองเล่น

ตู้ถ่ายภาพบนมือถือ
บูธถ่ายรูปในอุปกรณ์เคลื่อนที่

คุณสามารถดูการสาธิตสดได้ที่นี่ (เรียกใช้บนโทรศัพท์ iPhone หรือ Android)

http://u9html5rocks.appspot.com/demos/mobile_photo_booth

ในการตั้งค่า คุณต้องมีอินสแตนซ์แอปพลิเคชันฟรีของ Google App Engine ซึ่งคุณสามารถเรียกใช้แบ็กเอนด์ได้ โค้ดส่วนหน้าไม่ซับซ้อน แต่มี Gote ที่เป็นไปได้ 2 รายการ มาดูกันทีละข้อ

  1. ประเภทไฟล์รูปภาพที่อนุญาต เราต้องการให้ผู้ใช้อัปโหลดรูปภาพได้เท่านั้น (เนื่องจากเป็นบูธถ่ายภาพ ไม่ใช่บูธวิดีโอ) ในทางทฤษฎี คุณเพียงแค่ระบุตัวกรองใน 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();
     }
   }
   });
  1. การยกเลิกการอัปโหลดหรือการเลือกไฟล์ ความไม่สอดคล้องกันอีกอย่างที่เราพบในขั้นตอนการพัฒนาคือการที่อุปกรณ์ต่างๆ แจ้งเตือนการเลือกไฟล์ที่ถูกยกเลิก โทรศัพท์และแท็บเล็ต 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 ที่ลิงก์นี้

เครดิต

คลิกที่นี่เพื่อดูรายการเครดิตทั้งหมด

รายการอ้างอิง