Dot Spheres อันน่าทึ่งด้วย WebGL
ลูกโลก WebGL ที่สวยงามและโต้ตอบได้มีช่วงเวลาที่น่าสนใจเมื่อเร็วๆ นี้ โดยทั้งStripeและGitHubนำเสนอสิ่งเหล่านี้อย่างเด่นชัดในหน้าแรกของพวกเขา หลังจากนั้นแต่ละคนก็เขียนบล็อกโพสต์เกี่ยวกับวิธีการทำเช่นนั้น (Stripe อยู่ที่นี่และ GitHub อยู่ที่นี่หากคุณสงสัย)
ลูกโลกทั้งสองประกอบด้วยจุดเป็นหลัก ซึ่งทำให้ฉันคิดถึงวิธีต่างๆ ที่จุดสามารถกระจายไปทั่วพื้นผิวของทรงกลมได้ การบรรจุ Sphere เป็นปริศนาที่ซับซ้อนภายใต้การพิจารณาอย่างแข็งขันโดยนักคณิตศาสตร์ ดังนั้นสำหรับจุดประสงค์ของบทความนี้ ฉันได้จำกัดตัวเองให้วางแนวทางพื้นฐานสองสามข้อและวิธีทำให้สำเร็จใน WebGL
การจัดฉาก
ก่อนที่จะดำเนินการต่อไป เราต้องสร้างฉาก WebGL พื้นฐานสำหรับสร้างทรงกลม ฉันใช้Three.jsเป็นเฟรมเวิร์กจริงที่เลือกสำหรับการโต้ตอบกับ WebGL API ฉันจะตั้งใจรักษาโค้ดในบทความนี้ให้กระชับและตรงประเด็นมากที่สุด สำรวจ Sandboxes ที่ฝังไว้สำหรับโค้ดแบบเต็ม
หลังจากสร้างฉากแล้ว เราสร้างdotGeometries
อาร์เรย์ซึ่งในท้ายที่สุดจะมีรูปทรงเรขาคณิตสำหรับจุดทั้งหมดของเรา จากนั้นเราจะสร้างเวกเตอร์เปล่า ซึ่งเป็นจุด 3 มิติในอวกาศภายในฉาก ซึ่งตำแหน่งจะถูกกำหนดใหม่ทุกครั้งที่เราสร้างจุด
// Set up the scene.
const scene = new THREE.Scene();
// Define an array to hold the geometries of all the dots.
const dotGeometries = [];
// Create a blank vector to be used by the dots.
const vector = new THREE.Vector3();
// We'll create and position the dots here!
// Merge all the dot geometries together into one buffer geometry.
const mergedDotGeometries = BufferGeometryUtils.mergeBufferGeometries(
dotGeometries
);
// Define the material for the dots.
const dotMaterial = new THREE.MeshBasicMaterial({
color: DOT_COLOR,
side: THREE.DoubleSide
});
// Create the dot mesh from the dot geometries and material.
const dotMesh = new THREE.Mesh(mergedDotGeometries, dotMaterial);
// Add the dot mesh to the scene.
scene.add(dotMesh);
วิธีการพื้นฐาน
วิธีที่ง่ายที่สุดในการเพิ่มจุดลงในทรงกลมคือกำหนดจำนวนเส้นละติจูดและเส้นลองจิจูดที่เราต้องการให้ทรงกลมมี จากนั้นกระจายจุดไปตามจุดเหล่านั้น มีสิ่งสำคัญสองสามข้อที่ควรทราบที่นี่
ประการแรก เรากำลังกำหนดphi
และtheta
มุมสำหรับแต่ละจุด มุมเหล่านี้เป็นส่วนหนึ่งของระบบพิกัดทรงกลม ซึ่งเป็นระบบสำหรับกำหนดตำแหน่งของจุดในพื้นที่ 3 มิติที่สัมพันธ์กับจุดกำเนิด (ซึ่งในกรณีของเราคือจุดศูนย์กลางของทรงกลมของเรา)
ประการที่สองphi
และtheta
ทั้งคู่วัดเป็นเรเดียนไม่ใช่องศา กุญแจสำคัญคือต้องจำไว้ว่ามี π เรเดียน ใน180º เพื่อหาตรงphi
นี้ สิ่งที่เราต้องทำคือหาร π ด้วยจำนวนเส้นละติจูด แต่ในการหาtheta
เราต้องหาร2 * π
ด้วยจำนวนเส้นลองจิจูด เนื่องจากเราต้องการให้เส้นลองจิจูดของเราเดินต่อไปจนครบ 360 องศาของทรงกลม
// Loop across the latitudes.
for (let lat = 0; lat < LATITUDE_COUNT; lat += 1) {
// Loop across the longitudes.
for (let lng = 0; lng < LONGITUDE_COUNT; lng += 1) {
// Create a geomtry for the dot.
const dotGeometry = new THREE.CircleGeometry(DOT_SIZE, 5);
// Defin the phi and theta angles for the dot.
const phi = (Math.PI / LATITUDE_COUNT) * lat;
const theta = ((2 * Math.PI) / LONGITUDE_COUNT) * lng;
// Set the vector using the spherical coordinates generated from the sphere radius, phi and theta.
vector.setFromSphericalCoords(SPHERE_RADIUS, phi, theta);
// Make sure the dot is facing in the right direction.
dotGeometry.lookAt(vector);
// Move the dot geometry into position.
dotGeometry.translate(vector.x, vector.y, vector.z);
// Push the positioned geometry into the array.
dotGeometries.push(dotGeometry);
}
}
หากคุณโต้ตอบกับทรงกลมเพื่อหมุน คุณจะสังเกตได้ว่าวงแหวนที่อยู่ด้านบนและด้านล่างนั้นแน่นกว่าวงแหวนที่อยู่ตรงกลางมาก เนื่องจากเราไม่ได้เปลี่ยนจำนวนจุดในแต่ละเส้นละติจูด นี่คือที่มาของการบรรจุทรงกลม
วิธีการไฟโตแทกซิส
หากคุณเคยดูที่หัวดอกทานตะวันหรือฐานของโคนต้นสน คุณจะสังเกตเห็นรูปแบบที่แปลกตาและโดดเด่น รูปแบบนี้สร้างขึ้นโดยการจัดเรียงตามลำดับฟีโบนัชชี เรียกว่า phyllotaxis เราสามารถใช้ที่นี่เพื่อวางตำแหน่งจุดต่างๆ ในลักษณะที่ปรากฏโดยเว้นระยะห่างเท่าๆ กันบนพื้นผิวทรงกลม
คราวนี้ แทนที่จะกำหนดจำนวนเส้นละติจูดและลองจิจูด เราแค่กำหนดจำนวนจุดทั้งหมดที่เราต้องการให้ปรากฏบนทรงกลม แทนที่จะวนซ้ำไปตามเส้นละติจูด จุดต่างๆ จะแสดงเป็นรูปก้นหอยเดียวต่อเนื่องจากขั้วหนึ่งของทรงกลมไปยังอีกขั้วหนึ่ง
// Loop across the number of dots.
for (let dot = 0; dot < DOT_COUNT; dot += 1) {
// Create a geomtry for the dot.
const dotGeometry = new THREE.CircleGeometry(DOT_SIZE, 5);
// Work out the spherical coordinates of each dot, in a phyllotaxis pattern.
const phi = Math.acos(-1 + (2 * dot) / DOT_COUNT);
const theta = Math.sqrt(DOT_COUNT * Math.PI) * phi;
// Set the vector using the spherical coordinates generated from the sphere radius, phi and theta.
vector.setFromSphericalCoords(SPHERE_RADIUS, phi, theta);
...
}
นี้น่าพอใจกว่ามาก แต่ถ้าเราต้องการรวมจุดให้เท่ากันที่สุดเท่าที่จะเป็นไปได้ แต่ยังมีอิสระในการกำหนดจำนวนเส้นละติจูด
วิธีการเชิงเส้น
ครั้งนี้เราจะกำหนดจำนวนเส้นละติจูดที่ต้องการ แต่จำนวนจุดจะปรับขนาดตามเส้นรอบวงของเส้นละติจูดที่ตำแหน่งนั้นอยู่ เพื่อให้เราควบคุมระยะห่างได้มากขึ้น เราจะกำหนดพารามิเตอร์ความหนาแน่นของจุดด้วย
ส่วนที่ยุ่งเหยิงในที่นี้คือการคำนวณรัศมีของเส้นละติจูดแต่ละเส้น เมื่อได้ข้อมูลนั้นแล้ว ก็ค่อนข้างง่ายที่จะคิดออกว่าจะแสดงกี่จุด จากนั้นค้นหาphi
และtheta
สำหรับแต่ละจุดในลักษณะที่คล้ายกับวิธีแรก
// Loop across the latitude lines.
for (let lat = 0; lat < LATITUDE_COUNT; lat += 1) {
// Calculate the radius of the latitude line.
const radius =
Math.cos((-90 + (180 / LATITUDE_COUNT) * lat) * (Math.PI / 180)) *
SPHERE_RADIUS;
// Calculate the circumference of the latitude line.
const latitudeCircumference = radius * Math.PI * 2 * 2;
// Calculate the number of dots required for the latitude line.
const latitudeDotCount = Math.ceil(latitudeCircumference * DOT_DENSITY);
// Loop across the dot count for the latitude line.
for (let dot = 0; dot < latitudeDotCount; dot += 1) {
const dotGeometry = new THREE.CircleGeometry(DOT_SIZE, 5);
// Calculate the phi and theta angles for the dot.
const phi = (Math.PI / LATITUDE_COUNT) * lat;
const theta = ((2 * Math.PI) / latitudeDotCount) * dot;
...
}
}
ดังนั้นเราจึงกล่าวถึงวิธีการแสดงจุดบนทรงกลม แต่สิ่งที่เกี่ยวกับการบรรลุผลที่ซับซ้อนมากขึ้น?
การกำบังรูปร่าง
การหาวิธีแสดงจุดในรูปแบบที่ซับซ้อนมากขึ้นอาจทำให้ปวดหัวทางคณิตศาสตร์ได้อย่างรวดเร็ว อย่างไรก็ตาม ด้วยการใช้หนึ่งในการจัดบรรจุภัณฑ์ข้างต้นร่วมกับภาพมาสก์ เราสามารถสร้างเอฟเฟกต์พิเศษบางอย่างได้
ในการทำเช่นนี้ ก่อนอื่นเราต้องสร้างองค์ประกอบผ้าใบ HTML และวาดภาพมาสก์บนองค์ประกอบนั้น องค์ประกอบนี้จะไม่แสดงผลบนหน้าจอจริงๆ เป็นเพียงวิธีที่สะดวกในการดึงข้อมูลพิกเซลออกจากรูปภาพ เราจำเป็นต้องทำเพียงครั้งเดียวเท่านั้น ดังนั้นเราจะดำเนินการล่วงหน้า แล้วจึงส่งข้อมูลรูปภาพที่แยกออกมาไปยังrenderScene
ฟังก์ชัน ของเรา
// Initialise an image loader.
const imageLoader = new THREE.ImageLoader();
// Load the image used to determine where dots are displayed. The sphere
// cannot be initialised until this is complete.
imageLoader.load(MASK_IMAGE, (image) => {
// Create an HTML canvas, get its context and draw the image on it.
const tempCanvas = document.createElement("canvas");
tempCanvas.width = image.width;
tempCanvas.height = image.height;
const ctx = tempCanvas.getContext("2d");
ctx.drawImage(image, 0, 0);
// Read the image data from the canvas context.
const imageData = ctx.getImageData(0, 0, image.width, image.height);
renderScene(imageData);
});
// Utility function to convert a dot on a sphere into a UV point on a
// rectangular texture or image.
const spherePointToUV = (dotCenter, sphereCenter) => {
// Create a new vector and give it a direction from the center of the sphere
// to the center of the dot.
const newVector = new THREE.Vector3();
newVector.subVectors(sphereCenter, dotCenter).normalize();
// Calculate the UV coordinates of the dot and return them as a vector.
const uvX = 1 - (0.5 + Math.atan2(newVector.z, newVector.x) / (2 * Math.PI));
const uvY = 0.5 + Math.asin(newVector.y) / Math.PI;
return new THREE.Vector2(uvX, uvY);
};
// Utility function to sample the data of an image at a given point. Requires
// an imageData object.
const sampleImage = (imageData, uv) => {
// Calculate and return the data for the point, from the UV coordinates.
const point =
4 * Math.floor(uv.x * imageData.width) +
Math.floor(uv.y * imageData.height) * (4 * imageData.width);
return imageData.data.slice(point, point + 4);
};
// Move the dot geometry into position.
dotGeometry.translate(vector.x, vector.y, vector.z);
// Find the bounding sphere of the dot.
dotGeometry.computeBoundingSphere();
// Find the UV position of the dot on the land image.
const uv = spherePointToUV(
dotGeometry.boundingSphere.center,
new THREE.Vector3()
);
// Sample the pixel on the land image at the given UV position.
const sampledPixel = sampleImage(imageData, uv);
// If the pixel contains a color value (in other words, is not transparent),
// continue to create the dot. Otherwise don't bother.
if (sampledPixel[3]) {
// Push the positioned geometry into the array.
dotGeometries.push(dotGeometry);
}
เราสามารถใช้ภาพมาสก์ที่ซับซ้อนมากขึ้นเพื่อให้ได้รูปทรงต่างๆ เช่น เอฟเฟกต์โลกนี้:
หรือแม้กระทั่งการแสดงข้อความ:
นั่นคือห่อ
ฉันใช้เทคนิคการทำแผนที่ทรงกลมเหล่านี้ในที่ต่างๆ เป็นพื้นฐานของ WebGL ที่เชิดหน้าชูตา หวังว่าพวกเขาจะเป็นแรงบันดาลใจให้คุณทำเช่นเดียวกัน หากคุณชอบบทความนี้หรือมีประโยชน์กับคุณในทางใดทางหนึ่ง โปรดแจ้งให้เราทราบ! เว็บไซต์ของฉันอยู่ที่นี่