حقیقی دنیا کے نقشے پر ڈیزاسٹر سمولیشن کیسے بنایا جائے: Mapbox GL JS اور Three.js کا استعمال کرتے ہوئے اینی میٹڈ کینوس کی تہوں اور 3D عمارتوں کو اسکیل کرنا

کسی وقت، آپ کے نقشے کے پروجیکٹ کو مارکر اور کثیر الاضلاع سے زیادہ کی ضرورت ہوتی ہے۔ میں نقشے پر کچھ ہونا چاہتا ہوں۔ گلی میں بہتا ہوا پانی، ہوا میں تیرتا ہوا دھواں، ایک نقطہ سے پھیلنے والی صدمے کی لہریں وغیرہ۔

پہلی بار جب میں نے اس کی کوشش کی تو میں نے کچھ ایسا کیا جو مجھے واضح نظر آیا۔ میں ہوں میں نے نقشے پر ایک حرکت پذیری بنائی۔

لیکن یہ اتنا غلط معلوم ہوتا تھا کہ اسے مزید تطہیر کے ساتھ ٹھیک نہیں کیا جا سکتا تھا۔ اثر ایک کنارے تھا. یہ زمین کے اوپر تیر رہا تھا۔ اور جیسے ہی میں نے نقشے کو جھکایا یا گھمایا، نقشے کے نیچے موجود آئٹمز غلط طریقے سے منسلک ہو گئے۔

HazardSim، ایک براؤزر سمیلیٹر جو حقیقی نقشوں پر قدرتی آفات کو چلاتا ہے، بناتے وقت میں اس مسئلے کا شکار ہوا۔ سونامی کا منظرنامہ تقریباً ایک درجن دوبارہ لکھنے سے گزرا یہاں تک کہ یہ کسی شہر پر چسپاں کٹ آؤٹ کی طرح نظر آنا بند ہو گیا۔

زیادہ تر جانچ اور بگ رپورٹس کا ایک اہم حصہ میرے 12 سالہ بیٹے ٹِم نے کیا تھا، جو مجھ سے زیادہ مریض ٹیسٹر نکلا۔ زیادہ تر دوبارہ لکھنے والوں نے مجھے ایک ہی چیز مختلف سمت سے سکھائی ہے۔ اسکرین کوآرڈینیٹ کے بجائے نقشہ کوآرڈینیٹس میں نقالی بنائیں۔

اس اصول نے مجھے سونامی کے ذریعے حاصل کیا۔ پھر، زلزلے کے ساتھ شروع، ہم مخالف مسئلہ میں بھاگ گئے. Mapbox GL JS آپ کے شہر کو 3D میں ڈراتا ہے۔ fill-extrusion تہوں اور ویکٹر ٹائلوں پر تمام قدموں کے نشان اپنی اصل اونچائی تک بڑھتے ہیں۔

یہ اس وقت تک اچھا لگتا ہے جب تک کہ آپ عمارتوں میں سے کسی ایک کو گرا نہیں دیتے، پھر آپ کو احساس ہوتا ہے کہ نقشے پر 3D ایک پیش کرنے والی چال ہے نہ کہ کوئی منظر۔ پکڑنے، گھمانے یا توڑنے کے لیے کچھ نہیں ہے۔ یہ پتہ چلتا ہے کہ زمین پر لیٹنا اور بلند ہونا دو مختلف مہارتوں کی ضرورت ہے، اور یہ گائیڈ دونوں کا احاطہ کرتا ہے۔

حصہ 1 گراؤنڈ فلور بنائیں۔ نقشے کی اپنی ٹائلوں کے ساتھ منسلک ایک گرڈ ترتیب دیں، اصل بلندی کا ڈیٹا لوڈ کریں، سیلاب کا ایک فلڈ فل چلائیں جو پہاڑیوں اور وادیوں کو مدنظر رکھے، کینوس کو نقشے پر ایک راسٹر ماخذ کے طور پر منتقل کریں، ہر فریم پر پکسلز لکھیں، کینوس کمپوزٹنگ موڈ کا استعمال کرتے ہوئے اوپر ویڈیو کی ساخت لگائیں، اور کیمرے کو منظر میں لے جائیں۔

حصہ 2 اونچائی جوڑتا ہے۔ نقشے کی ٹائلوں سے عمارت کے پاؤں کے نشانات اور اونچائیاں حاصل کریں، اوور ہینگز کو عمارتوں کی طرح بنائیں، اس بات کا تعین کریں کہ کون سے سائز اور فاصلے میں گرتے ہیں، تہوں کو دوبارہ لکھے بغیر متحرک گرنا، three.js نقشے کے اپنے WebGL سیاق و سباق کے اندر ایک حسب ضرورت پرت شامل کریں اور قدموں کے نشانات کو گرتے ہوئے خانوں میں تبدیل کریں، ملبہ اور دھول پھینکیں، اور کیمرے کو ہلائیں۔

آخر میں، ہم دونوں حصوں میں ہونے والی غلطیوں کو دیکھیں گے۔ ان میں سے کچھ نے غلطی کے پیغامات پیدا کیے بغیر ہر ایک دن کا وقت لیا۔

شرطیں

آپ کو جاوا اسکرپٹ سے واقف ہونا چاہیے۔ requestAnimationFrame2D کینوس API کی بنیادی باتیں: getContext("2d")، drawImageاور ImageData.

ویب نقشوں کو پس منظر کی معلومات کی ضرورت نہیں ہے۔ نقشہ سے متعلق ہر چیز کی وضاحت کی گئی ہے جہاں یہ ظاہر ہوتا ہے۔

حصہ 2 یہ بھی مانتا ہے کہ آپ نے استعمال کیا ہے: three.js کم از کم ایک بار: میش، مواد، منظر اور کیمرہ۔ بصورت دیگر، صرف three.js کی بنیادی باتوں پر عمل کرنا کافی ہوگا، اور نقشے کے ساتھ تعامل کی تفصیل سے وضاحت کی گئی ہے۔

مثالیں HazardSim پر چلنے والے Mapbox GL JS کا استعمال کرتی ہیں۔ بیس میپ اور ویکٹر ٹائلز کے لیے مفت میپ باکس تک رسائی کا ٹوکن درکار ہے۔ یہاں استعمال ہونے والے تمام APIs ایک اوپن سورس فورک، MapLibre GL JS میں بھی اسی نام سے موجود ہیں۔ دونوں کے درمیان فرق حصہ 2 میں زیر بحث آئے گا۔

انڈیکس

حصہ 1: زمین پر ڈرائنگ

اس گائیڈ کا پہلا نصف نقشے کی سطح سے متعلق ہر چیز کے بارے میں ہے: پانی، آگ کا پھیلاؤ، دھوئیں کے بلاب، شاک ویوز، وغیرہ۔ یہ تکنیک ایک خیال کا محتاط اطلاق ہے، اور اگلے سات مراحل اسے گرڈ سے کیمرے کی نقل و حرکت تک بناتے ہیں۔

کلیدی خیال: نقشہ کی جگہ، اسکرین کی جگہ نہیں۔

ویب کا نقشہ سطح کو دیکھنے والا کیمرہ ہے۔ جب صارف گھسیٹتا ہے، زوم کرتا ہے، جھکتا ہے، یا گھماتا ہے، کیمرہ حرکت کرتا ہے اور سطح ایک جیسی رہتی ہے۔ سڑکیں، عمارتیں، سیلاب وغیرہ سبھی سطحیں ہیں، اس لیے کیمرے کو ان کا ترجمہ کرنا چاہیے۔

نقشے کے DOM عنصر پر رکھا ہوا کینوس کیمرہ سے منسلک ہوتا ہے، سطح سے نہیں۔ اسے عینک کے سامنے شیشے کی پلیٹ کے طور پر سوچیں۔ جو بھی آپ کھینچتے ہیں وہیں رہتا ہے اور دنیا اس کے نیچے چلتی ہے۔

بغیر گھماؤ کے ٹاپ ڈاون زوم میں، آپ ہر فریم میں پوائنٹس کو دوبارہ پروجیکٹ کر کے الائنمنٹ کو جعلی بنا سکتے ہیں، اور یہ تقریباً کام کرتا ہے۔ پچ شامل کریں اور یہ الگ ہوجاتا ہے۔ فلیٹ اسپرائٹس کا کوئی نقطہ نظر نہیں ہے، کوئی پیش گوئی نہیں ہے، اور اس سے پہلے یا بعد میں کچھ نہیں ہے۔

دو خاکے ساتھ ساتھ ہیں۔ بائیں، عنوان

اس کا حل یہ ہے کہ نقشے کو ڈیٹا سورس کے طور پر کینوس فراہم کیا جائے۔ میپ باکس جی ایل جے ایس کے پاس ہے۔ canvas ماخذ کی قسم۔ ہم ایک کینوس کے عنصر اور چار جغرافیائی کونوں سے گزرتے ہیں، پھر ہم کینوس کو مستطیل پر اسی طرح کھینچتے ہیں جس طرح ہم نقشے پر سیٹلائٹ کی تصویر بناتے ہیں۔ نقشے کو گھمانے سے کینوس گھومتا ہے۔ جھکاؤ کینوس کو چھوٹا کرتا ہے۔ جب آپ 3D ٹیرائن کو آن کرتے ہیں تو کینوس پہاڑیوں کی پیروی کرے گا۔

نقشہ کی جگہ میں ڈرائنگ کی سطح رکھنے سے کچھ مشکلات ختم ہوجاتی ہیں۔ اس ہینڈ بک کا باقی حصہ وضاحت کرتا ہے کہ آپ اس کے ساتھ کیا کر سکتے ہیں۔

مرحلہ 1: گرڈ سے منسلک نقشہ کی ٹائلیں منتخب کریں۔

تخروپن ایک گرڈ کی ضرورت ہے. یعنی، 2D خلیوں کی ایک صف، ہر ایک میں بلندی، گہرائی، یا جو بھی ماڈل ٹریک کر رہا ہے۔ ایک فیصلہ جو نیچے کی طرف ہر چیز کو آسان بناتا ہے وہ ہے اس گرڈ کو نقشے پر اپنے ٹائلوں کے اہرام کے ساتھ سیدھ میں لانا۔

ویب نقشہ ویب مرکٹر پروجیکشن کا استعمال کرتا ہے اور اسے 0 سے 22 تک زوم کی سطح پر 256×256 پکسل ٹائلوں میں کاٹا جاتا ہے۔ z دنیا 2^z ٹائلیں چوڑی ہیں۔ اگر ایک گرڈ سیل کسی زوم پر بالکل ایک ٹائل پکسل ہے، تو گرڈ انڈیکس اور طول بلد/ عرض البلد کے درمیان تبدیل کرنے کے لیے چار مختصر فنکشنز کی ضرورت ہوتی ہے، اور ٹائل کے طور پر پیش کیے گئے تمام ڈیٹاسیٹس (بلندی، آبادی، تصاویر) کو دوبارہ نمونے کے بغیر گرڈ میں شامل کیا جاتا ہے۔

یہاں چار تبدیلیاں ہیں: lon2x اور lat2y فریکشنل ٹائل کوآرڈینیٹس لوٹاتا ہے۔ پکسلز حاصل کرنے کے لیے 256 سے ضرب دیں۔

function lon2x(lon, z) {
  return (lon + 180) / 360 * Math.pow(2, z);
}

function lat2y(lat, z) {
  const rad = lat * Math.PI / 180;
  return (1 - Math.log(Math.tan(rad) + 1 / Math.cos(rad)) / Math.PI) / 2 * Math.pow(2, z);
}

function x2lon(x, z) {
  return x / Math.pow(2, z) * 360 - 180;
}

function y2lat(y, z) {
  const n = Math.PI - 2 * Math.PI * y / Math.pow(2, z);
  return 180 / Math.PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)));
}

اگلا، زوم کو منتخب کریں۔ آپ چاہتے ہیں کہ آپ کی دلچسپی کا علاقہ ایک ایسے گرڈ میں فٹ ہو جو ہموار نظر آنے کے لیے کافی ٹھیک ہو، لیکن اتنا چھوٹا ہو کہ فی سیکنڈ میں کم از کم 60 بار دہرایا جا سکے۔ HazardSim زوم 15 سے شروع ہوتا ہے اور نیچے اترتا ہے جب تک کہ باؤنڈنگ باکس تقریباً 620,000 سیلز میں افقی اور عمودی طور پر تقریباً 780 پکسلز فٹ نہ ہو جائے۔

function chooseZoom(bbox) {
  for (let z = 15; z >= 8; z--) {
    const pw = (lon2x(bbox.e, z) - lon2x(bbox.w, z)) * 256;
    const ph = (lat2y(bbox.s, z) - lat2y(bbox.n, z)) * 256;
    if (pw <= 780 && pw * ph <= 620000) return z;
  }
  return 8;
}

زوم منتخب کرنے کے ساتھ، باؤنڈنگ باکس کو پوری ٹائل کے باہر کی طرف کھینچیں۔ وہ مستطیل وہی ہے جو کینوس کا احاطہ کرے گا اور ڈاؤن لوڈ کرنے والی ٹائلیں ہیں:

function gridFor(bbox) {
  const z = chooseZoom(bbox);
  const x0 = Math.floor(lon2x(bbox.w, z)), x1 = Math.floor(lon2x(bbox.e, z));
  const y0 = Math.floor(lat2y(bbox.n, z)), y1 = Math.floor(lat2y(bbox.s, z));
  const nx = x1 - x0 + 1, ny = y1 - y0 + 1;
  const latMid = (bbox.n + bbox.s) / 2;

  return {
    z, x0, y0, nx, ny,
    W: nx * 256,
    H: ny * 256,
    // metres per cell at this latitude and zoom
    cellM: 156543.03392 * Math.cos(latMid * Math.PI / 180) / Math.pow(2, z),
    // exact geographic corners of the snapped rectangle
    corners: {
      w: x2lon(x0, z), e: x2lon(x1 + 1, z),
      n: y2lat(y0, z), s: y2lat(y1 + 1, z),
    },
  };
}

نارنجی ٹائل بارڈر کے ساتھ نقشہ کی ٹائلوں کے 4x3 ترتیب کا خاکہ۔ اندرونی نیلے نقطے والا مستطیل شہر کے باؤنڈنگ باکس کو نشان زد کرتا ہے۔ ایک ٹائل کو چھوٹے سیلز میں تقسیم کیا جاتا ہے، جس میں ایک گرڈ سیل ایک ٹائل پکسل کی نمائندگی کرتا ہے۔ سائیڈ پینل ان وجوہات کی فہرست دیتا ہے کہ ٹائل کی چھانٹی کیوں مددگار ہو سکتی ہے اور تبادلوں کا فارمولہ دکھاتا ہے۔

برقرار رکھنا cellM تقریباً "موج نے 400 میٹر کا سفر کیا" ~ کو "لہر نے 3.8 سیلوں کا سفر کیا" کو موڑنا ضروری ہے۔ اوڈیسا کے لیے، یہاں کے زیادہ تر اسکرین شاٹس میں نمایاں شہر، گرڈ 105 میٹر سیلز کے ساتھ زوم 10 پر ختم ہوتا ہے۔

مرحلہ 2: اصل بلندی کو گرڈ میں لوڈ کریں۔

پہاڑیوں کو نظر انداز کرنے والا سیلاب نیلے رنگ کا بلاب ہے۔ جو چیز حقیقی نقشے پر نقالی کو حقیقی محسوس کرتی ہے وہ اصلی خطہ ہے، اور خطہ مفت ہے۔

AWS اوپن ڈیٹا پروگرام Mapzen کی میزبانی کرتا ہے۔ ٹیریریم بلندی کی ٹائلیں۔ پکسل رنگ میں انکوڈ شدہ اونچائی کے ساتھ ایک سادہ PNG۔

height_m = R * 256 + G + B / 256 - 32768

گرڈ ٹائل کے ساتھ منسلک ہے، لہذا بلندی کو لوڈ کرنے کا مطلب ہے ٹائلوں کو لے جانا، انہیں ایک بڑے کینوس پر ان کی قدرتی پوزیشن میں کھینچنا، اور پکسلز کو دوبارہ پڑھنا اور ڈی کوڈ کرنا۔ کسی بھی چیز کا دوبارہ نمونہ نہیں بنایا گیا ہے۔ گرڈ سیل اور بلندی کے نمونے ایک ہی پکسل ہیں۔

function terrariumUrl(z, x, y) {
  return `https://s3.amazonaws.com/elevation-tiles-prod/terrarium/${z}/${x}/${y}.png`;
}

function loadImage(url) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.crossOrigin = "anonymous";
    img.onload = () => resolve(img);
    img.onerror = reject;
    img.src = url;
  });
}

async function loadElevation(grid) {
  const cv = document.createElement("canvas");
  cv.width = grid.W;
  cv.height = grid.H;
  const cx = cv.getContext("2d", { willReadFrequently: true });

  const jobs = [];
  for (let iy = 0; iy < grid.ny; iy++) {
    for (let ix = 0; ix < grid.nx; ix++) {
      jobs.push(
        loadImage(terrariumUrl(grid.z, grid.x0 + ix, grid.y0 + iy))
          .then(img => cx.drawImage(img, ix * 256, iy * 256))
      );
    }
  }
  await Promise.all(jobs);

  const d = cx.getImageData(0, 0, grid.W, grid.H).data;
  const elev = new Float32Array(grid.W * grid.H);
  for (let i = 0; i < elev.length; i++) {
    const o = i * 4;
    elev[i] = d[o] * 256 + d[o + 1] + d[o + 2] / 256 - 32768;
  }
  return elev;
}

crossOrigin = "anonymous" آپ کو اس کی ضرورت ہے۔ اس کے بغیر، کینوس "میدہ" ہے اور getImageData اسے پھینک دیں ٹیریریم بالٹی صحیح CORS ہیڈر بھیجتی ہے، لہذا یہ تحریری طور پر کام کرتا ہے۔

چونکہ آپ کے پاس پہلے سے ہی میپ باکس ٹوکن ہے، آپ میپ باکس کا اپنا ٹوکن استعمال کر سکتے ہیں۔ ٹیرین-آر جی بی اس کے بجائے ٹائل لگائیں۔ URL ہے: https://api.mapbox.com/v4/mapbox.terrain-rgb/{z}/{x}/{y}.pngraw?access_token=… ڈی کوڈ فارمولا مختلف ہے۔

// Mapbox Terrain-RGB
elev[i] = -10000 + (d[o] * 65536 + d[o + 1] * 256 + d[o + 2]) * 0.1;

HazardSim کنفیگریشن جھنڈوں کا استعمال کرتے ہوئے دونوں کو سپورٹ کرتا ہے اور سوئچ کرتا ہے۔ Terrain-RGB چند اسپائکس کے ساتھ صاف ڈیٹا ہے۔ ٹیریریم فی ٹائل چارج نہیں کرتا ہے۔ ٹیوٹوریل کے لیے، ہم ٹیریریم سے شروع کریں گے۔

اوڈیسا کے ارد گرد ساحل کی ڈی کوڈ شدہ بلندی، خاکستری اونچائی کے نقشے کے طور پر پیش کی گئی ہے، جس میں سمندر گہرا نیلا ہے اور ہائی لینڈز ہلکی ہیں۔ اورنج لائن 4 x 4 ٹیریریم ٹائلوں کی بارڈر کو ایک ساتھ سلی ہوئی دکھاتی ہے۔

دو عملی نوٹ۔ ٹیریریم ڈیٹا میں کبھی کبھار سنگل پکسل اسپائکس ہوتے ہیں، جو 3×3 میڈین پھیلاؤ کے ذریعے تمام پڑوسیوں اور سیلوں کے لیے صاف کیے جاتے ہیں جو چند میٹر سے زیادہ مختلف ہوتے ہیں۔ اور 0 سے نیچے کے خلیات سمندر ہیں۔ تاہم، نیدرلینڈز، نیو اورلینز، اور ڈیتھ ویلی جیسے غیر سمندری علاقوں کو خارج کر دیا گیا ہے۔ میں علاج کرتا ہوں elev <= 0 سیل کو سمندر کے طور پر صرف اس صورت میں نشان زد کیا جاتا ہے جب وہ کسی دوسرے سمندری سیل کے ذریعے نقشے کے کنارے سے جڑا ہو۔ عام کیس کو ہینڈل کرتا ہے۔

مرحلہ 3: کینوس کو نقشہ کے ماخذ سے جوڑیں۔

اب یہ کشش ثقل کے مرکز کو منتقل کرنے کا وقت ہے. ایک گرڈ سائز کینوس بنائیں اور اسے نقشے پر رجسٹر کریں۔ canvas ماخذ وہ مستطیل ہے جس کے چار کونوں کو مرحلہ 1 میں چھین لیا گیا تھا۔


mapboxgl.accessToken = "YOUR_MAPBOX_TOKEN";

const map = new mapboxgl.Map({
  container: "map",
  style: "mapbox://styles/mapbox/satellite-streets-v12",
  center: [30.73, 46.48],
  zoom: 11,
});

const fc = document.getElementById("floodcanvas");
const fctx = fc.getContext("2d");
let fimg = null;

function attachCanvas(grid) {
  fc.width = grid.W;
  fc.height = grid.H;
  fimg = fctx.createImageData(grid.W, grid.H);

  const c = grid.corners;
  const coords = [[c.w, c.n], [c.e, c.n], [c.e, c.s], [c.w, c.s]];

  if (map.getSource("flood")) {
    map.getSource("flood").setCoordinates(coords);
    return;
  }
  map.addSource("flood", {
    type: "canvas",
    canvas: "floodcanvas",
    coordinates: coords,
    animate: true,
  });
  map.addLayer({
    id: "flood",
    type: "raster",
    source: "flood",
    paint: {
      "raster-opacity": 0.94,
      "raster-resampling": "linear",
      "raster-fade-duration": 0,
    },
  });
}

یہاں تین اختیارات اہم ہیں:

  • animate: true Mapbox GL کو کینوس کو ہر فریم میں بناوٹ کے طور پر دوبارہ اپ لوڈ کرنے کی ہدایت کرتا ہے۔ ورنہ حرکت پذیری پہلے فریم پر رک جائے گی۔

  • "raster-resampling": "linear" جب صارف گرڈ ریزولوشن سے آگے زوم ان کرتا ہے تو سیلز کے درمیان بلینڈ ہو جاتا ہے۔ شہر کی توسیع میں 105 میٹر سیلز کا استعمال پانی اور پکسل آرٹ گیم کے درمیان فرق ہوسکتا ہے۔

  • "raster-fade-duration": 0 راسٹر ٹائلز پر لاگو کراس فیڈ میپ باکس GL کو آف کرتا ہے۔ ہر فریم کو دوبارہ بنانے سے دھندلا پھیل جائے گا۔

کینوس ہے۔ display:none کیونکہ نقشہ اسے براہ راست پڑھتا ہے۔ اسے DOM میں نظر آنے کی ضرورت نہیں ہے۔

اب سے جو چاہو لکھو۔ fimg اور دھکا fctx.putImageData(fimg, 0, 0) جب خطہ آن ہوتا ہے، تو اسے خطوں کے اوپر لپیٹ دیا جائے گا اور نقشے پر کسی بھی پچ اور واقفیت پر صحیح جگہ پر ظاہر ہوگا۔ رینڈرنگ کے مسائل پکسل الائنمنٹ کے مسائل میں بدل گئے۔

مرحلہ 4: ٹپوگرافی پر غور کرتے ہوئے فلڈ سمولیشن

سونامی لہر کی اونچائی سے نیچے ہر چیز کو سیلاب نہیں کرتے۔ پانی وہاں پہنچنا چاہیے۔ یہ سمندر سے اندر آتا ہے، نشیبی علاقوں سے بہتا ہے، اور جہاں زمین پانی سے اوپر اٹھتی ہے وہاں رک جاتی ہے، خواہ پہاڑی کے دوسری طرف نشیبی علاقے ہوں۔ یہ کنیکٹیویٹی کا مسئلہ ہے، اور چوڑائی پہلی تلاش اس کے لیے ٹول ہے۔

سب سے پہلے، فاصلے کا میدان. ہر سیل ساحل سے کتنا دور ہے؟ قطار کو زمین سے ملحق تمام سمندری خلیوں میں لگائیں اور پھر اسے سمندر میں پھیلائیں۔ زمین کی طرف اندرون ملک آنے اور پھیلنے والی لہروں کو متحرک کرنے کے لیے اس کا استعمال کریں۔

function seaDistance(grid, sea) {
  const { W, H, cellM } = grid;
  const N = W * H;
  const dist = new Float32Array(N).fill(-1);
  const q = new Int32Array(N);
  let head = 0, tail = 0;

  // seed: sea cells that touch land
  for (let i = 0; i < N; i++) {
    if (!sea[i]) continue;
    const x = i % W, y = (i / W) | 0;
    const touchesLand =
      (x > 0 && !sea[i - 1]) || (x < W - 1 && !sea[i + 1]) ||
      (y > 0 && !sea[i - W]) || (y < H - 1 && !sea[i + W]);
    if (touchesLand) { dist[i] = 0; q[tail++] = i; }
  }

  // expand
  while (head < tail) {
    const c = q[head++];
    const cx = c % W, cy = (c / W) | 0;
    for (let dy = -1; dy <= 1; dy++) {
      for (let dx = -1; dx <= 1; dx++) {
        if (!dx && !dy) continue;
        const nx = cx + dx, ny = cy + dy;
        if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
        const n = ny * W + nx;
        if (!sea[n] || dist[n] >= 0) continue;
        dist[n] = dist[c] + (dx && dy ? cellM * Math.SQRT2 : cellM);
        q[tail++] = n;
      }
    }
  }
  return dist;
}

سیلاب دوسری سمت میں ایک ہی تلاش ہے، ایک اضافی اصول کے ساتھ۔ خلیوں تک صرف اس صورت میں رسائی حاصل کی جاسکتی ہے جب پانی کی سطح زمین سے اوپر ہو۔ waveH میٹر میں ساحلی لہر کی اونچائی ہے۔ پانی اندر کی طرف بڑھتے ہی اونچائی کھو دیتا ہے، اور لکیری ٹوٹنا دیکھنے کے لیے کافی ہے۔

function floodFill(grid, elev, sea, waveH) {
  const { W, H, cellM } = grid;
  const N = W * H;
  const depth = new Float32Array(N);       // metres of water above ground
  const reach = new Float32Array(N).fill(-1); // distance from shore, metres
  const q = new Int32Array(N);
  let head = 0, tail = 0;

  for (let i = 0; i < N; i++) {
    if (!sea[i]) continue;
    const x = i % W, y = (i / W) | 0;
    const nb = [i - 1, i + 1, i - W, i + W];
    for (const n of nb) {
      if (n < 0 || n >= N || sea[n] || reach[n] >= 0) continue;
      if ((n === i - 1 && x === 0) || (n === i + 1 && x === W - 1)) continue;
      reach[n] = 0;
      q[tail++] = n;
    }
  }

  const decayPerM = waveH / 12000; // loses its height over ~12 km

  while (head < tail) {
    const c = q[head++];
    const level = waveH - reach[c] * decayPerM;
    const d = level - elev[c];
    if (d <= 0) continue;               // ground is above the water here: stop
    depth[c] = d;

    const cx = c % W, cy = (c / W) | 0;
    for (let dy = -1; dy <= 1; dy++) {
      for (let dx = -1; dx <= 1; dx++) {
        if (!dx && !dy) continue;
        const nx = cx + dx, ny = cy + dy;
        if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
        const n = ny * W + nx;
        if (sea[n] || reach[n] >= 0) continue;
        reach[n] = reach[c] + (dx && dy ? cellM * Math.SQRT2 : cellM);
        q[tail++] = n;
      }
    }
  }
  return { depth, reach };
}

ہر منظر نامے میں ایک بار چلائیں، فی فریم نہیں۔ 700×500 گرڈ پر اس میں چند ملی سیکنڈ لگتے ہیں۔ بجلی کی پیداوار، depth ہر سیل کے لیے reach آپ اندرون ملک کتنی دور ہیں اس کے لیے تمام اینیمیشن کے تقاضے ہیں۔

بھرنا اصل بلندی کی پیروی کرتا ہے، دریا کی وادی میں بڑھتا ہے اور پشتے پر رکتا ہے۔ سیلاب زدہ خلیوں کی تعداد کو اس سے ضرب دیں: cellM² اور آپ کو پیچھے کھڑے ہونے کے لیے مربع کلومیٹر میں سیلاب زدہ علاقہ ملتا ہے۔

مصنوعی سونامی کے بعد اوڈیسا کا اوپر سے نیچے کا منظر۔ گہرا بھورا پانی ساحلی نشیبی علاقوں اور بندرگاہوں کو ڈھانپتا ہے، ساحل کی شکل کی پیروی کرتا ہے، اور اندرون ملک ہائی لینڈز پر رک جاتا ہے۔ کھلا سمندر اپنا معمول کا گہرا نیلا رنگ برقرار رکھتا ہے۔

مرحلہ 5: متحرک کرنے کے لیے پکسلز بنائیں

فریم لوپس ایک کام کرتے ہیں: ہر سیلاب زدہ زمینی سیل کے لیے، اس بات کا تعین کریں کہ آیا پانی ابھی آیا ہے اور، اگر ہے، تو پانی کا رنگ کیا ہے۔

"آئے ہوئے" خلیوں کا موازنہ۔ reach سامنے والے مقام کے بارے میں F یہ وقت کے ساتھ بڑھتا ہے۔ فی سیل شور شامل کریں۔ reach تو سامنے والا پرفیکٹ سموچ نہیں ہے اور پانی پانی کی طرح نظر آنے لگتا ہے۔

let t0 = null;
const T_LANDFALL = 3.9;   // seconds until the wave reaches the shore
const T_FULL = 10.9;      // seconds until it reaches its maximum extent

function ease(k) {
  return k <= 0 ? 0 : k >= 1 ? 1 : 1 - Math.pow(1 - k, 2.2);
}

function frame(now) {
  if (t0 === null) t0 = now;
  const t = (now - t0) / 1000;
  const d = fimg.data;
  const { W, H } = grid;

  // how far inland the front has got, in metres
  const F = t < T_LANDFALL
    ? -1e9
    : maxReach * ease((t - T_LANDFALL) / (T_FULL - T_LANDFALL));

  for (let i = 0; i < W * H; i++) {
    const o = i * 4;
    if (sea[i] || depth[i] <= 0) { d[o + 3] = 0; continue; }

    const arrival = reach[i] + noise[i] * 180;   // ±90 m of jitter
    if (arrival > F) { d[o + 3] = 0; continue; }

    const age = (F - arrival) / 150;             // how long the cell has been under
    const dp = depth[i];

    // base colour: shallow is lighter, deep is darker
    let r = 40 + Math.max(0, 60 - dp * 10);
    let g = 70 + Math.max(0, 50 - dp * 8);
    let b = 95 + Math.max(0, 40 - dp * 6);
    let a = dp < 0.5 ? 215 + dp * 60 : 245;

    // the leading edge is foam: push toward white for the first moments
    if (age < 1.4) {
      const k = Math.pow(1 - age / 1.4, 2) * 0.8;
      r += (240 - r) * k; g += (246 - g) * k; b += (246 - b) * k;
    }

    d[o] = r; d[o + 1] = g; d[o + 2] = b; d[o + 3] = a;
  }

  fctx.putImageData(fimg, 0, 0);
  if (t < T_FULL + 3) requestAnimationFrame(frame);
}

requestAnimationFrame(frame);

noise ہے Float32Array بے ترتیب قدر [-0.5, 0.5] فی رن ایک بار تیار کیا گیا۔ maxReach سب سے بڑا reach سیلاب زدہ خلیوں کے درمیان قدریں۔

کارکردگی کے دو نکات ہیں: ایک 700×500 گرڈ 350,000 سیلز ہے اور یہ لوپ میرے لیپ ٹاپ پر 60fps اور درمیانے فاصلے والے فون پر تقریباً 30fps پر چلتا ہے۔ اگر مزید ضرورت ہو تو، صرف سیلاب زدہ زمینی خلیوں کی فہرست کے لیے دہرائیں (ایک بار اشاریہ جمع کرنے کے بعد، floodFill) پورے گرڈ کے بجائے۔ ساحلی شہروں میں، یہ عام طور پر خلیوں کے ایک چوتھائی کے برابر ہوتا ہے۔ اور پر اور پر fimg فریموں کے درمیان۔ کال کرنا createImageData ہر فریم وہی ہوتا ہے جس کی طرف پروفائلر پہلے اشارہ کرتا ہے۔

اگر آپ یہاں رکتے ہیں تو، خطوں کے بارے میں آگاہی کا سمولیشن کام کر رہا ہے۔ باقی اسے خاکہ کی بجائے ویڈیو کی طرح بنا رہے ہیں۔

مرحلہ 6: کینوس کمپوزٹنگ کے ساتھ ویڈیو ٹیکسچر کی تہہ لگائیں۔

فلیٹ رنگ ڈایاگرام کے طور پر پڑھتے ہیں۔ اصلی پانی میں حرکت پذیر ساخت ہوتی ہے۔ اس کو حاصل کرنے کا سب سے سستا طریقہ سیلاب زدہ علاقے پر حقیقی پانی کی لوپنگ ویڈیو کاسٹ کرنا ہے۔ "صرف سیلاب زدہ علاقوں کے لیے" کوئی دو کینوس کمپوزٹ موڈز نہیں ہیں۔

  • source-atop ایک نئی تصویر صرف اس صورت میں کھینچتا ہے جب ہدف میں پہلے سے پکسلز ہوں۔ سیلاب کینوس پر ایک ویڈیو بنائیں۔ source-atop صرف پانی پر ظاہر ہوتا ہے۔ خشک زمین اسی طرح رہتی ہے کیونکہ خشک زمین شفاف ہوتی ہے۔

  • destination-in ہدف صرف اس صورت میں رکھیں جب نئی تصویر میں پکسلز ہوں۔ استعمال کرتے ہوئے بفر پر سیاہ اور سفید ماسک بنائیں: destination-inبفر کو ماسک کی شکل میں کاٹا جاتا ہے۔

چھپی ہوئی چیز کی ضرورت ہے؟ (خاموش، دوبارہ اور playsInline. آپ کو تینوں کی ضرورت ہے۔ بصورت دیگر، موبائل سفاری آٹو پلے سے انکار کر دے گا۔ ایک گرڈ سائز کا سکریچ کینوس:

const vid = document.createElement("video");
vid.muted = true; vid.loop = true; vid.playsInline = true;
vid.setAttribute("muted", ""); vid.setAttribute("playsinline", "");
vid.src = "https://www.freecodecamp.org/fx/water.webm";
vid.style.cssText = "position:absolute;width:1px;height:1px;opacity:0;pointer-events:none";
document.body.appendChild(vid);
vid.load();

const buf = document.createElement("canvas");
buf.width = grid.W; buf.height = grid.H;
const bctx = buf.getContext("2d");

function drawWaterTexture(mud) {
  if (vid.readyState < 2) return;         // not enough data yet: skip, don't block
  if (vid.paused) vid.play().catch(() => {});

  // 1. video into the scratch buffer, stretched to the grid
  bctx.globalCompositeOperation = "source-over";
  bctx.clearRect(0, 0, grid.W, grid.H);
  bctx.drawImage(vid, 0, 0, grid.W, grid.H);

  // 2. tint it, only where the video is (everywhere, at this point)
  bctx.globalCompositeOperation = "source-atop";
  bctx.globalAlpha = 0.5 + 0.38 * mud;
  bctx.fillStyle = `rgb(${28 + 108 * mud | 0}, ${112 - 10 * mud | 0}, ${178 - 126 * mud | 0})`;
  bctx.fillRect(0, 0, grid.W, grid.H);
  bctx.globalAlpha = 1;

  // 3. crop the buffer to land cells, so the open sea keeps its own colour
  bctx.globalCompositeOperation = "destination-in";
  bctx.drawImage(landMask, 0, 0);

  // 4. onto the flood canvas, only where there's already water
  fctx.save();
  fctx.globalCompositeOperation = "source-atop";
  fctx.globalAlpha = 0.8;
  fctx.drawImage(buf, 0, 0);
  fctx.restore();
}

اگلی بار مجھے اس پر کال کریں۔ fctx.putImageData(fimg, 0, 0) فریم لوپ میں. مرحلہ 4 وہ مرحلہ ہے جہاں آپ نتائج حاصل کرتے ہیں۔ کیونکہ جہاں پانی نہیں ہے وہاں سیلاب کا کینوس شفاف ہے۔ source-atop یہ بغیر کسی فی پکسل آپریشن کے، BFS کے ذریعے دریافت کردہ تمام خلیجوں اور وادیوں کے ساتھ، ساخت کو بالکل سیلابی شکل کے اندر رکھتا ہے۔

landMask ایک کینوس جو صرف ایک بار فی رن بنایا جاتا ہے۔ سفید sea[i] دوسری جگہ یہ غلط اور شفاف ہے۔ اس کے بغیر، ساخت سمندر کے ایک بڑے پھیلاؤ کو ڈھانپ لے گی اور پوری خلیج سیلاب کا رنگ بدل دے گی۔ ان کے پاس اس ورژن کو تقریبا ایک گھنٹہ باہر تھا اس سے پہلے کہ میں نے اسے دیکھا۔

اوڈیسا کے ساحل کے اوپر سے سمندر کی طرف جھکا ہوا منظر۔ ایک سفید جھاگ کا سامنے ساحلی پٹی کے ساتھ شہر بھر میں پھیلا ہوا ہے، جس کے پیچھے گہرے پانی ہیں۔

کہ mud پیرامیٹر 0 سے 1 تک کا ٹائم ریمپ ہے۔ سونامی کا پانی سمندری پانی کے طور پر آتا ہے اور زمین پر تلچھٹ کو جذب کرتے ہوئے گہرا بھورا ہو جاتا ہے۔ آپ اسے لینڈنگ کے چند سیکنڈ بعد سمندری نیلے رنگ سے مٹی کے بھورے رنگ تک انٹرپول کرکے مفت حاصل کرسکتے ہیں۔

جب میں نے پہلی بار اسے بنایا تو میں نے خود ہی ویڈیو فائل میں ایک بھورا گریڈ پکایا اور پہلے تو میں پانی کو نیلا نہیں کر سکتا تھا چاہے میں نے کتنے ہی رنگ استعمال کیے ہوں۔ سورس فوٹیج کو غیر جانبدار رکھیں اور اسے کوڈ میں رنگ دیں۔

ایک ہی destination-in یہ چال گہرائی پر منحصر سیاہ رنگ فراہم کرتی ہے (الفا کے متناسب کے ساتھ ایک ماسک)۔ depth[i]گہرے رنگ سے بھرا ہوا) اور آگے بڑھتے ہوئے ایک فوم بینڈ (گاوسی کے الفا کے ساتھ ایک ماسک)۔ reach[i] - Fبلبلوں کو منتشر کرنے کی ایک دوسری ویڈیو اس سے بھری ہوئی ہے)۔ ہر ایک میں چند لائنیں ہیں، اور ہر ایک پانی کو تھوڑا زیادہ حجم اور تھوڑا کم رنگ کے طور پر پڑھتا ہے۔

مرحلہ 7: کیمرے کو منظر میں منتقل کریں۔

اوپر سے نیچے کا نقشہ ایک مثال ہے۔ جب آپ کیمرے کو جھکاتے ہیں تو وہی پکسلز جگہ بن جاتے ہیں۔ سیلاب کینوس کو مختصراً مختصر کیا گیا ہے کیونکہ یہ زمین پر لپٹا ہوا ہے۔ آپ کا واحد کام کیمرہ کو کسی قابل دید چیز کی طرف اشارہ کرنا ہے۔

سونامی کی صورت میں، اس کا مطلب ہے کسی شہر کے قریب ساحل پر کھڑے ہونا، سمندر کی طرف دیکھنا اور اسے قریب آتے دیکھنا۔ "قریب شہر" نقشے کے مرکز کے قریب ترین ساحلی سیل ہے۔ "سمندر سے باہر" اس مقام سے قریبی سمندری خلیوں کی اوسط سمت ہے۔ پورے ساحلی پٹی کے بجائے مقامی طور پر شمار کیا جاتا ہے۔ بصورت دیگر، ایک خمیدہ خلیج کیمرے کو غلط افق کی طرف اشارہ کرنے کا سبب بنے گی۔ میں نے اسے اوڈیسا میں پایا، جہاں شہر بھر میں اوسط نے کیمرے کو ہوائی اڈے کے ساتھ ایک خالی میدان کی طرف اشارہ کیا۔

function shoreCamera(grid, sea, center) {
  const { W, H, z, x0, y0, cellM } = grid;

  // 1. nearest coastline cell to the map centre
  let best = -1, bestD = Infinity;
  for (let i = 0; i < W * H; i++) {
    if (!sea[i]) continue;
    const x = i % W, y = (i / W) | 0;
    const coast = (x > 0 && !sea[i - 1]) || (x < W - 1 && !sea[i + 1]) ||
                  (y > 0 && !sea[i - W]) || (y < H - 1 && !sea[i + W]);
    if (!coast) continue;
    const lng = x2lon(x0 + (x + 0.5) / 256, z), lat = y2lat(y0 + (y + 0.5) / 256, z);
    const d = (lng - center.lng) ** 2 + (lat - center.lat) ** 2;
    if (d < bestD) { bestD = d; best = i; }
  }
  const sx = best % W, sy = (best / W) | 0;

  // 2. local seaward direction: average offset to sea cells within ~2.5 km
  const r = Math.max(6, Math.round(2500 / cellM));
  let vx = 0, vy = 0;
  for (let dy = -r; dy <= r; dy++) {
    for (let dx = -r; dx <= r; dx++) {
      const x = sx + dx, y = sy + dy;
      if (x < 0 || y < 0 || x >= W || y >= H || !sea[y * W + x]) continue;
      const len = Math.hypot(dx, dy);
      if (len < 1 || len > r) continue;
      vx += dx / len; vy += dy / len;
    }
  }
  const len = Math.hypot(vx, vy) || 1;
  const east = vx / len, north = -vy / len;   // grid y grows southward

  const shoreLng = x2lon(x0 + (sx + 0.5) / 256, z);
  const shoreLat = y2lat(y0 + (sy + 0.5) / 256, z);
  const mLng = 111320 * Math.cos(shoreLat * Math.PI / 180), mLat = 111320;

  return {
    // stand 900 m inland of the shore...
    center: [shoreLng - east * 900 / mLng, shoreLat - north * 900 / mLat],
    // ...facing the sea
    bearing: Math.atan2(east, north) * 180 / Math.PI,
  };
}

پھر 2 ہیں۔ easeTo کال کریں: اترنے سے پہلے غوطہ لگائیں اور نتائج سے پہلے پیچھے ہٹ جائیں۔

const before = {
  center: map.getCenter(), zoom: map.getZoom(),
  pitch: map.getPitch(), bearing: map.getBearing(),
};
const cam = shoreCamera(grid, sea, map.getCenter());

map.easeTo({
  center: cam.center,
  zoom: Math.min(13.6, before.zoom + 2.8),
  pitch: 72,
  bearing: cam.bearing,
  duration: 1200,
});

setTimeout(() => map.easeTo({ ...before, duration: 2600 }), (T_LANDFALL + 3.2) * 1000);

یہ دو کالیں کیا پیدا کرتی ہیں: کیمرا اوڈیسا کے ساحل کے اوپر ایک کھڑی حصے میں گرا اور خلیج کی طرف اشارہ کیا۔ سمندر پہلے پیچھے ہٹتا ہے، ریت کی ایک چوڑی پٹی کو بے نقاب کرتا ہے، اور پھر سامنے والا ساحل سمندر اور بندرگاہ کی طرف بڑھتا ہے۔ ہر وہ چیز جو حرکت کرتی ہے وہ اسٹیج 3 سے سیلاب کینوس سے ملتی جلتی ہے، جو کیمرے کے ذریعے پیش کی گئی ہے اور زمین پر لپٹی ہوئی ہے۔

حرکت پذیری: اوڈیسا ساحل کا جھکا ہوا کیمرے کا منظر۔ سمندر سب سے پہلے ساحل سے پیچھے ہٹتا ہے، ریت کو بے نقاب کرتا ہے، پھر سیاہ لہریں ساحل اور بندرگاہ پر واپس آتی ہیں، اور شہر فریم کے نچلے نصف حصے کو بھر دیتا ہے۔

نوٹ کریں کہ فریم لوپ میں کسی بھی چیز کو کیمرے کی نقل و حرکت کے بارے میں علم یا پرواہ نہیں ہے۔ لوپ ایک ہی کینوس پر ایک ہی پکسلز لکھنا جاری رکھتا ہے اور نقشہ باقی کام کرتا ہے۔ یہ وہی ہے جو آپ نے مرحلہ 3 میں خریدا ہے۔

جہاں گراؤنڈ فلور رکتا ہے۔

اب تک ہر چیز زمین پر رہتی ہے۔ سیلاب کینوس خود نقشے کی سطح ہے، اور اسی لیے یہ کام کرتا ہے۔ نقطہ نظر، گردش اور خطہ آزادانہ طور پر فراہم کیے جاتے ہیں۔ جس لمحے آپ کو کسی لمبے لمبے چیز کی ضرورت ہو، چال چلی گئی۔ آپ کسی راسٹر پر ٹاور نہیں باندھ سکتے۔

میں نے سیکھا کہ کون سا راستہ مہنگا ہے۔ میں نے ایک حقیقی 3D پانی کی دیوار بنائی۔ three.js اپنی مرضی کی پرت نے کام کیا۔ میں نے اسے بھی کاٹ دیا، ان وجوہات کی بنا پر جو آپ اس پر کوئی وقت گزارنے سے پہلے جان لیں۔

250 میٹر دور سے نظر آنے والی 30 میٹر کی لہر 37 ڈگری دیکھنے کے زاویہ کے تقریباً 7 ڈگری یا اسکرین کے تقریباً پانچویں حصے پر محیط ہے۔ فریم کو بھرنے کے لیے کیمرہ کو کیمرے سے 100 میٹر کے فاصلے پر تقریباً زمین پر رکھنا پڑتا ہے، کیونکہ سیٹلائٹ کی تصویر مٹی میں دبی ہوئی ہے۔ جیومیٹری درست تھی اور شاٹس خراب تھے۔ یہ پتہ چلتا ہے کہ جھکے ہوئے کیمرے کے نیچے لپٹا ہوا پانی صحیح سائز کی دیوار سے زیادہ پانی کے طور پر پڑھتا ہے۔

اس لیے زمینی تہہ زمینی تہہ ہی رہی۔ لیکن زلزلے کے منظر نامے میں ایسا کوئی راستہ نہیں تھا۔ گرنے والی عمارت ایک چیز ہونی چاہیے۔ یہ حصہ 2 ہے۔

حصہ 2: چابیاں والی چیزیں

وسطی کیف میں 7.8 شدت کے زلزلے کے دوران ایک گلی۔ بناوٹ والے بلند و بالا مینار پیش منظر میں کھڑے ہیں، ان میں سے کچھ جھک رہے ہیں، اور اولمپک اسٹیڈیم پر سرمئی دھول کے بادل منڈلا رہے ہیں۔

زلزلے کے منظر نامے میں، عمارت کو گرنا پڑے گا۔ یہ نصف وہی ہے جو میں نے تقریباً 10 تعمیر نو کے بعد ختم کیا۔ یہ ان ہزاروں عمارتوں کے لیے نقشہ کی بنیاد کے اخراج کا مرکب ہے جنہیں وہاں کھڑے ہونے کی ضرورت ہے۔ three.js نیچے آنے والے منظر کے لیے، یہ وہ منظر ہے جو نقشے کے اپنے WebGL سیاق و سباق میں پیش کیا گیا ہے۔

دونوں راستے ایک ہی ویکٹر ٹائلز کو پڑھتے ہیں، ایک ہی گہرائی والے بفر کا اشتراک کرتے ہیں، اور کیمرے کی ہر حرکت کے ساتھ ساتھ چلتے ہیں۔

3D کی دو قسمیں اور آپ کو دونوں کی ضرورت کیوں ہے۔

کوئی راستہ نہیں fill-extrusion پرتیں نقشہ کے انجن کی ہی 3D ہیں۔ یہ تیز ہے، دسیوں ہزار عمارتوں کو ہینڈل کرتا ہے، باقی نقشے کے ساتھ مستقل طور پر روشنی کرتا ہے، اور پراپرٹی کی اونچائی، پراپرٹی کے رنگ، اور (یہاں اہم حصہ) اونچائی کو خصوصیت کے لحاظ سے اونچائی سے ضرب کے اعداد و شمار کے ذریعہ تقویت یافتہ ہے۔ صورت حال آپ اسے رن ٹائم پر تبدیل کر سکتے ہیں۔ یہ جو نہیں کر سکتا وہ حرکت ہے۔ پروٹروژن ایک عمودی پرزم ہے جو ہمیشہ زمین پر کھڑا ہوتا ہے۔

کوئی راستہ نہیں custom ایک پرت ہر فریم پر نقشے کا WebGL سیاق و سباق اور پروجیکشن میٹرکس فراہم کرتی ہے اور آپ کو جو چاہیں کھینچنے کی اجازت دیتی ہے۔ کے ساتھ three.js دوسری طرف، یہ حقیقی اشیاء سے مراد ہے. میش، پوزیشن، گردش، اور پیمانے کے ساتھ ہزاروں میں فوری طور پر، نقشہ کے طور پر اسی گہرائی کے بفر پر لکھا جاتا ہے، اس بات کو یقینی بناتا ہے کہ گرنے والے بلاکس صحیح طریقے سے عمارتوں کے پیچھے غائب ہو جائیں جو اب بھی کھڑی ہیں.

جو تقسیم کام کرتی ہے وہ آسان ہے۔ تمام عمارتیں پھیلی ہوئی ہیں۔ اگر کسی کو نقصان پہنچا ہے تو، اخراج سکڑ جاتا ہے (ایک خصوصیت سے متعلق مخصوص حالت)۔ فرق دیکھنے کے لیے کیمرے کے کافی قریب زوم آؤٹ کرنے پر، اخراج سکڑ کر کچھ بھی نہیں ہو جاتا ہے۔ three.js شے گرتے ہی 2 سیکنڈ تک اپنی جگہ پر رہتی ہے۔ اس کے بعد اس چیز کو ملبے کے ڈھیر سے بدل دیا جاتا ہے۔ three.jsفریم کی شرح بالکل تسلیم شدہ نہیں ہے۔

ہلچل شروع ہونے سے پہلے وسطی کیف کا خوبصورت منظر۔ ہزاروں عمارتیں نقشے کی ویکٹر ٹائلوں سے دیواروں پر کھڑکیوں کے پیٹرن کی ساخت کے ساتھ نکلتی ہیں۔ اولمپک اسٹیڈیم کا ایک 3D ماڈل درمیانی فاصلے پر کھڑا ہے۔

مرحلہ 8: نقشے پر ان کی اپنی ٹائلوں سے عمارتیں حاصل کریں۔

عمارت کے ڈیٹاسیٹ کی ضرورت نہیں ہے۔ نقشہ پہلے ہی ڈاؤن لوڈ ہو رہا ہے۔ میپ باکس اسٹریٹ ویکٹر ٹائل building کثیر الاضلاع فوٹ پرنٹ کے ساتھ ماخذ پرت height، min_height زمین سے شروع ہونے والی عمارتوں کے لیے extrude پرچم querySourceFeatures فی الحال لوڈ کردہ مواد پڑھتا ہے۔

function buildingSource(map) {
  const sources = map.getStyle().sources;
  if (sources.composite) return "composite";         // Mapbox styles
  if (sources.openmaptiles) return "openmaptiles";   // MapLibre + OpenMapTiles
  for (const id in sources) if (sources[id].type === "vector") return id;
  return null;
}

function collectBuildings(map, center, kx, ky) {
  const src = buildingSource(map);
  if (!src) return [];
  const feats = map.querySourceFeatures(src, { sourceLayer: "building" });
  const out = [], seen = new Set();

  for (const f of feats) {
    const p = f.properties || {}, g = f.geometry;
    if (!g || p.extrude === "false" || p.underground === "true") continue;

    const polys = g.type === "Polygon" ? [g.coordinates]
                : g.type === "MultiPolygon" ? g.coordinates : null;
    if (!polys || !polys[0] || polys[0][0].length < 4) continue;

    // centroid and bounds of the outer ring
    const ring = polys[0][0], n = ring.length - 1;
    let sx = 0, sy = 0, x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
    for (let j = 0; j < n; j++) {
      const [x, y] = ring[j];
      sx += x; sy += y;
      if (x < x0) x0 = x; if (x > x1) x1 = x;
      if (y < y0) y0 = y; if (y > y1) y1 = y;
    }
    sx /= n; sy /= n;

    // the same building appears once per tile it touches
    const key = sx.toFixed(6) + "," + sy.toFixed(6) + "," + n;
    if (seen.has(key)) continue;
    seen.add(key);

    let h = +(p.height ?? p.render_height);
    if (!(h > 0)) h = 8;                              // no data: assume two storeys
    const mh = Math.min(h, +(p.min_height ?? p.render_min_height) || 0);

    out.push({
      geometry: g, polys, lng: sx, lat: sy, h, mh,
      d: Math.hypot((sx - center.lng) * kx, (sy - center.lat) * ky),   // metres from epicentre
      rf: 0.5 * Math.sqrt(Math.max(4, (x1 - x0) * kx * (y1 - y0) * ky)), // rough radius, metres
    });
  }
  out.sort((a, b) => a.d - b.d);
  return out.slice(0, 9000);
}

kx اور ky شہر کے عرض بلد کا طول بلد اور میٹر فی عرض بلد۔ 111320 * cos(lat) اور 110574. ہر چیز ڈاون اسٹریم کے مرکز سے میٹر بہ میٹر چلتی ہے۔

اس ڈیٹا کے بارے میں جاننے کے لیے تین چیزیں۔ سب سے پہلے querySourceFeatures چونکہ ہم ہر ایک دوسرے کو ایک دوسرے کو ملانے والی ٹائل کے لیے ایک بار عمارت واپس کرتے ہیں، اس لیے ہمیں نقلیں ہٹانے کی ضرورت ہے۔ اچھی کلید حاصل کرنے کے لیے گول مراکز اور چوٹیوں کی تعداد شامل کریں۔

دوسرا، فنکشن کوئی مستحکم ID نہیں ہے۔ میپ باکس اسٹریٹ پر۔ یہ مرحلہ 11 میں بہت اہم ہے۔

تیسرا، آپ کو صرف وہی ملتا ہے جو بھری ہوئی ہے۔ اس کا مطلب ہے کہ آپ کو عمارتیں کہاں ہیں اس پر زوم ان کرنا ہوگا اور ٹائلیں جمع کرنے سے پہلے ان کے آنے کا انتظار کرنا ہوگا۔

HazardSim میں کیمرہ دو مراحل میں ڈوبتا ہے اور map.areTilesLoaded() اگر صحیح ہے تو، فنکشن کی گنتی پولز کے درمیان تبدیل ہو کر رک جائے گی اور پھر چلائی جائے گی۔ collectBuildings. اگر آپ اسے بہت جلد جمع کرتے ہیں، تو آپ کو 10 عمارتیں ملیں گی جو پہلی ٹائل پر تھیں۔

مرحلہ 9: اوور ہینگ کو عمارت کی طرح بنائیں

پہلے سے طے شدہ اخراج ایک ٹھوس، فلیٹ سایہ دار خانہ ہے۔ اسے ایک خاکہ کے طور پر پڑھا جاتا ہے۔ اس حل کی قیمت بہت کم ہے۔ fill-extrusion-pattern آپ اسپرائٹ سے اسٹائل میں تصویر لے سکتے ہیں، اسے دیوار پر ٹائل کر سکتے ہیں، اور رن ٹائم کے وقت تصویر کو اسپرائٹ میں شامل کر سکتے ہیں: map.addImage.

میں ایک کینوس پر کچھ بیرونی ساخت (ونڈوز کی ایک قطار جس میں دیوار کے رنگ کے ساتھ تھوڑا سا شور ہوتا ہے تاکہ وہ کمپیوٹر سے تیار کردہ نظر نہ آئیں) ڈرا اور رجسٹر کرتا ہوں، پھر عمارت کی اونچائی پر ان کو ایک ایک کرکے منتخب کرتا ہوں۔

function makeFacade(map, id, wall, glass) {
  const cv = document.createElement("canvas");
  cv.width = 128; cv.height = 64;
  const c = cv.getContext("2d");
  c.fillStyle = wall; c.fillRect(0, 0, 128, 64);

  // four floors of windows, 16px per floor
  for (let f = 0; f < 4; f++) {
    for (let x = 3; x + 6 <= 128; x += 12) {
      const lit = Math.random() < 0.07;
      c.fillStyle = lit ? "#c6b68c" : glass;
      c.fillRect(x, f * 16 + 3, 6, 9);
    }
  }
  // film grain
  const im = c.getImageData(0, 0, 128, 64), d = im.data;
  for (let i = 0; i < d.length; i += 4) {
    const n = (Math.random() - 0.5) * 12;
    d[i] += n; d[i + 1] += n; d[i + 2] += n;
  }
  map.addImage(id, im);
}

makeFacade(map, "fac-glass",  "#566a78", "#3c4e5c");
makeFacade(map, "fac-light",  "#cdc8be", "#364048");
makeFacade(map, "fac-brick",  "#8a5a44", "#2e2a26");

پھر پرت۔ اونچائی اور بنیاد اظہار ہیں اور دونوں کو فیچر اسٹیٹ ویلیو سے ضرب دیا جاتا ہے۔ k پہلے سے طے شدہ 1 ہے۔ وہ ضرب اب درج کیا گیا ہے کیونکہ یہ مجموعی طور پر کمی کا طریقہ کار ہے۔

const H  = ["coalesce", ["get", "height"], ["get", "render_height"], 8];
const MH = ["coalesce", ["get", "min_height"], ["get", "render_min_height"], 0];
const K  = ["coalesce", ["feature-state", "k"], 1];

function addBuildingLayers(map, source, sourceLayer, before) {
  const common = { type: "fill-extrusion", source, minzoom: 13 };
  if (sourceLayer) {
    common["source-layer"] = sourceLayer;
    common.filter = ["all", ["!=", ["get", "extrude"], "false"], ["!=", ["get", "underground"], "true"]];
  }
  map.addLayer({
    ...common, id: "bld-walls",
    paint: {
      "fill-extrusion-pattern": ["case",
        [">=", H, 45], "fac-glass",
        [">=", H, 14], "fac-light",
        "fac-brick"],
      "fill-extrusion-height": ["*", H, K],
      "fill-extrusion-base":   ["*", MH, K],
      "fill-extrusion-opacity": 1,
    },
  }, before);
  // a thin second extrusion on top gives the roof its own texture
  map.addLayer({
    ...common, id: "bld-roof",
    paint: {
      "fill-extrusion-pattern": "fac-roof",
      "fill-extrusion-height": ["+", ["*", H, K], 0.05],
      "fill-extrusion-base":   ["max", ["*", MH, K], ["-", ["*", H, K], 0.45]],
      "fill-extrusion-opacity": 1,
    },
  }, before);
}

before یہ پہلی شناخت ہونی چاہیے۔ symbol اس بات کو یقینی بنائیں کہ عمارت طرز کی پرت پر لیبل کے نیچے جاتی ہے۔ لوپ اوور کے ساتھ تلاش کریں۔ map.getStyle().layers.

فی فنکشن کی بنیاد پر پیٹرن کو گھمایا نہیں جا سکتا یا اس کا سائز تبدیل نہیں کیا جا سکتا، اس لیے کھڑکی فرش کے ساتھ بالکل سیدھ میں نہیں آئے گی۔ کیمرے کی اونچائی سے کسی کو پتہ نہیں چلے گا کہ یہ کیا جا رہا ہے۔

مرحلہ 10: فیصلہ کریں کہ کیا گرتا ہے۔

اب نقلی حصہ مختصر ہے۔ ہر عمارت کے لیے، ہمیں اس مقام پر ہلنے کی شدت اور اس شدت سے اس قسم کی عمارت کے گرنے کا امکان درکار ہے۔

زلزلے کا اظہار ترمیم شدہ مرکالی اسکیل I سے XII پر کیا جاتا ہے۔ معیاری کشندگی کا رشتہ شدت سے طاقت دیتا ہے۔ M اور فاصلے r کلومیٹر یونٹس (depth اتلی کرسٹل زلزلوں کے لیے، مرکز کی گہرائی 10 کلومیٹر ہے۔

const DEPTH_KM = 10;

function mmiAt(M, rKm) {
  const R = Math.sqrt(rKm * rKm + DEPTH_KM * DEPTH_KM);
  const v = 1.5 * M - 3.0 * Math.log10(R) - 0.0018 * R + 1.6;
  return Math.max(0, Math.min(12, v));
}

شدت کے فعل کے طور پر گرنے کا امکان نازک وکر ہے۔ شائع شدہ مواد عمارت کی کلاس کے ذریعہ لکھا گیا ہے اور نقلی کی ضرورت سے کہیں زیادہ تفصیلی ہے۔ اس ٹکڑے کے لحاظ سے لکیری ورژن کو درست نظر آنے اور عام جانی نقصان کی تعداد دینے کے لیے ایڈجسٹ کیا گیا ہے۔ اسے تعمیراتی معیار کے عنصر اور اونچائی کے عنصر سے ضرب دیں۔ درمیانی بلندی والی عمارتیں سب سے خراب ہیں اور بہت اونچی عمارتیں بہتر ڈیزائن کی جاتی ہیں۔

const FRAG = [[6.5, 0], [7, 0.004], [8, 0.025], [9, 0.08], [10, 0.2], [11, 0.38], [12, 0.55]];
const QUALITY = { strict: 0.2, avg: 1, poor: 3 };

function collapseProbability(mmi) {
  if (mmi <= FRAG[0][0]) return 0;
  for (let i = 1; i < FRAG.length; i++) {
    if (mmi <= FRAG[i][0]) {
      const [m0, p0] = FRAG[i - 1], [m1, p1] = FRAG[i];
      return p0 + (p1 - p0) * (mmi - m0) / (m1 - m0);
    }
  }
  return FRAG[FRAG.length - 1][1];
}

function assignDamage(buildings, M, epicentre, quality) {
  const kx = 111320 * Math.cos(epicentre.lat * Math.PI / 180), ky = 110574;
  const q = QUALITY[quality] || 1;

  for (const b of buildings) {
    const rKm = Math.hypot((b.lng - epicentre.lng) * kx, (b.lat - epicentre.lat) * ky) / 1000;
    const mmi = mmiAt(M, rKm);
    const hf = b.h < 5 ? 0.7 : b.h < 35 ? 1.15 : b.h < 70 ? 0.8 : 0.45;
    const pCollapse = Math.min(0.9, collapseProbability(mmi) * q * hf);
    const pDamage = mmi >= 6 ? Math.min(0.6, pCollapse * 2.5 + 0.004) : 0;

    const r = Math.random();
    b.cat = r < pCollapse ? 2 : r < pCollapse + pDamage ? 1 : 0;   // 2 collapse, 1 damaged, 0 fine
    b.start = 0.8 + Math.random() * 2.8;                            // seconds after the shock
  }
}

فی عمارت کی بے ترتیبی وہی ہے جو اسے فروخت کرتی ہے۔ ایک دوسرے کے قریب دو ایک جیسے ٹاور، ایک گر گیا اور دوسرا کھڑا۔ حقیقی زلزلے ایسے ہوتے ہیں۔

مرحلہ 11: پرت کو دوبارہ بنائے بغیر زوم آؤٹ اینیمیشن لگائیں۔

مرحلہ 9 کا طریقہ کار مندرجہ ذیل ہے: ہر فریم گرنے والی ہر عمارت کے لیے ایک فعال حالت کا تعین کرتا ہے۔ k 1 اور آخری ملبے کی اونچائی کے درمیان نمبر پر سیٹ کریں۔ اخراج واپس لیا جاتا ہے۔ پرتوں کو دوبارہ نہیں لکھا جاتا ہے، GeoJSON کو دوبارہ ترتیب نہیں دیا جاتا ہے، اور ٹائلز کو دوبارہ پارس نہیں کیا جاتا ہے۔

map.setFeatureState({ source: "bld", id: b.id }, { k: 0.62 });

ایک مسئلہ ہے، اور یہ وہ ہے جس نے مجھے اس پورے پروجیکٹ میں سب سے زیادہ وقت لیا۔ setFeatureState فیچر ID درکار ہے، لیکن Mapbox Streets عمارتوں میں فیچر ID نہیں ہے۔ جب میں ویکٹر سورس کے لیے خاموشی سے کال کرتا ہوں تو کچھ نہیں ہوتا۔

حل یہ ہے کہ جمع شدہ عمارتوں کو اپنے GeoJSON سورس میں کاپی کریں۔ id کنٹرول کرتا ہے اور ماخذ کو ہدایت دیتا ہے کہ وہ اس وصف کو ایک فنکشنل ID میں فروغ دے۔

const feats = buildings.map((b, i) => {
  b.id = i;
  return { type: "Feature", properties: { i, height: b.h, min_height: b.mh }, geometry: b.geometry };
});

map.addSource("bld", {
  type: "geojson",
  data: { type: "FeatureCollection", features: feats },
  promoteId: "i",
  maxzoom: 16,
});
addBuildingLayers(map, "bld", null, firstSymbolLayer);

اب setFeatureState({ source: "bld", id: 17 }, ...) عمارت 17 تک پہنچیں۔ اصل ٹائل بیک لیئر کو اس وقت تک رکھیں جب تک GeoJSON ذریعہ اس کی اطلاع نہیں دیتا۔ isSourceLoadedپھر اسے ہٹا دیں اور شہر میں چمکنے والے فریم نہیں ہوں گے۔

شکل k مقصد یہ ہے کہ گرنے کو وقت کے ساتھ زمین پر پھسلنے والی عمارت کی بجائے گرنے کی طرح نظر آئے۔ میں نے 5 طریقوں کا انتخاب کیا، جس میں ہر عمارت تصادفی طور پر منتخب اور اونچائی میں محدود تھی۔

// k0 = starting height multiplier (1), kf = final rubble height, u = progress 0..1
function collapseK(b, u) {
  const k0 = b.k0, sp = k0 - b.kf;
  switch (b.mode) {
    case 1: {   // floor by floor: ns steps, each with a fast drop then a pause
      const s = u * b.ns, i = Math.min(b.ns - 1, Math.floor(s));
      let f = s - i;
      f = f < 0.45 ? f * f / 0.45 * 1.1 : 0.55 + (f - 0.45) * 0.45 / 0.55;
      return k0 - sp * Math.min(1, (i + Math.min(1, f)) / b.ns);
    }
    case 2: {   // soft storey: ground floor gives, building drops one floor, pauses, then goes
      const sd = Math.min(sp * 0.5, 3.2 / b.h);
      if (u < 0.12) return k0 - sd * (u / 0.12) ** 2;
      if (u < 0.5)  return k0 - sd;
      const v = (u - 0.5) / 0.5;
      return k0 - sd - (sp - sd) * v * v;
    }
    case 3: {   // crushed top: upper part fails first
      const pt = sp * 0.45;
      if (u < 0.3)  return k0 - pt * (u / 0.3) ** 2;
      if (u < 0.55) return k0 - pt;
      const v = (u - 0.55) / 0.45;
      return k0 - pt - (sp - pt) * v * v;
    }
    case 4:     // sideways: steady, slightly accelerating
      return k0 - sp * Math.pow(u, 1.6);
    default:    // pancake: accelerating free fall
      return k0 - sp * u * u;
  }
}

لائن چارٹ جو باقی ماندہ اونچائی کو پانچ کمی کے طریقوں کے ذریعے پیش رفت کے طور پر دکھا رہا ہے۔ پینکیک ایک ہموار ایکسلریشن وکر ہے۔ ہر منزل گرنے اور رکنے کی سیڑھی ہے۔ نرم تہہ تھوڑی سی گرتی ہے اور گرنے سے پہلے کافی دیر تک چپٹی رہتی ہے۔ پسی ہوئی چوٹی جلد گر جاتی ہے، سیٹ ہوتی ہے اور پھر گر جاتی ہے۔ طرف ایک نرم وکر ہے. پانچوں ایک ہی آخری اونچائی پر ملتے ہیں۔

ہر عمارت میں فریم لوپس کی کتنی قطاریں ہیں۔

function frame(now) {
  const el = (now - T0) / 1000, dt = Math.min(0.04, (now - last) / 1000);
  last = now;

  for (const b of active) {
    if (b.done || el < b.start) continue;
    const u = Math.min(1, (el - b.start) / b.dur);
    const k = collapseK(b, u);
    map.setFeatureState({ source: "bld", id: b.id }, { k });
    if (u >= 1) { b.done = true; onLanded(b); }
  }
  requestAnimationFrame(frame);
}

تباہ شدہ لیکن باقی عمارتوں کے لیے (cat === 1) kf 1 کے قریب ہے اور موڈ ایک مختصر اسٹاپ ہے۔ عمارت نمایاں طور پر آدھا میٹر گرتی ہے اور پھر رک جاتی ہے۔ یہ ایک چھوٹی سی تفصیل ہے، اور اس کی وجہ سے ایسا لگتا ہے کہ پوری گلی متاثر ہوئی ہے، نہ صرف منہدم عمارتوں نے۔

چمٹے dt 40ms تک۔ جب نل ہکلاتی ہے اور فریم کو ایک چوتھائی سیکنڈ کا وقت لگتا ہے، تو زوال کے بیچ میں واقع عمارت گرنے کے بجائے اوپر کود جاتی ہے، اور آنکھ فوراً گرفت میں آجاتی ہے۔

مرحلہ 12: نقشے کے اندر Three.js منظر

سکڑ ایکسٹروشن ان عمارتوں کو ہینڈل کرتا ہے جو دور ہیں۔ کیمرے کے قریب لوگوں کے لیے، آپ ڈھانچے کو ٹوٹتے ہوئے دیکھنا چاہتے ہیں۔ چاہے وہ ٹاور کا گرنا اور بلاک سے ٹکرانا ہو، فرش سے ملبہ نکل رہا ہو، یا سڑک پر بچا ہوا ملبہ ہو۔ آپ کو ایک شے کی ضرورت ہے، تو یہ یہاں ہے۔ three.js اندر آو

حسب ضرورت پرتیں دو کال بیک لیتی ہیں: onAdd ایک بار نقشہ اور اس کا GL سیاق و سباق استعمال کرنے کے بعد render ہر فریم میں سیاق و سباق اور نقشے کا پروجیکشن میٹرکس ہوتا ہے۔ جو چیز واضح نہیں ہے وہ ہے نقاط۔ چونکہ ہم مرکیٹر یونٹس کے بجائے اصل سے میٹر میں کام کرنا چاہتے ہیں، اس لیے ہم ایک ٹرانسفارمنگ میٹرکس بناتے ہیں اور اسے ہر فریم کے نقشے کے پروجیکشن سے ضرب دیتے ہیں۔

function makeScene(map, origin, before) {
  const mc = mapboxgl.MercatorCoordinate.fromLngLat([origin.lng, origin.lat], 0);
  const s = mc.meterInMercatorCoordinateUnits();
  // metres east / north / up at the origin  ->  Mercator.  Mercator y grows southward, hence -s.
  const toMercator = new THREE.Matrix4()
    .makeTranslation(mc.x, mc.y, mc.z)
    .scale(new THREE.Vector3(s, -s, s));

  const scene = new THREE.Scene();
  const camera = new THREE.Camera();
  scene.add(new THREE.HemisphereLight(0xfff4e4, 0x3c342d, 0.55));
  scene.add(new THREE.AmbientLight(0xffffff, 0.1));
  const sun = new THREE.DirectionalLight(0xfff8ee, 0.8);
  sun.position.set(-0.45, -0.6, 1);
  scene.add(sun);

  let renderer = null;
  const layer = {
    id: "debris3d",
    type: "custom",
    renderingMode: "3d",
    onAdd(m, gl) {
      renderer = new THREE.WebGLRenderer({ canvas: m.getCanvas(), context: gl, antialias: true });
      renderer.autoClear = false;             // the map has already drawn this frame
      renderer.localClippingEnabled = true;   // used to sink buildings below ground
    },
    render(gl, matrix) {
      camera.projectionMatrix.fromArray(matrix).multiply(toMercator);
      renderer.resetState();                  // Mapbox changed GL state since our last draw
      renderer.render(scene, camera);
    },
  };
  map.addLayer(layer, before);
  return { scene, camera, layer };
}

MapLibre میں، دوسری دلیل ہے۔ render ایک شے ہے اور میٹرکس ہے: args.defaultProjectionData.mainMatrix. باقی سب کچھ ویسا ہی ہے۔

renderingMode: "3d" خیال یہ ہے کہ نقشہ کو صارف کے ساتھ اس کی گہرائی کے بفر کا اشتراک کرنا ہے۔ اٹھائے ہوئے حصوں کے پیچھے کنکریٹ کے بلاکس چھپے ہوئے ہیں۔ اس لیے اسے اوپری حصے میں علیحدہ کینوس کے بجائے نقشے کے تناظر میں پیش کیا گیا ہے۔

ملبے کے ہزاروں ٹکڑے انسٹی ٹیشنز کی نمائندگی کرتے ہیں۔ HazardSim درج ذیل پولز کو برقرار رکھتا ہے: InstancedMesh یہ ایک مقررہ صلاحیت والی اشیاء ہیں، ہر قسم کے ٹکڑے کے لیے ایک (سلیب، دیوار کا ٹکڑا، ریبار، چٹان، چھت کا پرزم) اور جب ٹکڑا بنتا ہے تو ان کو ایک مثال کا اشاریہ دیا جاتا ہے۔

function makePool(scene, geometry, material, capacity) {
  const mesh = new THREE.InstancedMesh(geometry, material, capacity);
  // allocate the colour buffer for the full capacity up front (see Gotchas)
  mesh.instanceColor = new THREE.InstancedBufferAttribute(new Float32Array(capacity * 3).fill(1), 3);
  mesh.count = 0;
  mesh.frustumCulled = false;                 // the map camera is not a three.js camera
  mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
  scene.add(mesh);

  const dummy = new THREE.Object3D(), color = new THREE.Color();
  dummy.rotation.order = "ZXY";
  return {
    n: 0, capacity,
    add(p) {
      if (this.n >= capacity) return -1;
      const i = this.n++;
      this.set(i, p);
      color.setRGB(p.rgb[0], p.rgb[1], p.rgb[2]);
      mesh.setColorAt(i, color);
      mesh.instanceColor.needsUpdate = true;
      mesh.count = this.n;
      return i;
    },
    set(i, p) {
      dummy.position.set(p.x, p.y, p.z);
      dummy.rotation.set(p.tx, p.ty, p.yaw);
      dummy.scale.set(p.sx, p.sy, p.sz);
      dummy.updateMatrix();
      mesh.setMatrixAt(i, dummy.matrix);
      mesh.instanceMatrix.needsUpdate = true;
    },
  };
}

const concrete = new THREE.MeshStandardMaterial({ map: concreteTexture, roughness: 1 });
const pools = {
  slab: makePool(scene, new THREE.BoxGeometry(1, 1, 1), concrete, 30000),
  wall: makePool(scene, new THREE.BoxGeometry(1, 1, 1), facadeMaterial, 1500),
  bar:  makePool(scene, new THREE.BoxGeometry(1, 1, 1), new THREE.MeshStandardMaterial({ color: 0x6e4c38, metalness: 0.25 }), 20000),
  rock: makePool(scene, rockGeometry(), concrete, 9000),
};

30,000 سلیب بہت زیادہ لگ سکتے ہیں، لیکن یہ ایک ڈرا کال ہے۔

نقشے کو ایک اور چیز کی ضرورت ہے: نقشہ صرف اس وقت دوبارہ تیار کیا جاتا ہے جب کچھ تبدیل ہوتا ہے۔ منظر کو تبدیل کرنے کا مطلب یہ نہیں ہے کہ نقشے کے نقطہ نظر سے کچھ بدل جائے۔ کال map.triggerRepaint() ہر فریم کے آخر میں آپ جو حرکت کرتے ہیں، بصورت دیگر میش کو میموری میں اپ ڈیٹ کیا جائے گا نہ کہ اسکرین پر۔

کیف کی گلیوں کا کلوز اپ۔ ایک کثیر ساختہ بلند و بالا ٹاور، جو تھری. جے ایس ماڈل سے دوبارہ تعمیر کیا گیا ہے، اونچی عمارتوں کے درمیان کھڑا ہے، ہلنے کے لیے تیار ہے۔ اولمپک اسٹیڈیم کا ایک ماڈل پس منظر میں ہے۔

ویکٹر ٹائل میں فوٹ پرنٹ کثیر الاضلاع کی خصوصیت ہے۔ کسی عمارت کو پلٹانے کے لیے، آپ کو ایک باکس کی ضرورت ہے جس میں مرکز، واقفیت اور دو طرف کی لمبائی ہو۔ ایک اورینٹڈ باؤنڈنگ باکس کئی لائنوں پر مشتمل ہوتا ہے۔

function orientedBox(ring, kx, ky, origin) {
  // ring in lng/lat -> local metres
  const loc = ring.map(([lng, lat]) => [(lng - origin.lng) * kx, (lat - origin.lat) * ky]);

  // the longest edge sets the orientation
  let ang = 0, best = 0;
  for (let i = 1; i < loc.length; i++) {
    const ex = loc[i][0] - loc[i - 1][0], ey = loc[i][1] - loc[i - 1][1];
    if (ex * ex + ey * ey > best) { best = ex * ex + ey * ey; ang = Math.atan2(ey, ex); }
  }

  // project every vertex onto that axis and its normal, take the extents
  const ca = Math.cos(ang), sa = Math.sin(ang);
  let u0 = Infinity, u1 = -Infinity, v0 = Infinity, v1 = -Infinity;
  for (const [x, y] of loc) {
    const u = x * ca + y * sa, v = -x * sa + y * ca;
    if (u < u0) u0 = u; if (u > u1) u1 = u;
    if (v < v0) v0 = v; if (v > v1) v1 = v;
  }
  const cu = (u0 + u1) / 2, cv = (v0 + v1) / 2;
  return {
    x: cu * ca - cv * sa, y: cu * sa + cv * ca,    // centre, metres from origin
    ang,
    L1: Math.max(2, u1 - u0), L2: Math.max(2, v1 - v0),
  };
}

3-پینل کا خاکہ۔ پہلا: عمارت کا علاقہ کثیرالاضلاع جس میں سب سے لمبا کنارہ نمایاں اور لیبل لگا ہوا ہے۔

سینے 2 سے 4 بلاکس کے ڈھیر ہوں گے۔ Mesh ہر ایک، ایک Group اصل نیچے کے کنارے پر ہے جس کے گرد عمارت گھومے گی۔ طبیعیات ایک سخت جسم ہے جو گر جاتا ہے۔ کونیی سرعت متناسب ہے: g / h · sin θابتدائی طور پر، یہ ہلکی ہلکی ہلکی ہلکی حرکت کرنے لگتا ہے۔ جب جھکاؤ دہلیز سے گزرتا ہے تو اوپری بلاک الگ ہو جاتا ہے اور اس فوری رفتار پر بیلسٹک طور پر اڑ جاتا ہے۔

function startTip(scene, box, h, onLand) {
  const n = Math.max(2, Math.min(4, Math.round(h / 9))), hs = h / n;
  const geo = new THREE.BoxGeometry(box.L1, box.L2, hs);
  const group = new THREE.Group();
  group.position.set(box.x + Math.cos(box.ang) * box.L1 / 2, box.y + Math.sin(box.ang) * box.L1 / 2, 0);
  group.rotation.z = box.ang;
  scene.add(group);

  const segs = [];
  for (let i = 0; i < n; i++) {
    const m = new THREE.Mesh(geo, wallMaterial);
    m.frustumCulled = false;
    group.add(m);
    segs.push({ m, free: false, gone: false, x: -box.L1 / 2, z: hs * (i + 0.5), th: 0, px: 0, pz: 0 });
  }

  const tip = { th: 0.03, w: 0.22, broke: false, bend: 0.1 + Math.random() * 0.14, brk: 0.4 + Math.random() * 0.3 };

  function pose() {
    let bx = -box.L1 / 2, bz = 0;
    for (let j = 0; j < n; j++) {
      const s = segs[j];
      if (s.free) continue;
      const th = tip.th * (1 + tip.bend * j);        // upper blocks bend a little further
      const sn = Math.sin(th), cs = Math.cos(th);
      if (j === 0) { bx = -box.L1 / 2 * cs; bz = box.L1 / 2 * sn; }
      s.px = s.x; s.pz = s.z;
      s.x = bx + sn * hs / 2; s.z = bz + cs * hs / 2; s.th = th;
      bx += sn * hs; bz += cs * hs;
      s.m.position.set(s.x, 0, s.z);
      s.m.rotation.y = th;
    }
  }

  return function tick(dt) {
    tip.w = Math.min(2.2, tip.w + 1.5 * 9.8 / h * Math.sin(tip.th) * dt);
    tip.th += Math.min(0.09, tip.w * dt);          // never more than ~5 degrees per frame
    pose();

    if (!tip.broke && tip.th * (1 + tip.bend * (n - 1)) > tip.brk && n > 1) {
      tip.broke = true;
      for (let j = 1; j < n; j++) {
        const s = segs[j];
        s.free = true;
        s.vx = Math.max(-18, Math.min(18, (s.x - s.px) / Math.max(dt, 0.008)));
        s.vz = Math.max(-18, Math.min(18, (s.z - s.pz) / Math.max(dt, 0.008)));
        s.w = tip.w * (1 + tip.bend * j) * (1.05 + Math.random() * 0.25);
      }
    }

    let left = 0;
    for (const s of segs) {
      if (s.gone) continue;
      if (s.free) {
        s.vz -= 9.8 * dt; s.x += s.vx * dt; s.z += s.vz * dt; s.th += s.w * dt;
        s.m.position.set(s.x, 0, s.z);
        s.m.rotation.y = s.th;
      }
      const landed = s.free ? s.z < Math.min(hs, box.L1) * 0.5 + 0.3 : tip.th >= 1.25;
      if (landed) {
        s.gone = true;
        group.remove(s.m);
        onLand(group.position.x + Math.cos(box.ang) * s.x, group.position.y + Math.sin(box.ang) * s.x);
      } else left++;
    }
    if (!left) { scene.remove(group); geo.dispose(); return true; }   // finished
    return false;
  };
}

کلیمپ آرائشی نہیں ہیں۔ علیحدگی کی رفتار 18 میٹر فی سیکنڈ تک محدود ہے اور فریم مرحلہ ہے۔ dt رفتار کا حساب پوزیشن کے فرق کے طور پر کیا جاتا ہے اور یہ کم از کم 8ms تک محدود ہے کیونکہ ہچکی کے فریم بلاکس کو مدار میں لاتے ہیں۔ اسی وجہ سے، کونیی رفتار 2.5 rad/s تک محدود ہے اور جھکاؤ 0.09 rad فی فریم تک محدود ہے۔ عمارت کو ایسا کچھ کرتے ہوئے دیکھ کر جو وہ نہیں کر سکتی تھی ان تمام نمبروں کو شامل کر دیا گیا تھا۔

onLand یہیں سے انہدام ایک جیسا نظر آنے لگتا ہے۔ ہر بلاک جو زمین کو چھوتا ہے اس مقام پر دھماکہ خیز ملبہ اور دھول پیدا کرتا ہے، جو ایک ڈھیر چھوڑ دیتا ہے۔

مرحلہ 14: ملبہ اور دھول

ملبہ سطح 12 پلس کشش ثقل سے ایک مثالی پول ہے۔ ایک بار جب گرنے والی عمارت ایک سنگ میل سے گزرتی ہے (پہلا زوال، ہر منزل کو فرش بہ منزل موڈ میں، بلاک لینڈنگ)، اس کے قدموں کے نشانات کے کنارے سے چند ٹکڑے پھینک دیں کیونکہ اس کی رفتار مرکز سے دور ہوتی جاتی ہے۔

function throwDebris(pools, b, origin, kx, ky, top, n) {
  const ring = b.polys[0][0];
  const cx = (b.lng - origin.lng) * kx, cy = (b.lat - origin.lat) * ky;
  for (let i = 0; i < n; i++) {
    // a random point on a random edge of the footprint
    const e = 1 + Math.floor(Math.random() * (ring.length - 1)), t = Math.random();
    const a = ring[e - 1], z = ring[e];
    const x = ((a[0] + (z[0] - a[0]) * t) - origin.lng) * kx;
    const y = ((a[1] + (z[1] - a[1]) * t) - origin.lat) * ky;
    // outward, with a little spread
    const ox = x - cx, oy = y - cy, ol = Math.hypot(ox, oy) || 1, v = 2 + Math.random() * 6;
    const size = 0.3 + Math.random() * 1.1;
    const idx = pools.rock.add({
      x, y, z: top * (0.35 + Math.random() * 0.65),
      yaw: Math.random() * 6, tx: Math.random() * 6, ty: Math.random() * 6,
      sx: size, sy: size * 0.8, sz: size * 0.6, rgb: concreteColour(),
    });
    if (idx >= 0) flying.push({ idx, x, y, z: top, vx: ox / ol * v, vy: oy / ol * v, vz: Math.random() * 3, spin: (Math.random() - 0.5) * 8 });
  }
}

function tickDebris(pools, dt) {
  for (let i = flying.length - 1; i >= 0; i--) {
    const p = flying[i];
    p.vz -= 9.8 * dt;
    p.x += p.vx * dt; p.y += p.vy * dt; p.z += p.vz * dt;
    p.tx += p.spin * dt;
    if (p.z <= p.sz * 0.35) { p.z = p.sz * 0.35; flying.splice(i, 1); }   // landed: stays as rubble
    pools.rock.set(p.idx, p);
  }
}

وہ ٹکڑے جو اترتے ہیں وہ اپ ڈیٹ ہونا بند کر دیتے ہیں اور پول میں رہتے ہیں۔ یہ آپ کا ملبہ ہے۔ یہ مفت ہے۔

HazardSim اس کے اوپر کچھ اور چیزیں شامل کرتا ہے۔ جس پر عمارت کھڑی ہے وہ فرش سلیب کا ایک سیٹ ہے جو 9 میٹر کے گرڈ میں سجا ہوا ہے، کھڑکیوں کے ساتھ دیوار کے ٹکڑے، جھکا ہوا ریبار، ڈھیروں کے نیچے لو فل ایکسٹروشن ماؤنڈز ہیں تاکہ یہ یقینی بنایا جا سکے کہ زمین برابر نہیں ہے، اور قریب ترین عمارت کے لیے، ملبے کا فوٹو گرافی سے اسکین کیا گیا ماڈل۔ وہ سجیلا ہیں اور پول اور کشش ثقل کو آپ سے بہترین فائدہ اٹھانے دیں۔

گرنے کے بعد گلی کی سطح کا کلوز اپ۔ ٹاور بلاکس پر دھول لٹکی ہوئی ہے، سرمئی ملبہ ان کے درمیان زمین کو اڑا دیتا ہے، اور آسمان اطراف سے دھوئیں کا ایک کہرا ہے۔

دھول واحد جگہ ہے جسے میں اسکرین اسپیس میں کھینچتا ہوں، اور یہ بات قابل ذکر ہے کہ حصہ 1 میں پانی کے لیے مہلک ہونے کے باوجود یہ کیوں ٹھیک ہے۔ دھول کے ذرات چھوٹے ہوتے ہیں اور ان کی تعداد سینکڑوں میں ہوتی ہے، ہر ایک 3 سے 8 سیکنڈ کے درمیان رہتا ہے۔ مزید برآں، ہر فریم کو ایک نرم اسپرائٹ کے طور پر کھینچا جاتا ہے جس میں ہر ذرہ کی پوزیشن، سائز اور گردش نقشے کے نقطہ سے پیش کی جاتی ہے۔ دیکھنے کے لئے کوئی کنارہ نہیں ہے اور اس کے ساتھ سیدھا کرنے کے لئے کچھ بھی نہیں ہے۔ اسکرین کی جگہ کے اصول بڑی، مستقل چیزوں کے بارے میں ہیں جن کی شکلیں ہیں۔

اہم فیصلے خاک کہاں سے آتی ہے. ابتدائی ورژن میں پورے شہر میں مسلسل دھند اور جلتی ہوئی آگ تھی، اور یہ اسکرین سیور کی طرح نظر آتا تھا۔ جسے ہم اصلی کے طور پر پڑھتے ہیں وہ خاک ہے جو صرف واقعات سے آتی ہے۔ فرش کے گرتے ہی دھڑکنے کی آواز، زمین پر گرتے ہی عمارت کے اوپر سے اٹھنے کی آواز، نرم فرش کے گرتے ہی گراؤنڈ فلور سے جیٹ طیاروں کے پھٹنے کی آواز۔ ہر واقعہ عمارت کے رقبے کے متناسب سائز اور زوال کے متناسب عمودی رفتار کا اخراج کرتا ہے، اور اگلے واقعے تک کچھ نہیں ہوتا ہے۔

function emitDust(b, level, n, surge) {
  for (let i = 0; i < n && dust.length < MAX_DUST; i++) {
    const a = Math.random() * Math.PI * 2;
    const speed = surge ? 5 + Math.random() * 8 : 1 + Math.random() * 3;
    dust.push({
      lng: b.lng, lat: b.lat,                                        // anchor on the map
      x: Math.cos(a) * b.rf * 0.6, y: Math.sin(a) * b.rf * 0.6,     // offset, metres
      z: surge ? Math.random() * 5 : level * (0.2 + Math.random() * 0.8),
      vx: Math.cos(a) * speed, vy: Math.sin(a) * speed,
      vz: surge ? 0.3 + Math.random() * 1.2 : 1.5 + Math.random() * 3,
      size: (surge ? b.rf * 0.5 + 6 : b.rf * 0.6 + 4) * (0.7 + Math.random() * 0.6),
      grow: 3 + Math.random() * 5,
      t: 0, life: (surge ? 5 : 3.5) + Math.random() * 2.5,
      sprite: sprites[Math.floor(Math.random() * sprites.length)],
      alpha: 0.5 + Math.random() * 0.3,
    });
  }
}

ہر فریم، ہر ذرہ سست ہو جاتا ہے، ہوا کے ساتھ بہہ جاتا ہے، بڑھتا ہے، اور پروجیکٹس۔ map.project([lng + x / kx, lat + y / ky]) گراؤنڈ پوائنٹ کی اسکرین لوکیشن فراہم کرتا ہے اور اونچائی کا اطلاق اس طرح ہوتا ہے: y -= z * pixelsPerMetre * sin(pitch). اسکرین کی گہرائی کے لحاظ سے ترتیب دیں اور اس کا استعمال کرتے ہوئے پیچھے سے سامنے کی طرف کھینچیں: drawImageیہ عمر کے ساتھ ختم ہو جاتا ہے۔ یہ چند سو ہے۔ drawImage یہ نقشے پر 2D کینوس پر فی فریم کال کرتا ہے اور اس کی قیمت بہت کم ہے۔

مرحلہ 15: اگر کیمرہ ہلتا ​​ہے۔

اب تک کی ہر چیز اسکرین کے کونے میں خاموشی سے چلنے میں کامیاب رہی ہے۔ یہ کیمرہ ہی ہے جو سامعین کو زلزلے کا احساس دلاتا ہے۔

دو چیزیں۔ سب سے پہلے، غوطہ لگائیں: جائزہ میں آسانی سے 45 ڈگری سے 14.6 تک زوم کریں، ٹائل لوڈ ہونے تک پکڑے رہیں، پھر مرکز میں آسانی سے 62 ڈگری سے 16.2 تک زوم کریں۔ پیچھے ہٹنا کوئی آپشن نہیں ہے۔ اگر میں سیدھے زوم 16 میں غوطہ لگاتا ہوں، تو اس زوم کے لیے ویکٹر ٹائل نہیں آیا ہے، querySourceFeatures ایک زلزلہ خالی شہر پر حملہ کرتا ہے، تقریبا کچھ بھی واپس نہیں آتا ہے۔

function diveToCity(map, center, then) {
  const bearing = map.getBearing();
  map.easeTo({ center, zoom: 14.6, pitch: 45, bearing: bearing - 10, duration: 1100, essential: true });
  const t1 = Date.now();
  (function hold() {
    const loaded = !map.isMoving() && map.areTilesLoaded();
    if (Date.now() - t1 > 1000 && (loaded || Date.now() - t1 > 2000)) {
      map.easeTo({ center, zoom: 16.2, pitch: 62, bearing: bearing - 25, duration: 1500, essential: true });
      map.once("moveend", then);
    } else setTimeout(hold, 150);
  })();
}

دوسرا، ہلنا. ایسا کرنے کے لیے، میپ کیمرے کو حرکت نہ دیں۔ ہلاتے ہوئے کیمرہ easeTo حرکت پذیری کے نظام سے لڑیں اور اپنے تمام گھماؤ کے لیے ٹائلیں لوڈ کریں۔ اسے ہلائیں کنٹینر CSS تبدیلی کے ساتھ۔ نقشہ نہیں جانتا کہ یہ منتقل ہو گیا ہے:

function shake(durationMs, magnitude) {
  const el = map.getContainer();
  const w = window.innerWidth, h = window.innerHeight;
  const amp = Math.min(28, w * 0.02, Math.max(6, (magnitude - 4) * 4.2));
  const dir = Math.random() * Math.PI;
  const t0 = performance.now();
  let last = t0, phase = 0;

  function tick(now) {
    const k = (now - t0) / durationMs;
    if (k >= 1) { el.style.transform = ""; return; }
    const dt = Math.min(0.05, (now - last) / 1000); last = now;

    // builds up over the first third, then decays; frequency rises with it
    let env, omega;
    if (k < 0.35) { const u = k / 0.35; env = 0.15 + 0.85 * u * u * (3 - 2 * u); omega = 7 + 39 * u * u; }
    else          { const v = (k - 0.35) / 0.65; env = Math.pow(1 - v, 1.4); omega = 46 - 18 * v; }
    phase += omega * dt;

    const a = amp * env;
    const osc = Math.sin(phase) * 0.78 + (Math.random() - 0.5) * 0.44 * Math.min(1, omega / 30);
    const x = Math.cos(dir) * osc * a, y = Math.sin(dir) * osc * a * 0.7;
    el.style.transform = `translate(${x.toFixed(1)}px, ${y.toFixed(1)}px) rotate(${(osc * a * 0.012).toFixed(3)}deg) scale(${(1 + 2.4 * amp / Math.min(w, h) * env).toFixed(4)})`;
    requestAnimationFrame(tick);
  }
  requestAnimationFrame(tick);
}

لفافہ طول و عرض سے زیادہ اہم ہے۔ اصل ہلنا ایک یا دو سیکنڈ تک رہتا ہے، چوٹیوں پر، اور پھر کچھ اور کے بعد ختم ہو جاتا ہے۔ جیسے جیسے یہ بڑا ہوتا جاتا ہے، تعدد بھی بڑھتا جاتا ہے۔ سسٹینڈ جیٹر کو ٹوٹی ہوئی ویڈیو کے طور پر پڑھا جاتا ہے۔ اثر ختم ہو جاتا ہے جب مرکزی جھٹکے کے 3 سے 4 سیکنڈ بعد نصف طول و عرض کا آفٹر شاک آتا ہے۔

ایک بار ہلنا بند ہوجانے کے بعد، کیمرہ کو دھیرے دھیرے قریب ترین گرنے والے مقام کے بیچ میں رکھیں، اسے چند سیکنڈ کے لیے وہاں رکھیں تاکہ ملبے کو اس لمحے کو پکڑنے کا موقع ملے، پھر طاقت کی انگوٹھی اور ہلاکتوں کے اعداد و شمار کی طرف کھینچیں۔

اسے تیز رکھو

چند فیصلوں کے ساتھ، ہم اپنے لیپ ٹاپ پر 60fps کو برقرار رکھنے اور اسے اپنے فون پر بھی استعمال کرنے کے قابل ہو گئے۔

1. فکسڈ صلاحیت پول

ہر مثال کے پول کی ایک حد ہوتی ہے۔ add رپورٹ -1 جب بھرا ہو۔ کم طاقت والے آلات کے لیے، ٹوپی کم ہو جاتی ہے۔ یعنی 30,000 کی بجائے 9,000 سلیب اور 9,000 کی بجائے 3,000 پتھر۔ منظر کو سلائیڈ شو کے بجائے کم ٹکڑوں میں تقسیم کیا گیا ہے۔

2. ایک بار آلہ کی پرت کا تعین ہو گیا۔

موبائل صارف ایجنٹ، navigator.deviceMemory < 6، hardwareConcurrency <= 4متبادل طور پر، 700px سے کم کا ایک مختصر اسکرین پہلو آلہ کو نچلے درجے میں رکھے گا۔ نچلے درجوں کو چھوڑ دیں۔ three.js مکمل طور پر جھکا ہوا، صرف گھٹا ہوا پروٹریشن استعمال کریں، 1/3 کو دھول سے ڈھانپیں، اور ہیرو ماڈل کو گرائیں۔

3. ہر 1.2 سیکنڈ تک ملبے کی پہاڑی کو دوبارہ بنائیں

کم fill-extrusion ہر ڈھیر کے نیچے کا ٹیلا GeoJSON ذریعہ ہے۔ setData GeoJSON ماخذ سے اسے دوبارہ پارس کریں۔ ہر منہدم عمارت کے لیے ایک بار ایسا کرنا ابتدائی ورژن میں ہنگامہ آرائی کا ایک بڑا سبب تھا۔ جن عمارتوں پر آپ اترتے ہیں وہ اب زیر التواء فہرست میں جائیں گی جو ٹائمر کی بنیاد پر صاف ہو جائیں گی۔

4. منہدم عمارت کو اس کے اصل مقام سے کبھی نہ ہٹائیں

یہ جاری رہتا ہے۔ bld کے ساتھ k 0.001 پر سیٹ کریں۔ کسی خصوصیت کو ہٹانے کا مطلب ہے: setDataاور setData اس کا مطلب ہے کینٹین۔

5. ارادے کے ساتھ اثاثے لوڈ کریں۔

مکمل تفصیل کے موڈ میں بناوٹ اور ماڈلز 24MB تک کا اضافہ کرتے ہیں۔ جب کوئی وزیٹر Earthquake ٹیب پر کلک کرتا ہے، تو ڈاؤن لوڈ صرف پوری پرت کے لیے شروع ہوگا نہ کہ صفحہ لوڈ پر۔

کیا غلط ہوا اور کیوں؟

ان میں سے ہر ایک میرے لیے حقیقی وقت کے اخراجات کا باعث بنتا ہے، اور پہلے دو اس لیے ہیں کہ حصہ 1 موجود ہے۔

اسٹیکر کا مسئلہ

میرا پہلا ورژن "پانی کی دیوار" تھا - ایک اسکرین اسپیس کینوس پر تیار کردہ ایک ویڈیو کلپ جس میں ہر فریم پر نقشہ کے نقطہ کے ساتھ پیش کیا گیا تھا۔ اس نے میرے 13 انچ کے لیپ ٹاپ پر تقریباً کام کیا۔ میرے 4K مانیٹر پر، یہ ایک مستطیل تھا جس کے نرم کنارے شہر پر منڈلا رہے تھے، لیکن واضح طور پر منظر کا حصہ نہیں تھا۔

مسئلہ اسپرائٹ کا نہیں ہے، لہذا دھندلاپن، پنکھوں، یا سائز کو ایڈجسٹ کرنے کی کوئی مقدار اسے ٹھیک نہیں کرے گی۔ اسپرائٹس 3D دنیا میں موجود نہیں ہوسکتے ہیں۔ تمام مراحل 3-6 متبادل ہیں۔

اگر آپ کو اسکرین اسپیس عناصر کی بالکل ضرورت ہے، مثال کے طور پر سپلیش اسپرائٹس جو ایک مقام پر طے شدہ ہیں، تو انہیں چھوٹے، قلیل المدت اور بہت زیادہ رکھیں، اور یہ قبول کریں کہ آپ کبھی کامیاب نہیں ہوں گے۔

میری پرت دو بار نہیں مل سکی۔

حصہ 1 کے آخر میں 3D پانی کی دیوار بغیر کسی غلطی کے پیش کی گئی، میش بالکل وہیں منتقل ہوا جہاں ڈیبگ پروب نے کہا تھا، لیکن اسکرین پر کچھ بھی ظاہر نہیں ہوا۔ میں نے اسے شامل کیا۔ map.addLayer(layer, "flood")یہ داخل کرنا نیچے سیلاب راسٹر اور مبہم پانی میری دیواروں کو چھپا رہے تھے۔ چیک کریں map.getStyle().layers کچھ اور چیک کرنے سے پہلے۔

دوسری بار جب میں نے ایک پرت کھو دی تو مجھے الٹا مسئلہ درپیش تھا۔ کیونکہ وہاں پرتیں بالکل نہیں تھیں۔ map.getStyle().layers حسب ضرورت پرتوں کی فہرست نہیں ہے۔ اسے چیک کریں map.getLayer("debris3d") اس کے بجائے. میں نے اپنے آپ کو یہ باور کرانے میں کچھ وقت گزارا کہ کوئی پرتیں شامل نہیں کی جا رہی ہیں۔

شفافیت کی 24 سطحیں۔

میں نے ہموار الفا کناروں کے ساتھ ایک سپرائٹ شیٹ بنائی اور چلائی۔ PIL.Image.quantize() PNG سکڑیں۔ پیلیٹ کمپریشن نے رنگوں کے ساتھ الفا چینل کو مقدار بخشی۔ لیول 256 لیول 24 بن گیا، ہموار فال آف سیڑھیاں بن گئیں، اور تمام سیڑھیوں کے سخت سیدھے کنارے تھے جو بالکل ٹائل کی سرحدوں کی طرح نظر آتے تھے۔ میں نے ٹائلنگ بگ کی تلاش میں دو چکر لگائے جو موجود نہیں تھا۔

سیاہ پس منظر پر ایک ہی دھوئیں کے اسپرائٹ کے دو ورژن۔ بائیں، پیلیٹ کوانٹائزیشن کے بعد: بڑا کنارہ کئی شیڈز کے بلاک پکسلز پر مشتمل ہوتا ہے، اور الفا ہسٹوگرام آٹھ الگ تھلگ اسپائکس دکھاتا ہے۔ دائیں، مکمل 8 بٹ الفا: زوم کیے ہوئے کنارے آسانی سے ختم ہو جاتے ہیں اور ہسٹوگرام کو مسلسل 253 سطحوں تک بڑھایا جاتا ہے۔

فکس کوانٹائزنگ کو روکنا اور آر جی بی چینلز کو مستقل بنانا تھا (اسپرائٹ ویسے بھی سفید تھا)۔ لہذا پی این جی کمپریشن کو مزید کچھ نہیں کرنا ہے۔ فائل 3 گنا چھوٹی ہو گئی ہے۔ اب میں اپنی آنکھوں پر بھروسہ کرنے کے بجائے ہسٹوگرام سے الفا چیک کرتا ہوں۔

ایک ہی فائل کا نام، مختلف فائل

میں نے اسے دوبارہ انکوڈ کیا۔ flood.webm دوسری سطح پر دوبارہ تفویض کیا گیا۔ زائرین گھنٹوں پرانی چیزیں خریدتے رہے۔ CDN URL کے ذریعے کیش شدہ اور ?v=2 استفسار کے اسٹرنگ نے اسے پہلے سے موجود فائلوں کے لیے نہیں بنایا۔

فائل کا نام بدل دیں۔ flood2.webm ہم نے اسے ایک تعیناتی سے حل کیا۔ اس کے بعد سے، یہ ہر بائنری اثاثے کا نام بدل دیتا ہے جو اسے URL ورژنز کو منظم کرنے کے بجائے اپ ڈیٹ کرتا ہے۔

بغیر ہیڈ اسکرین شاٹ نے دو بار جھوٹ بولا۔

سافٹ ویئر GL رینڈرر کا استعمال کرتے ہوئے ہیڈ لیس کروم میں تجربہ کیا گیا۔ ہر اسکرین شاٹ پر قبضہ کرنے میں ایک یا دو سیکنڈ لگے۔ کوئی "آؤٹ آف آرڈر" فریم کاؤنٹ بالکل نہیں تھا اور میں نے دوپہر کو شور کے خلاف وقت کو ایڈجسٹ کرتے ہوئے گزارا۔

دو مسائل حل ہو چکے ہیں۔ میں براؤزر سیشن کو بطور ویڈیو ریکارڈ کرتا ہوں اور ffmpeg کا استعمال کرتے ہوئے فریموں کو تراشتا ہوں، اور رنگوں کے لیے میں نقلی کینوس کو براہ راست استعمال کرتے ہوئے پڑھتا ہوں: getImageData دوسری آواز سے ایسا لگتا ہے کہ یہ کمپوزٹ اسکرینوں کی پیمائش کرنے کے بجائے واضح طور پر لکھا گیا تھا، لیکن اسے پہنچنے میں ایک دوپہر لگ گئی۔

اسی بے سر سیٹ اپ نے مجھے پھر زلزلہ پر بیوقوف بنایا۔ وہاں، سیٹلائٹ ٹائل آہستہ آہستہ لوڈ ہوئے اور اسکرین شاٹس نے سیاہ شہر دکھائے، لہذا میں نے سوچا کہ عمارت کی تہہ کافی عرصے سے ٹوٹی ہوئی ہے۔ جو نہیں پہنچا وہ بنیادی نقشہ تھا۔ اگر ہیڈ لیس ٹیسٹ میں کوئی پرت غلط معلوم ہوتی ہے، map.areTilesLoaded() اس سے پہلے کہ آپ اسکرین شاٹس پر یقین کریں۔

میں نے فیچر اسٹیٹ سیٹ کیا اور کچھ نہیں ہوا۔

setFeatureState چونکہ اس فیچر میں ID نہیں ہے، اس لیے اس نے ٹائل سورس کے بارے میں کچھ نہیں کیا اور نہ ہی کچھ رپورٹ کیا۔ فکس اس طرح ایک GeoJSON کاپی ہے: promoteId مرحلہ 11 سے۔ اسے تلاش کرنے میں شرمناک حد تک طویل وقت لگا کیونکہ API ناکام نہیں ہوتا ہے۔ یہ صرف کچھ نہیں کرتا ہے۔

تمام واقعات سیاہ تھے۔

InstancedMesh.setColorAt 3 r128 میں، جب پہلی بار کال کی جاتی ہے، تو یہ آہستگی سے میش کے سائز سے ملنے کے لیے رنگین بفر بناتا ہے۔ موجود count. میرا پول اس سے شروع ہوا: count = 0لہذا، بفر بغیر کسی آئٹم کے بنایا گیا تھا اور تمام ٹکڑوں کو سیاہ کر دیا گیا تھا۔ مختص instanceColor پہلے سے پہلے اپنے آپ کو پوری صلاحیت حاصل کریں۔ setColorAt. وہ لائن ہے makePool.

جیسے ہی ہم نے پین کیا، آدھا ملبہ ختم ہو چکا تھا۔

three.js یہ اپنے کیمرے کے لیے فرسٹم کلنگ میش کا استعمال کرتا ہے۔ ہمارے کیمرے ننگے ہیں۔ THREE.Camera فرسٹم کا خیال بے ہودہ ہے کیونکہ ہر فریم پروجیکشن میٹرکس سے بھرا ہوا ہے اور آپ کو اندازہ نہیں ہے کہ نقشہ کہاں دیکھ رہا ہے۔ سیٹ frustumCulled = false اپنی مرضی کے مطابق تہوں میں تمام meshes پر.

منظر کو اپ ڈیٹ کیا گیا ہے، لیکن اسکرین نہیں ہے.

نقشہ کو صرف اس وقت دوبارہ رنگ دیا جاتا ہے جب اسے لگتا ہے کہ کچھ بدل گیا ہے۔ کال map.triggerRepaint() اعتراض کو منتقل کرنے کے بعد۔ اس کے بغیر، حرکت پذیری اس شرح سے قطع نظر چلے گی جس پر نقشہ کو دوسری وجوہات کی بناء پر دوبارہ بنایا گیا ہے (جو اسٹیل سینز کے لیے 0 ہے)۔

آسمان نے سب کچھ غائب کر دیا

اسی ترتیبات کا استعمال کرتے ہوئے دوسرے پروجیکٹ میں map.setSky(...) میں نے سب کچھ چھپا دیا۔ three.js منظر آسمان کو گہرائی کے بفر پر لکھا جاتا ہے، اور جو بھی حسب ضرورت پرت بعد میں کھینچتی ہے وہ گہرائی کے امتحان میں ناکام ہو جائے گی۔ اگر آپ کسی نقشے پر 3D پیش کر رہے ہیں، تو آسمانی تہہ کو چالو نہ کریں۔

کہ idle بیک گراؤنڈ ٹیبز میں ایونٹس نہیں چل رہے ہیں۔

آپ نے ماڈل پہلے سے لوڈ کر دیا ہے۔ map.on("idle"). بیک گراؤنڈ ٹیب میں، نقشہ بے کار رہنے کے بغیر کافی دیر تک چل سکتا ہے، اور زائرین جنہوں نے ٹیب میں سائٹ کو کھولا اور بعد میں واپس آئے وہ ماڈل کے بغیر زلزلے کا تجربہ کریں گے۔ خطرے کی قسم کو منتخب کیے جانے کے وقت پریلوڈ کو منتقل کر دیا گیا تھا۔

نتیجہ

پوری گائیڈ کو دو ڈھانچے میں کم کر دیا گیا ہے:

زمین:

  1. اگر آپ نقشے کے ٹائل اہرام کے ساتھ گرڈ کو سیدھ میں کرتے ہیں، تو تمام ٹائل ڈیٹا سیٹس کو دوبارہ نمونے کے بغیر چھوڑ دیا جائے گا۔

  2. مفت ایلیویشن ٹائلوں کو ان کے متعلقہ گرڈ میں ڈی کوڈ کریں۔

  3. کینوس کو نقشہ کے ماخذ کے طور پر گرڈ کے مستطیل پر رجسٹر کریں۔ animate: true.

  4. ایک بار گرڈ پر نقلی چلائیں۔ آپ ہر فریم میں پکسلز لکھ کر اینیمیشن بناتے ہیں۔

  5. استعمال کریں source-atop اور destination-in نقلی شکلوں کے اندر ساخت کو ٹھیک ٹھیک سے ڈریپ کریں۔

  6. کیمرے کو حرکت دینے کی کوشش کریں۔ ڈرائنگ کی پیروی کرتے ہیں.

اونچائی:

  1. عمارتیں نقشے پر پہلے سے بھری ہوئی ویکٹر ٹائلوں سے درآمد کی جاتی ہیں۔ چونکہ کوئی IDs نہیں ہیں، ہم ڈپلیکیٹس کو ہٹا دیتے ہیں۔

  2. براہ کرم ہر چیز کو اس طرح کھینچیں: fill-extrusion GeoJSON کاپی کے ذریعے فی فیچر اونچائی ضرب استعمال کرنا promoteId تو فعال حالت کام کرتی ہے۔

  3. نقصان کا تعین عمارت کی مخصوص بے ترتیبی کے ذریعے توجہ کے رشتوں اور کمزوری کے منحنی خطوط کے ذریعے کیا جاتا ہے۔

  4. ہم ہر فریم میں ایک ضرب بنا کر زوم آؤٹ کو متحرک کرتے ہیں۔

  5. کیمرے کے قریب عمارتوں کے لیے رینڈرنگ three.js نقشے کے اپنے GL سیاق و سباق میں renderingMode: "3d"تو گہرائی کا بفر مشترکہ ہے۔

  6. قدموں کے نشانات کو اورینٹڈ بکس میں تبدیل کریں، انہیں بلاکس میں پلٹائیں، اور ان پر مثالی ملبہ پھینک دیں۔ یہ صرف واقعات میں دھول خارج کرتا ہے۔ کنٹینر ہلائیں، کیمرے کو نہیں۔

اس کے پیچھے خیال کچھ ایسا ہے جو میں نے بار بار سیکھا ہے۔ دنیا سے تعلق رکھنے والی ہر چیز کو عالمی نقاط میں کھینچا جانا چاہیے۔ نقشے پر کینوس ایک شیشے کی پلیٹ ہے۔ نقشے کے اندر کینوس زمین ہے، three.js نقشے کے جی ایل سیاق و سباق کے اندر کا منظر اس کے اوپر کی ہوا ہے۔

وہ تمام تر اصلاحات جنہوں نے دونوں سمولیشنز کو زیادہ حقیقت پسندانہ بنایا وہ پہلی قسم سے دوسرے میں جانے سے آئی ہیں۔

ایک اور چیز جو میں اپنے آپ کو شروع میں بتانا چاہتا ہوں وہ یہ ہے کہ نقشہ انجن اور three.js مدمقابل نہیں۔ نقشہ 10,000 فکسڈ عمارتوں پر مشتمل ہے جو آپ کی تخلیق کردہ کسی بھی چیز سے بہتر ہے۔ three.js یہ 10 جذبات کا اظہار کسی بھی دوسری اظہاری زبان سے بہتر کرتا ہے۔

ہر فرد کو ایک ایسا کام دیں جس میں وہ اچھے ہوں اور دونوں کے درمیان سیون پر کام کریں۔ یہ تقریباً مکمل طور پر ایک 12 قدمی کوآرڈینیٹ تبدیلی اور 11 قدمی فیچر اسٹیٹ ٹرک ہے۔

مکمل منظر نامہ: اوڈیسا میں 30 میٹر سونامی اوپر سے دیکھا گیا۔ براؤن فلڈنگ ساحلی علاقوں پر محیط ہے، اور بائیں طرف کے نتائج کا پینل اموات، سیلاب زدہ علاقے، اندرون ملک حد تک، اور زیادہ سے زیادہ گہرائی کی فہرست دیتا ہے۔

آپ Hazardim.com پر دونوں منظرنامے دیکھ سکتے ہیں۔ ایک ساحلی شہر منتخب کریں اور سونامی شروع کریں، یا شہر منتخب کریں، پیمانہ سیٹ کریں اور بٹن دبائیں۔ اس گائیڈ میں موجود کوڈ اس کوڈ کا کلین اپ ورژن ہے جو یہاں چلتا ہے۔ ٹم کا شکریہ کہ آپ نے اسے اس طرح سے توڑ دیا کہ میں کبھی نہیں کر سکتا تھا۔

اوپر تک سکرول کریں۔