تحليل معمقmapbox-gl3Docclusion

إخفاء علامات Mapbox GL: كيف نحجب علامات HTML خلف الطوابق والجدران والرفوف ثلاثية الأبعاد

علامات HTML في Mapbox GL تُرسم فوق لوحة WebGL، فلا تحجبها الطوابق والجدران المبثوقة. إليك كيف نخفيها باختبار تحليلي لشعاع الكاميرا مقابل المواشير.

قراءة 9 دقيقةنُشر تم التحديث بقلم Spatial Map team
متجر Retail Mart Store ثلاثي الأبعاد بزاوية مائلة: رفوف وجدران وسلالم متحركة مع نبضات قارئات RFID وشارات المخزون وعلامات موضوعة على ارتفاع طابقها

المشكلة في جملة واحدة

يرسم Mapbox GL الطوابق والجدران والرفوف والنماذج ثلاثية الأبعاد داخل لوحة WebGL واحدة، حيث يحدّد الـ depth buffer أيّ بكسل يظهر. أما `mapboxgl.Marker` فهو عنصر HTML موضوع فوق تلك اللوحة. أَمِل الكاميرا فوق متجر من ثلاثة طوابق وسترى كل علامة في الطابق الأرضي تخترق البلاطات فوقها؛ وانظر عموديًا إلى الأسفل فترى علامات كل الطوابق دفعة واحدة دون أي وسيلة للتمييز بينها.

المخرج المغري هو خيار `occludedOpacity` في Marker نفسه، لكنه لا يفيد هنا: فهو يخفّف ظهور العلامات خلف التضاريس ثلاثية الأبعاد فقط.

ما يجرّبه الناس عادةً (ولماذا لا يكفي)

  • استدعاء `queryRenderedFeatures` مع كل حركة للكاميرا. يخبرك بالعناصر التي تغطي بكسلًا على الشاشة، لا بما إذا كانت أقرب إلى الكاميرا من العلامة، واستدعاؤه لكل علامة في كل إطار مكلف.
  • استبدال علامات HTML بطبقة symbol. تخضع الرموز لاختبار العمق، لكنك تخسر محتوى DOM (أيقونات React وحالات المرور ومقابض السحب والتحويل)، وكما سنرى فإن اختبار العمق نفسه غير موثوق للأشياء الموضوعة على أرضية.
  • رسم العلامات كدوائر مبثوقة. تدخل في الـ depth buffer، لكنها تبدو كعلب مضغوطة ويصعب النقر عليها.

ما أردناه فعلًا: إبقاء علامة DOM كما هي، وإخفاؤها كلما وُجدت هندسة صلبة بينها وبين الكاميرا.

الحل: شعاع واحد ومواشير كثيرة

كل ما يبثقه المحرّر هو منشور: حلقة بصمة ممتدة بين ارتفاعين. وهكذا يصبح سؤال «هل هذه العلامة خلف شيء ما؟» هو «هل القطعة من الكاميرا إلى العلامة، بعد قصّها على نطاق ارتفاع المنشور، تعبر بصمته؟». اختبار واحد يغطي الحالتين المهمتين: علامة تحت بلاطة (القطعة تعبر بصمة طابق أعلى) وعلامة خلف جدار (القطعة تعبر بصمة رفيعة تمتد عبر نطاق ارتفاع الكاميرا نفسها). أما مخطط الطابق المسطّح فهو مجرد منشور منحلّ تتساوى قاعدته مع قمته.

تجري كل الحسابات في فضاء Mercator، حيث يكون الارتفاع الثابت قيمة `z` ثابتة، فيصبح القصّ على النطاق عمليتَي قسمة فقط. وتأتي البصمات دائمًا من مولّد كل نوع، فتتطابق الدوران وسماكة الجدار وقصّ الصورة مع ما يُرسم فعلًا:

  • المناطق والدوائر: منشور واحد من الارتفاع إلى الارتفاع + السماكة (الدوائر في وضع الشبكة: منشور لكل خلية).
  • الجدران: مضلّع الجدار بسماكته؛ أما الخط المتعدد المسطّح فلا حجم له ولا يحجب شيئًا.
  • السلالم والمصاعد: منشور لكل درجة وبسطة مع تطبيق الدوران؛ ويُتجاهل الزجاج.
  • الرفوف: منشور لكل خلية (صف × عمود × مستوى)، فيحجب الرف الأمامي ملصقات الرف الذي خلفه؛ وتُتجاهل الخلايا المسطّحة.
  • مخططات الطوابق: رباعي الصورة بعد القصّ (لا صندوقها المحيط الخام) على ارتفاع المخطط.
  1. 1
    اقرأ الكاميرا مرة واحدة

    `map.getFreeCameraOptions().position` يعطي موضع الكاميرا بإحداثيات Mercator x/y/z.

  2. 2
    كوّن القطعة

    من الكاميرا إلى موضع العلامة على ارتفاعها الخاص.

  3. 3
    قصّ على النطاق

    لكل منشور، قصّ القطعة على `[zBottom, zTop]`. التقاطعات خلف العين أو بعد العلامة تُستبعد، ولهذا لا تحجب الهندسة الواقعة تحت العلامة أبدًا عند النظر إلى الأسفل.

  4. 4
    اختبر البصمة

    استبعاد سريع بالصندوق المحيط أولًا، ثم: هل إحدى نهايتي القطعة المقصوصة داخل الحلقة، أو هل تعبر القطعة أحد الأضلاع؟

  5. 5
    توقّف عند أول إصابة

    يكفي حاجب واحد؛ تنكسر الحلقة ويُضاف معرّف العلامة إلى مجموعة المحجوبات.

ts·القصّ على نطاق الارتفاع (مبسّط)
// Simplified from src/lib/marker-occlusion.ts — the real loop also handles a
// camera level with the marker, an AABB reject and the "embedded in a solid" skip.
const camera = map.getFreeCameraOptions().position!        // read once
const p = mapboxgl.MercatorCoordinate.fromLngLat(marker.position, altitudeM)
const dx = p.x - camera.x, dy = p.y - camera.y, dz = p.z - camera.z

for (const prism of prisms) {
  if (prism.topM <= altitudeM + 0.5) continue              // standing on it, not behind it
  // Clip the camera→marker segment to the prism's altitude slab: two divisions.
  const ta = (prism.zBottom - camera.z) / dz
  const tb = (prism.zTop - camera.z) / dz
  let tLo = Math.min(ta, tb), tHi = Math.max(ta, tb)
  if (tHi <= 0 || tLo >= 1) continue                       // behind the eye, or past the marker
  tLo = Math.max(tLo, 0); tHi = Math.min(tHi, 1)
  const ax = camera.x + tLo * dx, ay = camera.y + tLo * dy
  const bx = camera.x + tHi * dx, by = camera.y + tHi * dy
  if (segmentHitsRing(prism.ring, ax, ay, bx, by)) {        // endpoint inside, or an edge crossed
    occluded.add(marker.id)
    break
  }
}

يعمل الحساب على حدث `move` في Mapbox، لأن الحجب يتعلق بالكاميرا لا بالبيانات، و`move` يُطلق مرة واحدة على الأكثر في كل إطار. لكل علامة ولكل منشور هو فحص للصندوق المحيط وبضع عمليات قسمة، بلا ذهاب وإياب إلى الـ GPU.

الإخفاء هو سمة data وقاعدة CSS واحدة، ويجب أن تكون `!important`: إذ يعيد `mapboxgl.Marker` كتابة `opacity` و`pointer-events` المضمّنتين في العنصر أثناء تلاشي الضباب/التضاريس الخاص به كلما تحرّكت الكاميرا، فيعمل `el.style.opacity = "0"` لحظة ثم يتراجع بصمت.

css·index.css
/* Outside any Tailwind @layer: the attribute only ever exists at runtime. */
.shape-marker[data-occluded='true'] {
  opacity: 0 !important;
  pointer-events: none !important;
}
متجر Retail Mart Store بطوابقه الثلاثة بزاوية مائلة: العلامات وشارات الرفوف ونبضات قارئات RFID على ارتفاع طابقها بين الجدران والرفوف والسلالم المتحركة
معاينة Spatial Map لمتجر Retail Mart Store ثلاثي الأبعاد: مناطق أقسام حمراء وبرتقالية وخضراء مع رفوف، وسلالم متحركة في الوسط، ونبضات بنفسجية لقارئات RFID وشارات مخزون خضراء فوق الرفوف
معاينة Spatial Map لمتجر Retail Mart Store ثلاثي الأبعاد: مناطق أقسام حمراء وبرتقالية وخضراء مع رفوف، وسلالم متحركة في الوسط، ونبضات بنفسجية لقارئات RFID وشارات مخزون خضراء فوق الرفوف

قواعد تعلّمناها بالطريقة الصعبة

  • الهندسة الشفافة يجب ألا تحجب. خفض طابق علوي إلى 30% لإلقاء نظرة على الطابق الذي تحته استخدام حقيقي. لا يُعدّ الشكل حاجبًا إلا إذا كان مرئيًا ومعتمًا بنسبة 50% على الأقل؛ كما تتوقف الطبقات والطوابق المخفية عن الحجب.
  • الوقوف على سطح ليس وجودًا خلفه. لا يحجب العلامةَ أي شيء تقع قمته ضمن 0.5 م من ارتفاعها، ولا يحجب العلامةَ أبدًا مخططُ الطابق الذي تنتمي إليه.
  • العلامة داخل جسم صلب تتجاهل ذلك الجسم. وإلا ستومض مع دوران الكاميرا. يستخدم الفحص `>=` على قاعدة المنشور عمدًا: ملصق على ارتفاع 0 م داخل غرفة مبثوقة من 0 م هو *داخل* الغرفة لا خلفها.
  • فتحات الأبواب تبقى صلبة. تُبنى منشورات الجدران دون فتحات الأبواب، فتُخفى العلامة المؤطّرة في مدخل باب بدل أن تومض. الحجب الزائد هو الاتجاه الآمن.
  • لا تُعِد اشتقاق شيء. ابنِ البصمات بالمولّد نفسه الذي ترسم به الطبقة. البصمة المصنوعة يدويًا هي الطريق الذي ينحرف به الحاجب بصمت عمّا يظهر على الشاشة.

ما لا نجعله يحجب عن قصد

  • نماذج GLB ومستويات الصور: لا نعرف عنها سوى صندوق محيط، ومعاملته كمنشور ستخفي علامات على بعد أمتار من أي أصل كبير.
  • زجاج المصاعد والسلالم المتحركة: الزجاج شفاف فلا يحجب شيئًا.
  • كل ما تقل عتامته عن 50% أو كان مخفيًا أو على طابق مخفي: الأولوية لسير العمل الشفاف.

طبقات symbol تحتاج المعالجة نفسها

ليس كل ما على الخريطة علامة DOM. فالملصقات النصية وملصقات خلايا الرفوف والأبواب ونقاط البيانات الحيّة القادمة من روابط البيانات هي طبقات `symbol` أصلية. قد تتوقع أن يتكفّل بها `text-occlusion-opacity` في Mapbox، لكن أمرين وقفا في الطريق.

غياب قيمة عتامة الحجب ليس هو القيمة الافتراضية. تذكر مواصفة الأنماط أن القيمة الافتراضية لـ `text-occlusion-opacity` هي `0` («أخفِ عند الحجب»)، لكن السطر التالي يوضح أن القيمة *الغائبة* تعني حجبًا كاملًا مقابل التضاريس فقط. إن تركتها دون تعيين، تتجاهل الطبقة بلاطاتك وجدرانك. يجب أن تسمّي كل طبقة symbol يُفترض أن تختفي خلف الهندسة هذه الخاصية صراحةً.

دقة العمق أخشن من أن تناسب ما يوضع على الأرضية. الملصق على ارتفاع 0 م يقع تحت بلاطة منطقة سماكتها 15 سم أو مخطط طابق نقطي سماكته 10 سم بقليل. في المنظر العلوي اعتبر اختبار العمق هذه الملصقات «خلف محتوى ثلاثي الأبعاد» ورسمها بعتامة صفرية، ثم أعاد إظهار بعضها عند الإمالة. لذلك تضبط طبقة الملصقات `text-occlusion-opacity: 1` (رؤية بالأشعة) وتترك القرار للمحلِّل التحليلي، الذي يقلب `feature-state.occluded` ← `text-opacity: 0`. وتستخدم ملصقات الرفوف والأبواب المحلِّل نفسه.

وتضيف نقاط البيانات الحيّة حقيقة أخرى عن Mapbox: طبقة `circle` لا يمكن رفعها (لا يوجد `circle-z-offset`) ولا حجبها. لذلك تُرسم كل نقطة وقرص وحلقة كـ `symbol` بمحرف `●`. تحتفظ هذه الطبقات بـ `text-occlusion-opacity: 0` وتدمج حكم المحلِّل في `text-opacity`، كـ *خاصية* للعنصر لأن `setData` يمسح الـ feature‑state، فيصبح الإخفاء حتميًا عند أي مستوى تكبير.

ts·نقطة رابط بيانات يمكنها الاختفاء تحت طابق
// Data-link dot: a symbol with a '●' glyph, because a circle layer
// can neither be lifted (no circle-z-offset) nor occluded in Mapbox GL 3.25.
paint: {
  'symbol-z-offset': ['coalesce', ['get', 'elevation'], 0],
  // Name it explicitly — an ABSENT occlusion opacity only occludes against terrain.
  'text-occlusion-opacity': 0,
  // The analytic verdict, stamped into feature properties (setData wipes feature-state).
  'text-opacity': ['case', ['boolean', ['get', 'occluded'], false], 0, 1],
}

أين تراه في Spatial Map

في العارض، يفتح زر Quality نافذة Graphics quality، حيث يكون خيار Hide items under floors (إخفاء العناصر تحت الطوابق) مفعّلًا افتراضيًا؛ وتسمّي اللوحة الجانبية في المحرّر المفتاح نفسه Hide markers under floors. يُحفظ الإعداد في متصفحك. إيقافه يعطّل الحساب لعلامات DOM ويعيد السلوك المسطّح الذي تُرسم فيه العلامات فوق كل شيء، وهو مفيد حين تريد رؤية كل الأصول على كل الطوابق دفعة واحدة. راجع الرسوميات والنمط في العارض.

نافذة Graphics quality في العارض: 3D shadows وAmbient occlusion وHide items under floors (مفعّل)
نافذة Graphics quality فوق خريطة ثلاثية الأبعاد مموّهة: 3D shadows معطّل، Ambient occlusion معطّل، Hide items under floors مفعّل، وزر Done
نافذة Graphics quality فوق خريطة ثلاثية الأبعاد مموّهة: 3D shadows معطّل، Ambient occlusion معطّل، Hide items under floors مفعّل، وزر Done

تبرز أهميته في المباني متعددة الطوابق: متجر بعدة طوابق مع سلالم متحركة ومصاعد (راجع المصاعد والسلالم المتحركة)، أو مركز تسوّق، أو مبنى مكاتب متعدد الطوابق، حيث يجب أن يُظهر المنظر العلوي الطابق الذي تنظر إليه لا دبابيس كل الطوابق معًا. استخدم منتقي الطوابق الموصوف في الطبقات والطوابق لعزل مستوى واحد.

مزالق

جرِّب المنصة التي تستحقها بياناتك.

تحوِّل Spatial Map جداول البيانات وتدفقات المستشعرات ورسومات CAD إلى مشهد ثلاثي الأبعاد حي للمنشأة — بلا تثبيت، وتجربة مجانية سبعة أيام.

اقرأ أيضًا