ทำดาว 100,000 ดวง

สวัสดี ฉันชื่อ Michael Chang และทำงานร่วมกับทีม Data Arts ที่ Google เมื่อเร็วๆ นี้ เราได้สร้าง 100,000 Stars ซึ่งเป็นการทดลองของ Chrome ที่แสดงภาพดาวที่อยู่ใกล้เคียง โปรเจ็กต์นี้สร้างขึ้นด้วย THREE.js และ CSS3D ในกรณีศึกษานี้ ฉันจะอธิบายกระบวนการค้นพบ แชร์เทคนิคการเขียนโปรแกรมบางอย่าง และปิดท้ายด้วยแนวคิดบางอย่างสำหรับการปรับปรุงในอนาคต

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

100,000 Stars ซึ่งเป็นโปรเจ็กต์ทดลองของ Chrome โดยทีม Data Arts
100,000 Stars ใช้ THREE.js เพื่อแสดงภาพดาวที่อยู่ใกล้เคียงในทางช้างเผือก

การค้นพบ Space

หลังจากทำ Small Arms Globe เสร็จไม่นาน ผมก็ทดลองใช้เดโมอนุภาค THREE.js ที่มีระยะชัดลึก ฉันสังเกตเห็นว่าสามารถเปลี่ยน "สเกล" ที่ตีความของฉากได้โดยการปรับปริมาณเอฟเฟกต์ที่ใช้ เมื่อเอฟเฟกต์ระยะชัดลึกมีความชัดเจนมาก วัตถุที่อยู่ไกลจะเบลอมาก ซึ่งคล้ายกับวิธีที่การถ่ายภาพแบบทิลต์ชิฟต์ทำงานเพื่อสร้างภาพลวงตาให้ดูเหมือนกำลังมองฉากขนาดเล็ก ในทางกลับกัน การลดเอฟเฟกต์จะทำให้ดูเหมือนว่าคุณกำลังมองไปยังห้วงอวกาศอันไกลโพ้น

ฉันเริ่มค้นหาข้อมูลที่ใช้เพื่อแทรกตำแหน่งอนุภาคได้ ซึ่งนำไปสู่ฐานข้อมูล HYG ของ astronexus.com ซึ่งเป็นการรวบรวมแหล่งข้อมูล 3 แหล่ง (Hipparcos, Yale Bright Star Catalog และ Gliese/Jahreiss Catalog) พร้อมด้วยพิกัดคาร์ทีเซียน xyz ที่คำนวณไว้ล่วงหน้า มาเริ่มกันเลย

การลงจุดข้อมูลดาว
ขั้นตอนแรกคือการพล็อตดาวทุกดวงในแคตตาล็อกเป็นอนุภาคเดียว
ดาวฤกษ์ที่มีชื่อ
ดาราบางคนในแคตตาล็อกมีชื่อที่ถูกต้อง ซึ่งเราได้ติดป้ายกำกับไว้ที่นี่

ใช้เวลาประมาณ 1 ชั่วโมงในการแฮ็กเพื่อสร้างสิ่งที่จะวางข้อมูลดาวในพื้นที่ 3 มิติ ชุดข้อมูลมีดาวทั้งหมด 119,617 ดวง ดังนั้นการแสดงดาวแต่ละดวงด้วยอนุภาคจึงไม่ใช่ปัญหาสำหรับ GPU รุ่นใหม่ นอกจากนี้ ยังมีดาวที่ระบุได้ทีละดวงอีก 87 ดวง ผมจึงสร้างการวางซ้อนเครื่องหมาย CSS โดยใช้เทคนิคเดียวกับที่อธิบายไว้ใน Small Arms Globe

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

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

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

สร้างกาแล็กซี

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

กาแล็กซีต้นแบบในยุคแรก
ต้นแบบแรกของระบบอนุภาคทางช้างเผือก

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

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

การหาขนาดของกาแล็กซี
หน่วย GL ทุกหน่วยคือปีแสง ในกรณีนี้ ทรงกลมมีเส้นผ่านศูนย์กลาง 110,000 ปีแสง ซึ่งครอบคลุมระบบอนุภาค

ฉันตัดสินใจตั้งแต่เนิ่นๆ ว่าจะแทนหน่วย GL หนึ่งหน่วย ซึ่งก็คือพิกเซลในรูปแบบ 3 มิติด้วยระยะทาง 1 ปีแสง ซึ่งเป็นธรรมเนียมที่รวมตำแหน่งของทุกสิ่งทุกอย่างที่มองเห็นได้ และน่าเสียดายที่ทำให้ฉันเจอปัญหาความแม่นยำที่ร้ายแรงในภายหลัง

อีกอย่างที่ฉันตัดสินใจทำคือการหมุนฉากทั้งหมดแทนที่จะย้ายกล้อง ซึ่งเป็นสิ่งที่ฉันเคยทำในโปรเจ็กต์อื่นๆ มาบ้างแล้ว ข้อดีอย่างหนึ่งคือทุกอย่างจะวางอยู่บน "แท่นหมุน" เพื่อให้การลากเมาส์ไปทางซ้ายและขวาหมุนออบเจ็กต์ที่เป็นปัญหา แต่การซูมเข้าเป็นเพียงการเปลี่ยน camera.position.z

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

วิธีต่างๆ ในการแสดงผลกาแล็กซี
(ด้านบน) กาแล็กซีอนุภาคยุคแรก (ด้านล่าง) อนุภาคที่มาพร้อมกับระนาบรูปภาพ

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

ระบบสุริยะ
ดวงอาทิตย์ที่มีดาวเคราะห์โคจรอยู่รอบๆ และทรงกลมที่แสดงถึงแถบไคเปอร์

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

เราใช้เทคนิคที่คล้ายกันกับโคโรนาของดวงอาทิตย์ แต่จะใช้การ์ดสไปรต์แบบแบนที่หันหน้าเข้าหากล้องเสมอโดยใช้ https://github.com/mrdoob/three.js/blob/master/src/extras/core/Gyroscope.js

การแสดงผล Sol
เวอร์ชันแรกๆ ของ Sun

เปลวสุริยาถูกสร้างขึ้นผ่าน Vertex และ Fragment Shader ที่ใช้กับทอรัส ซึ่งหมุนอยู่รอบขอบของพื้นผิวสุริยา Vertex Shader มีฟังก์ชัน Noise ที่ทำให้การเคลื่อนไหวเป็นแบบหยด

ตรงนี้เองที่ฉันเริ่มพบปัญหาการซ้อนทับกันของวัตถุ 3 มิติเนื่องจากความแม่นยำของ GL ตัวแปรทั้งหมดสำหรับความแม่นยำได้รับการกำหนดไว้ล่วงหน้าใน THREE.js ดังนั้นฉันจึงไม่สามารถเพิ่มความแม่นยำได้อย่างสมจริงโดยไม่ต้องทำงานอย่างหนัก ปัญหาความแม่นยำไม่ได้แย่มากนักเมื่ออยู่ใกล้จุดเริ่มต้น แต่เมื่อเริ่มสร้างแบบจำลองระบบดาวอื่นๆ ปัญหานี้ก็เกิดขึ้น

โมเดลดาว
ต่อมาได้มีการปรับโค้ดสำหรับการแสดงผลดวงอาทิตย์ให้เป็นแบบทั่วไปเพื่อแสดงผลดาวดวงอื่นๆ

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

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

การสร้างแสงสะท้อนจากเลนส์

อำนาจที่ยิ่งใหญ่มาพร้อมกับความรับผิดชอบอันใหญ่ยิ่ง
พลังที่ยิ่งใหญ่มาพร้อมกับความรับผิดชอบอันใหญ่ยิ่ง

ภาพจักรวาลเป็นที่ที่ผมรู้สึกว่าใช้เลนส์แฟลร์มากเกินไปได้ THREE.LensFlare ตอบโจทย์จุดประสงค์นี้ สิ่งที่ฉันต้องทำก็แค่ใส่หกเหลี่ยมแบบอนามอร์ฟิกและเติมกลิ่นอายของ JJ Abrams ลงไป ข้อมูลโค้ดด้านล่างแสดงวิธีสร้างในฉาก

// This function returns a lesnflare THREE object to be .add()ed to the scene graph
function addLensFlare(x,y,z, size, overrideImage){
var flareColor = new THREE.Color( 0xffffff );

lensFlare = new THREE.LensFlare( overrideImage, 700, 0.0, THREE.AdditiveBlending, flareColor );

// we're going to be using multiple sub-lens-flare artifacts, each with a different size
lensFlare.add( textureFlare1, 4096, 0.0, THREE.AdditiveBlending );
lensFlare.add( textureFlare2, 512, 0.0, THREE.AdditiveBlending );
lensFlare.add( textureFlare2, 512, 0.0, THREE.AdditiveBlending );
lensFlare.add( textureFlare2, 512, 0.0, THREE.AdditiveBlending );

// and run each through a function below
lensFlare.customUpdateCallback = lensFlareUpdateCallback;

lensFlare.position = new THREE.Vector3(x,y,z);
lensFlare.size = size ? size : 16000 ;
return lensFlare;
}

// this function will operate over each lensflare artifact, moving them around the screen
function lensFlareUpdateCallback( object ) {
var f, fl = this.lensFlares.length;
var flare;
var vecX = -this.positionScreen.x _ 2;
var vecY = -this.positionScreen.y _ 2;
var size = object.size ? object.size : 16000;

var camDistance = camera.position.length();

for( f = 0; f < fl; f ++ ) {
flare = this.lensFlares[ f ];

flare.x = this.positionScreen.x + vecX * flare.distance;
flare.y = this.positionScreen.y + vecY * flare.distance;

flare.scale = size / camDistance;
flare.rotation = 0;

}
}

วิธีง่ายๆ ในการเลื่อนพื้นผิว

ได้รับแรงบันดาลใจจาก Homeworld
ระนาบ Cartesian เพื่อช่วยในการจัดตำแหน่งตามพื้นที่ในอวกาศ

สำหรับ "ระนาบการวางแนวเชิงพื้นที่" เราได้สร้าง THREE.CylinderGeometry() ขนาดมหึมาและวางไว้ตรงกลางดวงอาทิตย์ หากต้องการสร้าง "คลื่นแสง" ที่แผ่ออกไปด้านนอก ฉันได้แก้ไขออฟเซ็ตของพื้นผิวตามเวลาดังนี้

mesh.material.map.needsUpdate = true;
mesh.material.map.onUpdate = function(){
this.offset.y -= 0.001;
this.needsUpdate = true;
}

map คือพื้นผิวที่เป็นของวัสดุ ซึ่งมีฟังก์ชัน onUpdate ที่คุณเขียนทับได้ การตั้งค่าออฟเซ็ตจะทำให้เท็กซ์เจอร์ "เลื่อน" ไปตามแกนนั้น และการสแปม needsUpdate = true จะบังคับให้ลักษณะการทำงานนี้วนซ้ำ

การใช้แถบสี

ดาวแต่ละดวงมีสีแตกต่างกันตาม "ดัชนีสี" ที่นักดาราศาสตร์กำหนดให้ โดยทั่วไปแล้ว ดาวสีแดงจะเย็นกว่า และดาวสีน้ำเงิน/ม่วงจะร้อนกว่า การไล่ระดับสีนี้มีแถบสีขาวและสีส้มระดับกลาง

เมื่อแสดงผลดาว ฉันต้องการให้แต่ละอนุภาคมีสีของตัวเองตามข้อมูลนี้ วิธีทำคือใช้ "แอตทริบิวต์" ที่กำหนดให้กับวัสดุ Shader ที่ใช้กับอนุภาค

var shaderMaterial = new THREE.ShaderMaterial( {
uniforms: datastarUniforms,
attributes: datastarAttributes,
/_ ... etc _/
});
var datastarAttributes = {
size: { type: 'f', value: [] },
colorIndex: { type: 'f', value: [] },
};

การกรอกข้อมูลอาร์เรย์ colorIndex จะทำให้แต่ละอนุภาคมีสีที่ไม่ซ้ำกันใน Shader โดยปกติแล้วเราจะส่ง vec3 ของสี แต่ในกรณีนี้ฉันจะส่ง float เพื่อให้ได้การค้นหา Color Ramp ในท้ายที่สุด

การไล่ระดับสี
การไล่ระดับสีที่ใช้เพื่อค้นหาสีที่มองเห็นได้จากดัชนีสีของดาวฤกษ์

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

// make a blank canvas, sized to the image, in this case gradientImage is a dom image element
gradientCanvas = document.createElement('canvas');
gradientCanvas.width = gradientImage.width;
gradientCanvas.height = gradientImage.height;

// draw the image
gradientCanvas.getContext('2d').drawImage( gradientImage, 0, 0, gradientImage.width, gradientImage.height );

// a function to grab the pixel color based on a normalized percentage value
gradientCanvas.getColor = function( percentage ){
return this.getContext('2d').getImageData(percentage \* gradientImage.width,0, 1, 1).data;
}

จากนั้นจะใช้วิธีเดียวกันนี้ในการระบายสีดาวแต่ละดวงในมุมมองโมเดลดาว

แสบตา!
เทคนิคเดียวกันนี้ใช้ในการค้นหาสีสำหรับประเภทสเปกตรัมของดาวฤกษ์

การจัดการ Shader

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

// list of shaders we'll load
var shaderList = ['shaders/starsurface', 'shaders/starhalo', 'shaders/starflare', 'shaders/galacticstars', /*...etc...*/];

// a small util to pre-fetch all shaders and put them in a data structure (replacing the list above)
function loadShaders( list, callback ){
var shaders = {};

var expectedFiles = list.length \* 2;
var loadedFiles = 0;

function makeCallback( name, type ){
return function(data){
if( shaders[name] === undefined ){
shaders[name] = {};
}

    shaders[name][type] = data;

    //  check if done
    loadedFiles++;
    if( loadedFiles == expectedFiles ){
    callback( shaders );
    }

};

}

for( var i=0; i<list.length; i++ ){
var vertexShaderFile = list[i] + '.vsh';
var fragmentShaderFile = list[i] + '.fsh';

//  find the filename, use it as the identifier
var splitted = list[i].split('/');
var shaderName = splitted[splitted.length-1];
$(document).load( vertexShaderFile, makeCallback(shaderName, 'vertex') );
$(document).load( fragmentShaderFile,  makeCallback(shaderName, 'fragment') );

}
}

ฟังก์ชัน loadShaders() จะรับรายการชื่อไฟล์ Shader (คาดหวัง .fsh สำหรับ Fragment และ .vsh สำหรับ Vertex Shader) พยายามโหลดข้อมูล แล้วแทนที่รายการด้วยออบเจ็กต์ ผลลัพธ์สุดท้ายจะอยู่ใน THREE.js uniforms ซึ่งคุณสามารถส่งผ่าน Shader ไปยัง THREE.js uniforms ได้ดังนี้

var galacticShaderMaterial = new THREE.ShaderMaterial( {
vertexShader: shaderList.galacticstars.vertex,
fragmentShader: shaderList.galacticstars.fragment,
/_..._/
});

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

ป้ายกำกับข้อความ CSS บน THREE.js

ในโปรเจ็กต์ล่าสุดของเราอย่าง Small Arms Globe ฉันได้ลองทำให้ป้ายกำกับข้อความปรากฏที่ด้านบนของฉาก THREE.js วิธีที่ฉันใช้จะคำนวณตำแหน่งโมเดลสัมบูรณ์ของตำแหน่งที่ต้องการให้ข้อความปรากฏ จากนั้นจะแปลงตำแหน่งหน้าจอโดยใช้ THREE.Projector() และสุดท้ายจะใช้ CSS "top" และ "left" เพื่อวางองค์ประกอบ CSS ในตำแหน่งที่ต้องการ

การทำซ้ำในช่วงแรกๆ ของโปรเจ็กต์นี้ใช้วิธีเดียวกันนี้ แต่ฉันก็อยากลองวิธีอื่นที่ Luis Cruz อธิบายไว้

แนวคิดพื้นฐานคือการจับคู่การแปลงเมทริกซ์ของ CSS3D กับกล้องและฉากของ THREE แล้วคุณจะ "วาง" องค์ประกอบ CSS ใน 3 มิติได้ราวกับว่าอยู่เหนือฉากของ THREE อย่างไรก็ตาม ฟีเจอร์นี้มีข้อจำกัด เช่น คุณจะวางข้อความไว้ใต้ออบเจ็กต์ THREE.js ไม่ได้ ซึ่งยังคงเร็วกว่าการพยายามจัดเลย์เอาต์โดยใช้แอตทริบิวต์ CSS "top" และ "left" มาก

ป้ายกำกับข้อความ
ใช้การเปลี่ยนรูปแบบ CSS3D เพื่อวางป้ายกำกับข้อความไว้ด้านบน WebGL

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

/_ Fixes the difference between WebGL coordinates to CSS coordinates _/
function toCSSMatrix(threeMat4, b) {
var a = threeMat4, f;
if (b) {
f = [
a.elements[0], -a.elements[1], a.elements[2], a.elements[3],
a.elements[4], -a.elements[5], a.elements[6], a.elements[7],
a.elements[8], -a.elements[9], a.elements[10], a.elements[11],
a.elements[12], -a.elements[13], a.elements[14], a.elements[15]
];
} else {
f = [
a.elements[0], a.elements[1], a.elements[2], a.elements[3],
a.elements[4], a.elements[5], a.elements[6], a.elements[7],
a.elements[8], a.elements[9], a.elements[10], a.elements[11],
a.elements[12], a.elements[13], a.elements[14], a.elements[15]
];
}
for (var e in f) {
f[e] = epsilon(f[e]);
}
return "matrix3d(" + f.join(",") + ")";
}

เนื่องจากทุกอย่างได้รับการเปลี่ยนรูป ข้อความจึงไม่ได้หันหน้าเข้าหากล้องอีกต่อไป วิธีแก้คือใช้ THREE.Gyroscope() ซึ่งบังคับให้ Object3D "สูญเสีย" การวางแนวที่สืบทอดมาจากฉาก เทคนิคนี้เรียกว่า "การแสดงป้ายโฆษณา" และ Gyroscope เหมาะอย่างยิ่งสำหรับการทำเช่นนี้

สิ่งที่ยอดเยี่ยมคือ DOM และ CSS ปกติทั้งหมดก็ยังทำงานร่วมกันได้ เช่น การวางเมาส์เหนือป้ายกำกับข้อความ 3 มิติแล้วทำให้ป้ายกำกับนั้นส่องสว่างพร้อมเงาตกกระทบ

ป้ายกำกับข้อความ
ให้ป้ายกำกับข้อความหันเข้าหากล้องเสมอโดยติดป้ายกำกับกับ THREE.Gyroscope()

เมื่อซูมเข้า ฉันพบว่าการปรับขนาดของตัวอักษรทำให้เกิดปัญหาในการจัดตำแหน่ง อาจเป็นเพราะระยะห่างระหว่างตัวอักษรและระยะขอบของข้อความใช่ไหม ปัญหาอีกอย่างคือข้อความจะกลายเป็นพิกเซลเมื่อซูมเข้า เนื่องจากโปรแกรมแสดงผล DOM จะถือว่าข้อความที่แสดงเป็นสี่เหลี่ยมพื้นผิว ซึ่งเป็นสิ่งที่ควรทราบเมื่อใช้วิธีนี้ เมื่อมองย้อนกลับไป ฉันอาจใช้ข้อความขนาดแบบอักษรขนาดใหญ่ก็ได้ และบางทีนี่อาจเป็นสิ่งที่ควรสำรวจในอนาคต ในโปรเจ็กต์นี้ ฉันยังใช้ป้ายกำกับข้อความตำแหน่ง CSS "top/left" ที่อธิบายไว้ก่อนหน้านี้สำหรับองค์ประกอบขนาดเล็กมากที่มาพร้อมกับดาวเคราะห์ในระบบสุริยะ

การเล่นเพลงและการเล่นวนซ้ำ

เพลงที่เปิดใน "แผนที่กาแล็กซี" ของ Mass Effect แต่งโดย Sam Hulick และ Jack Wall ซึ่งเป็นนักแต่งเพลงของ Bioware และมีอารมณ์แบบที่ฉันอยากให้ผู้เข้าชมได้สัมผัส เราต้องการใส่เพลงในโปรเจ็กต์เพราะคิดว่าเพลงเป็นส่วนสำคัญของบรรยากาศ ซึ่งจะช่วยสร้างความรู้สึกทึ่งและประหลาดใจที่เราพยายามจะสื่อ

Valdean Klump โปรดิวเซอร์ของเราได้ติดต่อ Sam ซึ่งมีเพลงที่ "ถูกตัดทิ้ง" จาก Mass Effect อยู่จำนวนหนึ่ง และเขาก็อนุญาตให้เราใช้เพลงเหล่านั้น แทร็กนี้มีชื่อว่า "In a Strange Land"

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

var musicA = document.getElementById('bgmusicA');
var musicB = document.getElementById('bgmusicB');
musicA.addEventListener('ended', function(){
this.currentTime = 0;
this.pause();
var playB = function(){
musicB.play();
}
// make it wait 15 seconds before playing again
setTimeout( playB, 15000 );
}, false);

musicB.addEventListener('ended', function(){
this.currentTime = 0;
this.pause();
var playA = function(){
musicA.play();
}
// otherwise the music will drive you insane
setTimeout( playA, 15000 );
}, false);

// okay so there's a bit of code redundancy, I admit it
musicA.play();

ส่วนที่ต้องปรับปรุง

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

นอกจากนี้ Ray McClure เพื่อนร่วมงานของเรายังได้ใช้เวลาสร้าง "เสียงอวกาศ" ที่ยอดเยี่ยมซึ่งสร้างขึ้นมาเอง แต่ต้องตัดออกเนื่องจาก Web Audio API ไม่เสถียรและทำให้ Chrome ขัดข้องเป็นครั้งคราว น่าเสียดายที่เกิดเหตุการณ์นี้ขึ้น แต่ก็ทำให้เราคิดถึงเรื่องเสียงมากขึ้นสำหรับการทำงานในอนาคต ณ เวลาที่เขียนบทความนี้ เราได้รับแจ้งว่ามีการแก้ไข Web Audio API แล้ว ดังนั้นจึงเป็นไปได้ว่าตอนนี้ฟีเจอร์นี้จะใช้งานได้แล้ว โปรดคอยติดตามในอนาคต

องค์ประกอบการพิมพ์ที่จับคู่กับ WebGL ยังคงเป็นความท้าทาย และฉันไม่แน่ใจ 100% ว่าสิ่งที่เราทำอยู่นี้เป็นวิธีที่ถูกต้อง แต่ก็ยังรู้สึกเหมือนเป็นการแฮ็กอยู่ดี บางทีในอนาคตเราอาจใช้ THREE เวอร์ชันใหม่ที่มี CSS Renderer ที่กำลังจะเปิดตัวเพื่อเชื่อมโยงทั้ง 2 โลกนี้ให้ดียิ่งขึ้น

เครดิต

ขอขอบคุณ Aaron Koblin ที่อนุญาตให้เราทำโปรเจ็กต์นี้ Jono Brandel สำหรับการออกแบบและการติดตั้งใช้งาน UI ที่ยอดเยี่ยม การจัดรูปแบบข้อความ และการติดตั้งใช้งานทัวร์ Valdean Klump ที่ตั้งชื่อโปรเจ็กต์และเขียนข้อความทั้งหมด Sabah Ahmed ที่ช่วยเคลียร์สิทธิ์การใช้งานหลายตันสำหรับแหล่งข้อมูลและแหล่งที่มาของรูปภาพ Clem Wright ที่ติดต่อบุคคลที่เหมาะสมเพื่อเผยแพร่ Doug Fritz สำหรับความเป็นเลิศด้านเทคนิค George Brower ที่สอน JS และ CSS ให้ฉัน และแน่นอนว่าต้องขอบคุณ Mr. Doob สำหรับ THREE.js

ข้อมูลอ้างอิง