verify-occlusion.mjs 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. // Real render regression using the application engine and an entirely buried red 3D Tiles cube.
  2. // Run with COCKPIT_BASE and OCCLUSION_TERRAIN=local|ellipsoid; /gis/model-review mounts this directory.
  3. import assert from 'node:assert/strict'
  4. import { mkdirSync, writeFileSync } from 'node:fs'
  5. import { resolve } from 'node:path'
  6. import { PNG } from 'pngjs'
  7. import { Cartesian3, Transforms, Matrix4 } from 'cesium'
  8. const BASE = process.env.MODEL_REVIEW_BASE || 'http://127.0.0.1:8092'
  9. const USE_DEM = process.env.OCCLUSION_TERRAIN === 'local'
  10. const OUT = resolve('.scratch/cockpit-shell-panel-layout/verify/artifacts/t35/occlusion' + (USE_DEM ? '-dem' : ''))
  11. const FIXTURE = resolve('tools/city-massing/district/occlusion-fixture')
  12. mkdirSync(OUT, { recursive: true })
  13. mkdirSync(FIXTURE, { recursive: true })
  14. const p = [
  15. -40, -110, -40, 40, -110, -40, 40, -30, -40, -40, -30, -40, -40, -110, 40, 40, -110, 40, 40, -30, 40, -40, -30, 40,
  16. ]
  17. const ix = [0, 1, 2, 0, 2, 3, 4, 6, 5, 4, 7, 6, 0, 4, 5, 0, 5, 1, 3, 2, 6, 3, 6, 7, 0, 3, 7, 0, 7, 4, 1, 5, 6, 1, 6, 2]
  18. const bin = Buffer.concat([Buffer.from(new Float32Array(p).buffer), Buffer.from(new Uint16Array(ix).buffer)])
  19. const doc = {
  20. asset: { version: '2.0' },
  21. scene: 0,
  22. scenes: [{ nodes: [0] }],
  23. nodes: [{ mesh: 0 }],
  24. meshes: [{ primitives: [{ attributes: { POSITION: 0 }, indices: 1, material: 0 }] }],
  25. materials: [
  26. {
  27. extensions: { KHR_materials_unlit: {} },
  28. pbrMetallicRoughness: { baseColorFactor: [1, 0, 0, 1] },
  29. doubleSided: true,
  30. },
  31. ],
  32. extensionsUsed: ['KHR_materials_unlit'],
  33. buffers: [{ byteLength: bin.length }],
  34. bufferViews: [
  35. { buffer: 0, byteOffset: 0, byteLength: 96 },
  36. { buffer: 0, byteOffset: 96, byteLength: 72 },
  37. ],
  38. accessors: [
  39. { bufferView: 0, componentType: 5126, count: 8, type: 'VEC3', min: [-40, -110, -40], max: [40, -30, 40] },
  40. { bufferView: 1, componentType: 5123, count: 36, type: 'SCALAR' },
  41. ],
  42. }
  43. const txt = Buffer.from(JSON.stringify(doc))
  44. const js = Buffer.concat([txt, Buffer.alloc((4 - (txt.length % 4)) % 4, 32)])
  45. const header = Buffer.alloc(20)
  46. header.write('glTF')
  47. header.writeUInt32LE(2, 4)
  48. header.writeUInt32LE(28 + js.length + bin.length, 8)
  49. header.writeUInt32LE(js.length, 12)
  50. header.write('JSON', 16)
  51. const bh = Buffer.alloc(8)
  52. bh.writeUInt32LE(bin.length)
  53. bh.write('BIN\0', 4)
  54. writeFileSync(resolve(FIXTURE, 'red.glb'), Buffer.concat([header, js, bh, bin]))
  55. writeFileSync(
  56. resolve(FIXTURE, 'tileset.json'),
  57. JSON.stringify({
  58. asset: { version: '1.1' },
  59. geometricError: 1000,
  60. root: {
  61. transform: Matrix4.toArray(
  62. Transforms.eastNorthUpToFixedFrame(Cartesian3.fromDegrees(110.39028164, 28.4436787))
  63. ),
  64. boundingVolume: { box: [0, 0, -70, 40, 0, 0, 0, 40, 0, 0, 0, 40] },
  65. geometricError: 0,
  66. content: { uri: 'red.glb' },
  67. },
  68. })
  69. )
  70. const puppeteer = (
  71. await import(
  72. 'file:///C:/Users/ASUS/.dsh/profiles/desktop/node_modules/puppeteer-core/lib/puppeteer/puppeteer-core.js'
  73. )
  74. ).default
  75. const browser = await puppeteer.launch({
  76. executablePath: 'C:/Program Files/Google/Chrome/Application/chrome.exe',
  77. headless: true,
  78. defaultViewport: { width: 600, height: 600 },
  79. args: ['--no-sandbox'],
  80. })
  81. const results = []
  82. try {
  83. const page = await browser.newPage()
  84. await page.goto(BASE + '/preview.html#automation', { waitUntil: 'domcontentloaded' })
  85. await page.waitForFunction(() => typeof window.startPreview === 'function')
  86. await page.evaluate(
  87. (dem) =>
  88. window.startPreview({
  89. model: '/review-assets/district/occlusion-fixture/tileset.json',
  90. terrain: dem,
  91. imagery: false,
  92. }),
  93. USE_DEM
  94. )
  95. await page.evaluate(() => {
  96. const { v, C } = window.scene35
  97. v.camera.setView({
  98. destination: C.Cartesian3.fromDegrees(110.39028164, 28.4436787, 750),
  99. orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
  100. })
  101. document.querySelector('#caption').style.display = 'none'
  102. })
  103. await page.waitForFunction(() => window.scene35.v.scene.globe._surface._tilesToRender.length > 0, {
  104. timeout: 45000,
  105. })
  106. async function capture(name, mode) {
  107. const state = await page.evaluate((mode) => {
  108. const { v, E, tiles } = window.scene35
  109. if (mode === 'off') v.scene.globe.depthTestAgainstTerrain = false
  110. if (mode === 'on') v.scene.globe.depthTestAgainstTerrain = true
  111. if (mode === 'ground') E.applyViewModeLayers(v, tiles, { mode: 'ground', terrainAlpha: 1, tilesetAlpha: 1 })
  112. if (mode === 'pipeline')
  113. E.applyViewModeLayers(v, tiles, { mode: 'pipeline', terrainAlpha: 0.45, tilesetAlpha: 0.4 })
  114. return { depth: v.scene.globe.depthTestAgainstTerrain, translucent: v.scene.globe.translucency.enabled }
  115. }, mode)
  116. await page.waitForFunction(() => window.scene35.tiles[0].root.content != null, { timeout: 30000 })
  117. const frames = await page.evaluate(() => window.frameCount)
  118. await page.waitForFunction((f) => window.frameCount > f + 40, {}, frames)
  119. const im = PNG.sync.read(Buffer.from(await page.screenshot({ path: resolve(OUT, name + '.png') })))
  120. let red = 0
  121. for (let y = 190; y < 410; y++)
  122. for (let x = 190; x < 410; x++) {
  123. const i = (y * im.width + x) * 4
  124. if (im.data[i] > 100 && im.data[i] > im.data[i + 1] * 1.8 && im.data[i] > im.data[i + 2] * 1.8) red++
  125. }
  126. const row = { name, ...state, redPixels: red }
  127. results.push(row)
  128. console.log(JSON.stringify(row))
  129. return red
  130. }
  131. const initial = await capture('01-actual-initial', 'initial')
  132. const off = await capture('02-control-depth-off', 'off')
  133. const on = await capture('03-control-depth-on', 'on')
  134. const pipe = await capture('04-pipeline', 'pipeline')
  135. const ground = await capture('05-ground-return', 'ground')
  136. writeFileSync(
  137. resolve(OUT, 'report.json'),
  138. JSON.stringify({ terrain: USE_DEM ? 'local-dem' : 'ellipsoid', results }, null, 2)
  139. )
  140. assert(off > 100, 'Positive control must show buried model with depth off')
  141. assert(on < 5, 'Depth-on must hide buried model')
  142. assert(initial < 5, 'Actual createViewer must not see through terrain')
  143. assert(pipe > 100, 'Explicit pipeline translucency must permit viewing below ground')
  144. assert(ground < 5, 'Ground mode must restore terrain occlusion')
  145. console.log('PASS actual terrain depth, negative control, pipeline and return')
  146. } finally {
  147. await browser.close()
  148. }