Nightmare: ทำไมจอมืดและ effect ถูกบดบัง

ตรวจจาก client จริงวันที่ 2 พ.ย. 2026 · โค้ด js/nightmare.js และ js/render3d.js เวอร์ชัน 63ac3f741469 และ 9d2e938e9ff1 (ค่าตรงกันทั้งสองเวอร์ชัน) · แมพ W0 ช่วงที่ Nightmare เปิด

สรุป

ตอน Nightmare เปิด เกมไม่ได้เปลี่ยนแสงในฉาก 3D เลย แต่ใช้ CSS 3 ชั้นทับ canvas#game ทั้งแผ่น ทุกพิกเซลจึงโดนเหมือนกัน รวมถึง effect สกิล ชั้นที่หนักที่สุดคือ #nm-tint ซึ่งใช้ mix-blend-mode: color แทนสีของทุกพิกเซลด้วยม่วง 42% ไฟ น้ำแข็ง และสายฟ้าจึงเสียสีของธาตุ ผู้เล่นแยกธาตุจากสีไม่ออก

ข้อเสนอคือทำบรรยากาศ Nightmare ด้วยแสงในฉากผ่าน applyMood ซึ่งมีอยู่แล้ว วิธีนี้ทำให้โลกมืดและม่วงลงได้เหมือนเดิม แต่ effect ที่ส่องสว่างในตัวไม่โดนย้อม และกลับเด่นขึ้นในความมืด ทดลองบน client จริงแล้วได้ผลตามนี้

42%ม่วงทับสีทุกพิกเซล (#nm-tint)
−15%ความสว่างทั้งจอ (brightness(0.85))
−20%ความสดของสี (saturate(0.8))
50%ความมืดที่มุมจอ (#nm-veil)

โค้ดปัจจุบัน

startNightmare(s) ใน nightmare.js ฝัง CSS ลงหน้า และสร้าง div 2 แผ่นที่ z-index: 4 ทับ canvas ทุกอย่างเปิดด้วย class nm-on บน <html>

/* nightmare.js (CSS ที่ฝังใน startNightmare) */
html.nm-on canvas#game { filter: saturate(0.8) brightness(0.85) contrast(1.04); transition: filter 1.5s; }
#nm-tint { position: fixed; inset: 0; pointer-events: none; z-index: 4; opacity: 0; transition: opacity 1.5s;
  background: #6b3fa0; mix-blend-mode: color; } /* (color: the violet's hue, the world's own light and shade) */
html.nm-on #nm-tint { opacity: 0.42; }
#nm-veil { position: fixed; inset: 0; pointer-events: none; z-index: 4; opacity: 0; transition: opacity 1.5s;
  background: radial-gradient(ellipse at center, rgba(40, 0, 60, 0) 55%, rgba(40, 5, 65, 0.22) 80%, rgba(18, 0, 30, 0.5) 100%); }
html.nm-on #nm-veil { opacity: 1; animation: nm-breathe 5s ease-in-out infinite; }
// nightmare.js: เปิดโหมดเมื่อแมพปัจจุบันอยู่ในรายการ และสลับเพลง
const t = performance.now(),
  o = a.on.length > 0 && t < a.until,   // มี Nightmare และยังไม่หมดเวลา
  i = o && a.on.includes(s.mapId);        // แมพที่ยืนอยู่โดนด้วย
document.documentElement.classList.toggle("nm-on", i);
i && !u ? (G.setPlace("nightmare"), (y = !0)) : !i && u && (G.setPlace(s.mapId), (y = !1));

ผลของแต่ละชั้น

ชั้น ทำอะไร ผลต่อ effect สกิล ระดับ
#nm-tint แผ่นสี #6b3fa0 แบบ mix-blend-mode: color ทึบ 42% เปลี่ยน hue และความสดของทุกพิกเซลเป็นม่วง แต่คงความสว่างเดิม ไฟสีส้มกลายเป็นสีโคลนอมม่วง น้ำแข็งและสายฟ้าไปทางม่วงเหมือนกันหมด สีธาตุถูกลบเกือบครึ่ง หนักที่สุด
canvas#game filter saturate(0.8) brightness(0.85) contrast(1.04) effect มืดลง 15% และซีดลง 20% แสงเรืองดูแบนลง ปานกลาง
#nm-veil vignette สีม่วงเข้ม 0% กลางจอ, 22% ที่ 80%, 50% ที่มุม หายใจเข้าออกทุก 5 วิ effect ที่อยู่ขอบจอมืดลงเพิ่ม แต่กลางจอไม่โดน เบา

ค่าแสงของแมพ W0 ตอนตรวจปกติดี (sunI 2.4, hemiI 1.05, exposure 1.0605) ความมืดทั้งหมดตอน Nightmare จึงมาจาก 3 ชั้นนี้ แมพหิมะ (N) และ dungeon (D) ตั้งให้มืดอยู่แล้วโดยตั้งใจ

ทำไม effect ถึงโดนไปด้วย

ทั้ง 3 ชั้นทำงานหลังจาก WebGL วาดภาพเสร็จแล้ว ตอนนั้น browser เห็นแค่พิกเซล 2D แผ่นเดียว ไม่รู้ว่าพิกเซลไหนเป็นพื้น มอนสเตอร์ หรือ effect จึงย้อมทุกอย่างเท่ากัน

ในฉาก 3D ต่างกัน effect ส่วนใหญ่ของเกมใช้ toneMapped: false และ blending แบบ additive จึงไม่ขึ้นกับแสงของฉาก ถ้าลดแสงที่ sun และ hemi พื้นและโมเดลจะมืดลง แต่ effect ยังสว่างเท่าเดิมและตัดกับพื้นมืดได้ดีขึ้น ฉากกลางคืนในเกมส่วนใหญ่ทำแบบนี้

ข้อเสนอ: ทำ Nightmare ด้วยแสงในฉาก

แบบปัจจุบัน CSS ทับจอ

  • ทั้งจอเป็นม่วงหม่น รวมถึง effect
  • สีธาตุของสกิลหายเกือบครึ่ง
  • ปรับง่าย ไม่ต้องแตะ renderer

แบบเสนอ แสงในฉาก

  • โลกมืดและม่วงด้วยแสง หมอก และท้องฟ้า
  • effect คงสีจริงและเด่นขึ้นบนพื้นมืด
  • ไม่มี filter ทับจอ ซึ่งเป็นงาน compositing เพิ่มทุกเฟรม

ทดลองบน client จริงที่แมพ W0 แล้ว ฉากยังมืดและม่วงแบบ Nightmare แต่ไฟของ staff_4 ยังเป็นสีส้ม วงสายฟ้ายังเป็นม่วงสด หอกน้ำแข็งยังเป็นสีฟ้า และประกายน้ำเงินเด่นบนพื้น เทียบกับของเดิมที่ทุกอย่างกลายเป็นม่วงหม่นเหมือนกัน

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

โค้ดที่ต้องแก้

1. render3d.js: เพิ่มโหมด Nightmare ใน renderer

ให้ applyMood จำ mood ของแมพไว้ แล้วแปลงเป็นแบบ Nightmare เมื่อเปิดโหมด ต้องทำในจุดนี้ เพราะตอนเปลี่ยนแมพ loadMap เรียก this.applyMood(Di[t.id] || q[s]) ใหม่ ถ้าแก้ค่าจากข้างนอกจะถูกเขียนทับ

// Nightmare grading done with scene light, so self-lit effects keep their colour.
const NM_TINT = { sky: "#24123d", fog: "#3b2263", sun: "#b89cff", hemiSky: "#6b3fa0", hemiGround: "#1a0f26" };
const mixHex = (a, b, k) => `#${new p.Color(a).lerp(new p.Color(b), k).getHexString()}`;
const nightmareMood = (m) => ({
  ...m,
  sky: mixHex(m.sky, NM_TINT.sky, 0.85),
  fog: mixHex(m.fog, NM_TINT.fog, 0.88),
  sun: mixHex(m.sun, NM_TINT.sun, 0.55),
  sunI: m.sunI * 0.5,
  hemiSky: mixHex(m.hemiSky, NM_TINT.hemiSky, 0.75),
  hemiGround: mixHex(m.hemiGround, NM_TINT.hemiGround, 0.6),
  hemiI: m.hemiI * 0.55,
  fogNear: m.fogNear * 0.8,
  fogFar: m.fogFar * 0.85,
});

// in class Renderer3D
applyMood(t) {
  this.baseMood = t;
  t = this.nightmare ? nightmareMood(t) : t;
  // ...existing body unchanged...
}
setNightmare(on) {
  if (this.nightmare === on) return;
  this.nightmare = on;
  if (this.baseMood) this.applyMood(this.baseMood);
}

2. nightmare.js: เรียก renderer และเอา CSS ที่ทับสีออก

startNightmare(s) ได้ game object มา และ s.renderer เป็น Renderer3D อยู่แล้ว (ยืนยันบน client จริง) จึงเรียกได้ตรง ๆ ส่วน class nm-on เก็บไว้ใช้กับ banner และ vignette

// in b(), right after toggling nm-on
document.documentElement.classList.toggle("nm-on", i);
s.renderer?.setNightmare?.(i);
/* CSS: remove the filter and tint rules, keep a lighter vignette */
- html.nm-on canvas#game { filter: saturate(0.8) brightness(0.85) contrast(1.04); transition: filter 1.5s; }
- html.nm-on #nm-tint { opacity: 0.42; }
- html.nm-on #nm-veil { opacity: 1; animation: nm-breathe 5s ease-in-out infinite; }
+ html.nm-on #nm-veil { opacity: 0.45; }

ถ้าต้องการแก้น้อยที่สุดก่อน: ลด #nm-tint เหลือ ~0.2 และ filter เป็น brightness(0.92) จะช่วยได้บางส่วน แต่ effect ยังโดนย้อมอยู่ดี

ค่าที่ใช้ทดลอง (แมพ W0)

ค่า ปกติ Nightmare (เสนอ)
sky #a9c8e4 #4d556e
fog #ecd0a2 #68556d
sun #ffd9a0 #dcbbdb
sunI 2.4 1.2
hemiSky #ffe9c8 #a287ab
hemiGround #7a5530 #52382a
hemiI 1.05 0.58
fogNear / fogFar 32 / 82 25.6 / 69.7

สีคำนวณด้วย Color.lerp ของ three.js จึงผสมใน linear space ตัวเลขทั้งหมดเป็นจุดเริ่มต้นที่ปรับต่อได้ตามความชอบ

ลองเองบน client

ไฟล์ nightmare-lighting.js ใช้ทดลองแบบเสนอบนเครื่องตัวเอง ทำงานเฉพาะ client นั้น ไม่ส่งอะไรไป server

  1. เข้าแมพที่ Nightmare เปิดอยู่
  2. เปิด DevTools Console ของเกม แล้ววางเนื้อหาไฟล์และกด Enter
  3. เทียบได้ด้วย window.__nmDemo.off() และ window.__nmDemo.on()

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

ข้อควรระวัง

ข้อจำกัด