计算矩阵以反转 "flattened" 3D 渲染上下文

Calculating a matrix to invert a "flattened" 3D rendering context

上下文:

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

其中一些元素是“门户”,矩形 windows 通过它可以看到其他几个元素。重要的是要注意这些“门户”中的元素必须与其 parent 门户之外的元素存在于相同的全局 3d space(而不仅仅是 2d)中。

这些“门户”有 overflow: hidden 以隐藏其中溢出的元素。

As per the w3 specoverflow 是几个 css 属性 值之一,将 objects 组合在一起并创建二维“扁平化”渲染上下文;实际上与“transform-style: flat;”

相同的结果

问题:

我必须找到一些方法来反转 (cancel-out) 由 transform-style: flat; 创建的转换矩阵,并创建一个与使用 transform-style: preserve-3d;[=41 保留的相同的 3D rendering context =]

所有 3D css 变换在内部由 3d 矩阵表示。在 transform-style: flat 的情况下,user-agent 正在对自己的变换矩阵进行一些神秘的“扁平化”数学运算。然后这个矩阵被应用到它的 children,创造它的 children 在它们的 parent 的窗格中全部变平的错觉。应该可以通过一些矩阵数学来绕过这种影响。

不幸的是,w3 规范没有说明这种“扁平化”在数学上的含义;他们在这个问题上相当含糊,只是称其为 "flattened representation of their (the element's) content" that "has no depth."

我无法弄清楚需要进行哪些矩阵数学运算才能重现“扁平化”效果。如果可以对该算法进行逆向工程,则可以使用倒矩阵轻松地抵消这种“扁平化”行为。每个门户元素内的次要元素可用于将此倒置矩阵应用为变换,从而否定“扁平化”变换并正确恢复 3d position/rotation/perspective.

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

演示和链接:

备注:

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

我的整个 application/website 将建立在 3d space 中存在的导航模型上,那么为什么不直接使用 webgl?

  1. 搜索引擎或屏幕阅读器无法解析 Webgl
  2. Webgl不能静态生成和水化client-side
  3. 我的应用程序将包含可交互的 html。这不能在 webgl 中完成:

从技术上讲,有一些方法可以实现混合 html/webgl 网站,但它需要 css 转换和(对于我的用例)动态计算的裁剪,这在 chrome and firefox 上不起作用。进行裁剪的唯一其他方法是使用 overflow: hidden,这让我问了这个问题。

  1. 在 webgl 中构建它需要大量额外的工作来设计和构建导航和生命周期系统,否则我可以利用现有的 html 框架...我是一个人团队。
  2. 最后,webgl 中的门户不是一个容易解决的问题,我对能够“飞过”门户的要求使这个目标变得更加崇高......我需要尽可能以最简单的方式实现它我的目标只是创建一个 mvp 并将其发布给我耐心等待的测试人员。

就我的 testing/research 而言,webgl 不是我正在构建的可行替代方案(至少对于一个人的团队而言)但是,弄清楚一些数学应该是可行的.

编辑 2:部分解决方案

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

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

const flatten = identity()
flatten[10] = 1/10000 // arbitrarily small. not zero, since that will invalidate the matrix.

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

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

因此,可以合理地认为反转此效果会像 re-populating 第三列一样简单:

// normalize portal transform
const normalizedPortalTransform = normalize(portalTransform)

// try to re-establish perspective in z axis
const final = identity()
final[8] = normalizedPortalTransform[8]
final[9] = normalizedPortalTransform[9]
final[10] = normalizedPortalTransform[10]
final[11] = normalizedPortalTransform[11]

看起来有点像工作,但视角仍然不对:

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

我尝试了 re-populating 矩阵的多种不同组合,比如还包括第三行(索引 2、6、10 和 14),甚至分解 portalTransform 的透视组件使用下面的代码,并尝试将 re-incorporate 这些值放入 final de-flattening 矩阵中。但这也不管用。

// Returns the transpose of a 4x4 matrix
function transpose(matrix){
  return Array.from({ length: 16 }, (_, i) => {
    const y = i % 4
    const x = Math.floor(i / 4)
    return matrix[y * 4 + x]
  })
}

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

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

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

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

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

接下来,当视口完全被门户填满时,包含门户的场景将被删除。它被嵌入的场景所取代,只是现在传送门不见了,所以场景不再在传送门内。

为使此过渡生效,门户中项目的视角必须与门户外相同项目的视角完美匹配...否则此过渡将不顺利。

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

如果您不想使用 three.js 之类的 WebGL-lib,而是使用纯 css-transform,获得所需效果的直接方法是动态更改 perspective-origin 您的门户。

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

正常的扁平化是由 camera matrix that can be found in the documentation of the perspective parameter (see MDN) 完成的。它是透视值的函数。如果用户的视图与投影屏幕(在您的情况下是门户平面)不正交,您可以相应地更改 perspective-origin

就像在你的示例中一样,我已经使用 Rematrix 调整门户 window 的 perspective-origin。

<!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>

这是一张图表,用于勾勒调整后计算背后的想法 perspective-origin: