three.js + imgui-js 项目中的场景渲染问题

Scene rendering issues in three.js + imgui-js project

我有一个项目正在使用 Three.jsimgui-js。我正在尝试更新到每个版本的最新版本,但我 运行 遇到了问题(它们最近一次更新是在 2020 年 2 月左右)。

初始化我调用的两个库:

  await ImGui.default();
  ImGui.CreateContext();
  ImGui_Impl.Init(canvas);
  ImGui.StyleColorsDark();
  
  const clear_color = new ImGui.ImVec4(0.3, 0.3, 0.3, 1.00);
  const renderer = new THREE.WebGLRenderer({ canvas: canvas });
  renderer.shadowMap.enabled = true;
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
  renderer.gammaFactor = 2.2;
  renderer.physicallyCorrectLights = true;
  renderer.outputEncoding = THREE.sRGBEncoding;

然后我在三个场景中添加一些对象:

  const scene = new THREE.Scene();
  
  let floorDim = 1000;
  var grid = new THREE.GridHelper(floorDim, 200, 'orange', 'white');
  grid.name = "grid";
  grid.material.opacity = 1.0;
  grid.material.transparent = true;
  grid.material.color.convertGammaToLinear(2.2);
  grid.position.set(0, 2, 0);
  scene.add(grid);
  
  var subgrid = new THREE.GridHelper(floorDim, 600, 'grey', 'grey');
  subgrid.name = "subgrid";
  subgrid.material.opacity = 1.0;
  subgrid.material.color.convertGammaToLinear(2.2);
  subgrid.material.transparent = true;
  subgrid.position.set(0, 1, 0);
  scene.add(subgrid);
  
  const light = new THREE.DirectionalLight(0xffffff, 0.8);
    light.position.set(0, 100, -50);
    light.lookAt(new THREE.Vector3(0, 0, 0));
    scene.add(light);

    const box_1_mesh = new THREE.Mesh(new THREE.BoxGeometry(5, 5, 5), new THREE.MeshLambertMaterial({ color:0x970000 }));
  box_1_mesh.position.set(0, 5, 0);
  scene.add(box_1_mesh);

    const box_2_mesh = new THREE.Mesh(new THREE.BoxGeometry(5, 5, 5), new THREE.MeshLambertMaterial({ color:0x333000 }));
  box_2_mesh.position.set(-2, 6, 7);
  scene.add(box_2_mesh);
  
  const camera = new THREE.PerspectiveCamera(50, canvas.width / canvas.height, 2, 10000);
    camera.position.set(0, 10, -25);
  camera.updateProjectionMatrix();
  camera.lookAt(0, 0, 0);
    scene.add(camera);

并调用如下所示的更新循环:

  function _loop(time) {
    ImGui_Impl.NewFrame(time);
    ImGui.NewFrame();

    ImGui.SetNextWindowPos(new ImGui.ImVec2(20, 20), ImGui.Cond.FirstUseEver);
    ImGui.SetNextWindowSize(new ImGui.ImVec2(294, 140), ImGui.Cond.FirstUseEver);
    ImGui.Begin("Visblility Toggles:");
    ImGui.Checkbox("box_1 (near)", (value = box_1_mesh.visible) => box_1_mesh.visible = value);
    ImGui.Checkbox("box_2 (distant)", (value = box_2_mesh.visible) => box_2_mesh.visible = value);
    ImGui.Checkbox("grid", (value = grid.visible) => grid.visible = value);
    ImGui.Checkbox("subgrid", (value = subgrid.visible) => subgrid.visible = value);
    
    ImGui.End();
    ImGui.EndFrame();
    ImGui.Render();
    
    renderer.setClearColor(new THREE.Color(clear_color.x, clear_color.y, clear_color.z), clear_color.w);
    renderer.setSize(canvas.width, canvas.height);
    camera.aspect = canvas.width / canvas.height;
    camera.updateProjectionMatrix();
    renderer.render(scene, camera);

    ImGui_Impl.RenderDrawData(ImGui.GetDrawData());
    renderer.state.reset();

    window.requestAnimationFrame(_loop);
  }

更新到这些库的最新版本后,我遇到了场景中的对象未按预期渲染的问题。

例如,我将 2 GridHelpers 添加到场景中,但由于某种原因只显示了一个。此外,如果在场景中切换不同 Three 对象的可见性,例如切换其中一个网格 off/on 的显示,当切换回对象时不会再次显示(有时其他物体消失)。

发生的另一个问题是,更复杂的网格材质会有意想不到的透明度,但对于我在本例中添加的简单框,该问题似乎不会发生。虽然,框的外观根据是否进行了 ImGui_Impl.RenderDrawData 调用而改变可能是相关的。

我发现,如果我注释掉对 ImGui_Impl.RenderDrawData(ImGui.GetDrawData()) 的调用,场景会按预期显示,同时显示两个网格助手,所以我怀疑两者之间可能存在一些内部冲突webGl 的库用法。尽管我绝对有可能设置不正确,或者我错过了一个需要的电话。

为了帮助演示该问题,我创建了一个复制该问题的片段:https://codepen.io/bpeake/pen/KKNNQre

(async function() {
  await ImGui.default();
  const canvas = document.getElementById("output");
  const devicePixelRatio = window.devicePixelRatio || 1;
  canvas.width = canvas.scrollWidth * devicePixelRatio;
  canvas.height = canvas.scrollHeight * devicePixelRatio;
  window.addEventListener("resize", () => {
    const devicePixelRatio = window.devicePixelRatio || 1;
    canvas.width = canvas.scrollWidth * devicePixelRatio;
    canvas.height = canvas.scrollHeight * devicePixelRatio;
  });

  ImGui.CreateContext();
  ImGui_Impl.Init(canvas);
  ImGui.StyleColorsDark();
  
  const clear_color = new ImGui.ImVec4(0.3, 0.3, 0.3, 1.00);
  const renderer = new THREE.WebGLRenderer({ canvas: canvas });
  renderer.shadowMap.enabled = true;
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
  renderer.gammaFactor = 2.2;
  renderer.physicallyCorrectLights = true;
  renderer.outputEncoding = THREE.sRGBEncoding;
  
  const scene = new THREE.Scene();
  
  let floorDim = 1000;
  var grid = new THREE.GridHelper(floorDim, 200, 'orange', 'white');
  grid.name = "grid";
  grid.material.opacity = 1.0;
  grid.material.transparent = true;
  grid.material.color.convertGammaToLinear(2.2);
  grid.position.set(0, 2, 0);
  scene.add(grid);
  
  var subgrid = new THREE.GridHelper(floorDim, 600, 'grey', 'grey');
  subgrid.name = "subgrid";
  subgrid.material.opacity = 1.0;
  subgrid.material.color.convertGammaToLinear(2.2);
  subgrid.material.transparent = true;
  subgrid.position.set(0, 1, 0);
  scene.add(subgrid);
  
  const light = new THREE.DirectionalLight(0xffffff, 0.8);
    light.position.set(0, 100, -50);
    light.lookAt(new THREE.Vector3(0, 0, 0));
    scene.add(light);

    const box_1_mesh = new THREE.Mesh(new THREE.BoxGeometry(5, 5, 5), new THREE.MeshLambertMaterial({ color:0x970000 }));
  box_1_mesh.position.set(0, 5, 0);
  scene.add(box_1_mesh);

    const box_2_mesh = new THREE.Mesh(new THREE.BoxGeometry(5, 5, 5), new THREE.MeshLambertMaterial({ color:0x333000 }));
  box_2_mesh.position.set(-2, 6, 7);
  scene.add(box_2_mesh);
  
  const camera = new THREE.PerspectiveCamera(50, canvas.width / canvas.height, 2, 10000);
    camera.position.set(0, 10, -25);
  camera.updateProjectionMatrix();
  camera.lookAt(0, 0, 0);
    scene.add(camera);
  
  window.requestAnimationFrame(_loop);
  function _loop(time) {
    ImGui_Impl.NewFrame(time);
    ImGui.NewFrame();

    ImGui.SetNextWindowPos(new ImGui.ImVec2(20, 20), ImGui.Cond.FirstUseEver);
    ImGui.SetNextWindowSize(new ImGui.ImVec2(294, 140), ImGui.Cond.FirstUseEver);
    ImGui.Begin("Visblility Toggles:");
    ImGui.Checkbox("box_1 (near)", (value = box_1_mesh.visible) => box_1_mesh.visible = value);
    ImGui.Checkbox("box_2 (distant)", (value = box_2_mesh.visible) => box_2_mesh.visible = value);
    ImGui.Checkbox("grid", (value = grid.visible) => grid.visible = value);
    ImGui.Checkbox("subgrid", (value = subgrid.visible) => subgrid.visible = value);
    
    ImGui.End();
    ImGui.EndFrame();
    ImGui.Render();
    
    renderer.setClearColor(new THREE.Color(clear_color.x, clear_color.y, clear_color.z), clear_color.w);
    renderer.setSize(canvas.width, canvas.height);
    camera.aspect = canvas.width / canvas.height;
    camera.updateProjectionMatrix();
    renderer.render(scene, camera);

    ImGui_Impl.RenderDrawData(ImGui.GetDrawData());
    renderer.state.reset();

    window.requestAnimationFrame(_loop);
  }

})();
#output {
  position: absolute;
  top: 0px;
  right: 0px;
  width: 100%;
  height: 100%;
  z-index: 1;
}
<script>
// emu localStorage otherwise ImGUI fails because S.O blocks localStorage
{
  const localStorageStorage = new Map();
  const localStorage = {
    getItem(k) { return localStorageStorage.get(k); },
    setItem(k,v) { return localStorageStorage.set(k, v); },
  };
  Object.defineProperty(window, 'localStorage', {
    get() { return localStorage; }
  });
}
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r125/three.min.js"></script>
<script src="https://flyover.github.io/imgui-js/dist/imgui.umd.js"></script>
<script src="https://flyover.github.io/imgui-js/dist/imgui_impl.umd.js"></script>
<script src="https://flyover.github.io/nanovg-js/dist/nanovg.umd.js"></script>
<canvas tabindex="0" id="output"></canvas>

我设置的 imgui-jsthree-js 有什么问题吗?关于我可以做什么以使 ImGui_Impl.RenderDrawData 调用不影响我的 three.js 场景的渲染的任何想法?

问题是 imgui-js 破坏了属性状态。

你可能想考虑 运行 imgui-js 在另一个 canvas 覆盖在 three.js canvas,每个都有自己的 WebGL 上下文。这样他们就不用担心了。

这是一个快速破解方法

    const gl = renderer.getContext();
    window.vao = window.vao || gl.createVertexArray();
    const oldVao = gl.getParameter(gl.VERTEX_ARRAY_BINDING);
    gl.bindVertexArray(vao);
    
    ImGui_Impl.RenderDrawData(ImGui.GetDrawData());
    
    gl.bindVertexArray(oldVao);

但该 hack 仅适用于 WebGL2。要完全保存和恢复丢失的状态并不难,但处理 WebGL1、扩展和 WebGL2 可能需要 50-80 行代码。

我提交了 issue here with an example PR

(async function() {
  await ImGui.default();
  const canvas = document.getElementById("output");
  const devicePixelRatio = window.devicePixelRatio || 1;
  canvas.width = canvas.scrollWidth * devicePixelRatio;
  canvas.height = canvas.scrollHeight * devicePixelRatio;
  window.addEventListener("resize", () => {
    const devicePixelRatio = window.devicePixelRatio || 1;
    canvas.width = canvas.scrollWidth * devicePixelRatio;
    canvas.height = canvas.scrollHeight * devicePixelRatio;
  });

  ImGui.CreateContext();
  ImGui_Impl.Init(canvas);
  ImGui.StyleColorsDark();

  const clear_color = new ImGui.ImVec4(0.3, 0.3, 0.3, 1.00);
  const renderer = new THREE.WebGLRenderer({ canvas: canvas });
  renderer.shadowMap.enabled = true;
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
  renderer.gammaFactor = 2.2;
  renderer.physicallyCorrectLights = true;
  renderer.outputEncoding = THREE.sRGBEncoding;
  
  const scene = new THREE.Scene();
  
  let floorDim = 1000;
  var grid = new THREE.GridHelper(floorDim, 200, 'orange', 'white');
  grid.name = "grid";
  grid.material.opacity = 1.0;
  grid.material.transparent = true;
  grid.material.color.convertGammaToLinear(2.2);
  grid.position.set(0, 2, 0);
  scene.add(grid);
  
  var subgrid = new THREE.GridHelper(floorDim, 600, 'grey', 'grey');
  subgrid.name = "subgrid";
  subgrid.material.opacity = 1.0;
  subgrid.material.color.convertGammaToLinear(2.2);
  subgrid.material.transparent = true;
  subgrid.position.set(0, 1, 0);
  scene.add(subgrid);
  
  const light = new THREE.DirectionalLight(0xffffff, 0.8);
    light.position.set(0, 100, -50);
    light.lookAt(new THREE.Vector3(0, 0, 0));
    scene.add(light);

    const box_1_mesh = new THREE.Mesh(new THREE.BoxGeometry(5, 5, 5), new THREE.MeshLambertMaterial({ color:0x970000 }));
  box_1_mesh.position.set(0, 5, 0);
  scene.add(box_1_mesh);

    const box_2_mesh = new THREE.Mesh(new THREE.BoxGeometry(5, 5, 5), new THREE.MeshLambertMaterial({ color:0x333000 }));
  box_2_mesh.position.set(-2, 6, 7);
  scene.add(box_2_mesh);
  
  const camera = new THREE.PerspectiveCamera(50, canvas.width / canvas.height, 2, 10000);
    camera.position.set(0, 10, -25);
  camera.updateProjectionMatrix();
  camera.lookAt(0, 0, 0);
    scene.add(camera);

  setTimeout(() => {
    grid.visible = false;
  }, 1000);
  setTimeout(() => {
    grid.visible = true;
  }, 2000);
  setTimeout(() => {
    console.log(grid.visible);
  }, 2500);

  window.requestAnimationFrame(_loop);
  function _loop(time) {
    ImGui_Impl.NewFrame(time);
    ImGui.NewFrame();

    ImGui.SetNextWindowPos(new ImGui.ImVec2(20, 20), ImGui.Cond.FirstUseEver);
    ImGui.SetNextWindowSize(new ImGui.ImVec2(294, 140), ImGui.Cond.FirstUseEver);
    ImGui.Begin("Visblility Toggles:");
    ImGui.Checkbox("box_1 (near)", (value = box_1_mesh.visible) => box_1_mesh.visible = value);
    ImGui.Checkbox("box_2 (distant)", (value = box_2_mesh.visible) => box_2_mesh.visible = value);
    ImGui.Checkbox("grid", (value = grid.visible) => grid.visible = value);
    ImGui.Checkbox("subgrid", (value = subgrid.visible) => subgrid.visible = value);
    
    
    ImGui.End();
    ImGui.EndFrame();
    ImGui.Render();

    renderer.setClearColor(new THREE.Color(clear_color.x, clear_color.y, clear_color.z), clear_color.w);
    renderer.setSize(canvas.width, canvas.height);
    camera.aspect = canvas.width / canvas.height;
    camera.updateProjectionMatrix();
    renderer.render(scene, camera);

    const gl = renderer.getContext();
    window.vao = window.vao || gl.createVertexArray();
    const oldVao = gl.getParameter(gl.VERTEX_ARRAY_BINDING);
    gl.bindVertexArray(vao);
    
    ImGui_Impl.RenderDrawData(ImGui.GetDrawData());
    
    gl.bindVertexArray(oldVao);

    renderer.state.reset();

    window.requestAnimationFrame(_loop);
  }

})();
#output {
  position: absolute;
  top: 0px;
  right: 0px;
  width: 100%;
  height: 100%;
  z-index: 1;
}
<script>
// emu localStorage otherwise ImGUI fails because S.O blocks localStorage
{
  const localStorageStorage = new Map();
  const localStorage = {
    getItem(k) { return localStorageStorage.get(k); },
    setItem(k,v) { return localStorageStorage.set(k, v); },
  };
  Object.defineProperty(window, 'localStorage', {
    get() { return localStorage; }
  });
}
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r125/three.js"></script>
<script src="https://flyover.github.io/imgui-js/dist/imgui.umd.js"></script>
<script src="https://flyover.github.io/imgui-js/dist/imgui_impl.umd.js"></script>
<script src="https://flyover.github.io/nanovg-js/dist/nanovg.umd.js"></script>
<canvas tabindex="0" id="output"></canvas>