计算矩阵以反转“扁平化”3D 渲染上下文

Mar*_*lin 6 html javascript css 3d matrix

语境:

\n

在我的网络应用程序中,我创建了几组元素,所有元素都在 3d 空间中相对定位。所有元素都有transform-style: preserve-3d. 父元素用于旋转、缩放和移动所有这些元素以响应用户导航。

\n

其中一些元素是“门户”,即矩形窗口,通过它可以看到其他几个元素。需要注意的是,这些“门户”内的元素必须与其父门户之外的元素存在于同一全局 3d 空间(而不仅仅是 2d)中。

\n

这些“门户”是overflow: hidden为了隐藏其中溢出的元素。

\n

Web 应用程序图

\n

根据 w3 规范overflow是几个 css 属性值之一,它将对象组合在一起并创建 2d \xe2\x80\x9cflattened\xe2\x80\x9d 渲染上下文;实际上与 \xe2\x80\x9c 相同的结果transform-style: flat;\xe2\x80\x9d

\n

问题:

\n

我必须找到某种方法来反转(取消)由创建的变换矩阵,transform-style: flat;并创建与使用保留的相同的3D 渲染上下文transform-style: preserve-3d;

\n

所有 3D css 变换都由内部的 3d 矩阵表示。transform-style: flat在用户代理的情况下,正在对其自己的变换矩阵进行一些神秘的“扁平化”数学运算。然后这个矩阵被应用到它的子项上,创造出它的子项都被展平在父项窗格中的错觉。通过一些矩阵数学应该可以绕过这种影响。

\n

不幸的是,w3 规范没有详细说明这种“扁平化”在数学上的含义;他们在这个主题上相当模糊,简单地称之为“他们(元素)内容的扁平化表示”“没有深度”。

\n

我无法弄清楚需要进行哪些矩阵数学来重现“展平”效果。如果可以对算法进行逆向工程,则可以通过倒置矩阵轻松消除这种“扁平化”行为。每个门户元素内的辅助元素可用于应用此倒置矩阵作为变换,从而否定“展平”变换并正确恢复 3d 位置/旋转/视角。

\n

下面显示的演示演示了完全有可能在扁平化的父级中创建深度印象。在这种情况下,我天真地应用了最顶层父级变换的逆。不幸的是,反转“展平”矩阵并不那么简单:

\n

简单算法演示的演示屏幕截图

\n

演示和链接:

\n\n

笔记:

\n
    \n
  • 涉及使用掩码、剪辑路径等绕过问题的解决方案可能不起作用,因为它们也在对属性值进行分组
  • \n
  • 除了上述之外,clip-paths 在chromefirefox上还存在渲染问题
  • \n
  • 算法应该适用于不同的父位置、旋转、比例和视角。我并不是在寻找一个神奇的变换值来修复我的示例并且仅修复我的示例。
  • \n
  • 我已经尝试使用多个相互叠加的独立 3D 上下文来解决这个问题。剪辑路径用于剪切门户。然而,当门户需要被其他门户或其他层中的元素遮挡时,这会导致问题。
  • \n
  • 鉴于我的项目背景,我愿意接受其他替代建议。
  • \n
  • 类似的问题之前已经被问过,但是在一个非常不同的上下文中,OP正在寻找仅CSS的解决方案:CSS:“溢出:隐藏”替代方案不会破坏3D变换
  • \n
\n

编辑1:为什么不使用三个js?

\n

我的整个应用程序/网站将构建在 3D 空间中存在的导航模型上,那么为什么不只使用 webgl 呢?

\n
    \n
  1. Webgl 无法被搜索引擎或屏幕阅读器解析
  2. \n
  3. Webgl 无法静态生成和水合客户端
  4. \n
  5. 我的应用程序将包含可交互的 html。这不能在 webgl 中完成:
  6. \n
\n

从技术上讲,有多种方法可以实现混合 html/webgl 网站,但它需要 css 转换和(对于我的用例)动态计算剪切,这在chromefirefox上不起作用。进行剪辑的唯一其他方法是使用overflow: hidden,这导致我提出这个问题。

\n
    \n
  1. 在 webgl 中构建它需要大量额外的工作来设计和构建导航和生命周期系统,否则我可以从现有的 html 框架中利用这些系统......我是一个人团队。
  2. \n
  3. 最后,webgl 中的门户并不是一个需要解决的简单问题,而我对能够“穿过”门户的要求使该目标变得更加崇高......如果我的目标只是为了创建一个 MVP 并将其分发给我耐心等待的测试人员。
  4. \n
\n

就我的测试/研究而言,webgl 对于我正在构建的东西来说并不是一个可行的替代方案(至少对于一个人的团队来说),但是,计算出一些数学知识应该是可行的。

\n

编辑2:部分解决方案

\n

感谢@Markus'的回答,我发现可以通过删除与 z 轴变换相关的列中的所有值来实现展平效果。这可以通过将其变换乘以以下矩阵来完成:

\n

取单位矩阵,将第 3 列中的第 3 项更改为任意小数。

\n
const flatten = identity()\nflatten[10] = 1/10000 // arbitrarily small. not zero, since that will invalidate the matrix.\n
Run Code Online (Sandbox Code Playgroud)\n

这是一个演示: https: //jsfiddle.net/aywbe9p7/2/

\n

展平矩阵的演示

\n

这似乎表明,浏览器在内部通过删除第三列中的值将 3d 4x4 矩阵转换为 2d 3x3 矩阵。

\n

因此,有理由认为反转这种效果就像重新填充第三列一样简单:

\n
// normalize portal transform\nconst normalizedPortalTransform = normalize(portalTransform)\n\n// try to re-establish perspective in z axis\nconst final = identity()\nfinal[8] = normalizedPortalTransform[8]\nfinal[9] = normalizedPortalTransform[9]\nfinal[10] = normalizedPortalTransform[10]\nfinal[11] = normalizedPortalTransform[11]\n
Run Code Online (Sandbox Code Playgroud)\n

这似乎是一种工作,但观点仍然不正确:

\n

这是一个演示: https: //jsfiddle.net/aywbe9p7/3/

\n

错误去扁平化算法的演示

\n

我尝试了许多不同的重新填充矩阵的组合,例如还包括第三行(索引 2、6、10 和 14),甚至portalTransform使用下面的代码分解透视组件,并尝试重新将这些值合并到去final平坦矩阵中。但这也行不通。

\n
// Returns the transpose of a 4x4 matrix\nfunction transpose(matrix){\n  return Array.from({ length: 16 }, (_, i) => {\n    const y = i % 4\n    const x = Math.floor(i / 4)\n    return matrix[y * 4 + x]\n  })\n}\n\n// Decompose the perspective component of a 4x4 matrix.\n// https://www.w3.org/TR/css-transforms-2/#decomposing-a-3d-matrix\nfunction decompPerspective(matrix){\n  // There exists some perspective\n  if(matrix[15] != 0 && (matrix[3] != 0 || matrix[7] != 0 || matrix[11] != 0)){\n    // Normalize the matrix.\n    const m = normalize(matrix)\n    \n    // Used to solve for perspective\n    const perspectiveMatrix = Array.from(m)\n    perspectiveMatrix[3] = 0\n    perspectiveMatrix[7] = 0\n    perspectiveMatrix[11] = 0\n    perspectiveMatrix[15] = 1\n    \n    // The right hand side of the equation.\n    const r0 = m[3]\n    const r1 = m[7]\n    const r2 = m[11]\n    const r3 = m[15] // should be 1\n    \n    // Solve the equation by inverting perspectiveMatrix and multiplying\n    // rightHandSide by the inverse.\n    const f = transpose(inverse(perspectiveMatrix))\n    \n    return [\n      f[0] * r0 + f[4] * r1 + f[8] * r2 + f[12] * r3,\n      f[1] * r0 + f[5] * r1 + f[9] * r2 + f[13] * r3,\n      f[2] * r0 + f[6] * r1 + f[10] * r2 + f[14] * r3,\n      f[3] * r0 + f[7] * r1 + f[11] * r2 + f[15] * r3, // should be 1\n    ]\n  }\n  // No perspective\n  else{\n    return [0, 0, 0, 1]\n  }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

编辑 2:为什么数学需要完美而不是近似

\n

如果不制作动画,这对我来说可能有点难以解释,但想象一下您正在俯视一个包含门户的场景。在这个传送门内,又是另一番景象。

\n

我想在查看顶级场景和查看嵌入在门户中的场景之间制作动画。

\n

为了做到这一点,首先将视觉视角动画化为“飞向”门户元素。

\n

接下来,当视口被入口完全填充时,包含入口的场景将被删除。它被嵌入的场景所取代,只是现在门户消失了,因此场景不再位于门户内部。

\n

为了使这种过渡发挥作用,门户中项目的视角必须与门户外相同项目的视角完美匹配……否则这种过渡将不会顺利。

\n

如果这没有意义,请告诉我。我也许可以做一个例子来更好地证明这一点。

\n

Mar*_*kus 3

如果您不想使用像 WebGL-lib 这样的东西,three.js而是纯粹的 css-transform,那么获得所需效果的直接方法是动态更改门户的透视图原点。

请注意,Firefox 在 3D-css 方面仍然存在一些问题。例如,我建议不要在对象上使用任何边距或填充。

正常的展平是由相机矩阵完成的,可以在透视参数的文档中找到(参见MDN)。它是视角值的函数。如果用户的视图与投影屏幕不正交(在您的情况下是门户平面),您可以perspective-origin相应地更改。

就像在您的示例中一样,我曾经Rematrix用来调整门户窗口的透视图原点。

<!DOCTYPE HTML>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://unpkg.com/rematrix"></script>
<style>
.rect {
  width:400px;
  height:200px;
  border: 2px solid red;
}
.scene {
  perspective-origin: center;
}
.main {
  position: relative;
  transform-style: preserve-3d;
  display:inline-block;
  vertical-align:top;
}
.inner {
  position: absolute;
}
.portal{
  overflow:hidden;
}
</style>
<body>
</head>
<div id="scene" width=400px>
 <div id="scene1" class="scene rect">
  <div id="obj1" class="main rect">
    <div class="inner rect" style="transform:translateZ(-60px)"></div>
    <div class="inner rect" style="transform:translateZ(-120px)"></div>
    <div class="inner rect" style="transform:translateZ(-180px);background-color:#eee">preserve-3d</div>
  </div>
 </div>
 <div id="scene2" class="scene rect"> 
  <div id="obj2" class="main portal rect">
   <div class="inner rect" style="transform:translateZ(-60px)"></div>
   <div class="inner rect" style="transform:translateZ(-120px)"></div>
   <div class="inner rect" style="transform:translateZ(-180px);background-color:#eee">overflow hidden</div>
  </div>
 </div>
</div>
<script>
let r = Rematrix;
let obj1 = $("#obj1");
let obj2 = $("#obj2");
let scene = $("#scene");
let scene1 = $("#scene1");
let scene2 = $("#scene2");
// locate objects behind center of scene
let tx = (scene1.width() - obj1.width()) / 2;
let ty = (scene1.height() - obj1.height()) / 2;
let tz = -1000;
// set perspective
let p = 2000;
scene1.css("perspective", "" + p + "px");
scene2.css("perspective", "" + p + "px");
// initial camera position
let camPos = [0, 0, p, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
// start rendering
move(0.6, 0.6);
scene.mousemove(function(event){ move(event.clientX / scene.width(), event.clientY / scene.height()); });

function move(x, y) {
  // move objects
  let dx = 120 * (x - 0.5);
  let dy = -120 * (y - 0.5);
  let t = r.toString([r.translate3d(tx, ty, tz), r.rotateX(dy), r.rotateY(dx)].reduce(r.multiply));
  obj1.css("transform", t);
  obj2.css("transform", t);
  // adjust camera perspective of portal object
  let c = [r.rotateY(-dx), r.rotateX(-dy), r.translateZ(-tz), camPos].reduce(r.multiply);
  let px = c[0] + obj2.width() / 2;
  let py = c[1] + obj2.height() / 2;
  let pz = c[2];
  obj2.css("perspective", "" + pz + "px");
  obj2.css("perspectiveOrigin", "" + px + "px " + py + "px");
}
</script>
</body>
Run Code Online (Sandbox Code Playgroud)

下面的图表描绘了调整后的透视原点计算背后的想法: 门户.png

  • 我调整了相机视角的计算,使其不再是近似的。 (2认同)