计算矩阵以反转 "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 spec,overflow
是几个 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 的逆变换。不幸的是,反转“扁平”矩阵并不是那么简单:
演示和链接:
- JSFiddle demo unchanged for testing
- JSFiddle demo with the naive algorithm demonstration
- W3 spec
- MDN on
transform-style
备注:
- 涉及使用掩码绕过问题的解决方案,clip-paths,其他解决方案可能不起作用,因为它们也是 grouping property values
- 除上述之外,clip-paths 在 chrome and firefox
上有渲染问题
- 算法应该适用于不同的 parent 位置、旋转、比例和视角。我不是在寻找可以修复我的示例和仅我的示例的神奇转换值。
- 我已经尝试使用多个相互叠加的独立 3d 上下文来解决这个问题。剪辑路径用于切割门户。但是,当门户需要被其他门户或其他层中的元素遮挡时,这会导致问题。
- 鉴于我的项目背景,我愿意接受其他替代建议。
- 之前有人问过类似的问题,但是在一个截然不同的环境中,OP 正在寻找 css-only 解决方案:CSS: "overflow: hidden" alternative that doesn't break 3D transforms
编辑1:为什么不用三个js?
我的整个 application/website 将建立在 3d space 中存在的导航模型上,那么为什么不直接使用 webgl?
- 搜索引擎或屏幕阅读器无法解析 Webgl
- Webgl不能静态生成和水化client-side
- 我的应用程序将包含可交互的 html。这不能在 webgl 中完成:
从技术上讲,有一些方法可以实现混合 html/webgl 网站,但它需要 css 转换和(对于我的用例)动态计算的裁剪,这在 chrome and firefox 上不起作用。进行裁剪的唯一其他方法是使用 overflow: hidden
,这让我问了这个问题。
- 在 webgl 中构建它需要大量额外的工作来设计和构建导航和生命周期系统,否则我可以利用现有的 html 框架...我是一个人团队。
- 最后,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:
上下文:
在我的网络应用程序中,我创建了几组元素,所有元素都在 3d space 中相对于彼此定位。所有元素都有transform-style: preserve-3d
。 parent 元素用于旋转、缩放和移动所有这些元素以响应用户导航。
其中一些元素是“门户”,矩形 windows 通过它可以看到其他几个元素。重要的是要注意这些“门户”中的元素必须与其 parent 门户之外的元素存在于相同的全局 3d space(而不仅仅是 2d)中。
这些“门户”有 overflow: hidden
以隐藏其中溢出的元素。
As per the w3 spec,overflow
是几个 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 的逆变换。不幸的是,反转“扁平”矩阵并不是那么简单:
演示和链接:
- JSFiddle demo unchanged for testing
- JSFiddle demo with the naive algorithm demonstration
- W3 spec
- MDN on
transform-style
备注:
- 涉及使用掩码绕过问题的解决方案,clip-paths,其他解决方案可能不起作用,因为它们也是 grouping property values
- 除上述之外,clip-paths 在 chrome and firefox 上有渲染问题
- 算法应该适用于不同的 parent 位置、旋转、比例和视角。我不是在寻找可以修复我的示例和仅我的示例的神奇转换值。
- 我已经尝试使用多个相互叠加的独立 3d 上下文来解决这个问题。剪辑路径用于切割门户。但是,当门户需要被其他门户或其他层中的元素遮挡时,这会导致问题。
- 鉴于我的项目背景,我愿意接受其他替代建议。
- 之前有人问过类似的问题,但是在一个截然不同的环境中,OP 正在寻找 css-only 解决方案:CSS: "overflow: hidden" alternative that doesn't break 3D transforms
编辑1:为什么不用三个js?
我的整个 application/website 将建立在 3d space 中存在的导航模型上,那么为什么不直接使用 webgl?
- 搜索引擎或屏幕阅读器无法解析 Webgl
- Webgl不能静态生成和水化client-side
- 我的应用程序将包含可交互的 html。这不能在 webgl 中完成:
从技术上讲,有一些方法可以实现混合 html/webgl 网站,但它需要 css 转换和(对于我的用例)动态计算的裁剪,这在 chrome and firefox 上不起作用。进行裁剪的唯一其他方法是使用 overflow: hidden
,这让我问了这个问题。
- 在 webgl 中构建它需要大量额外的工作来设计和构建导航和生命周期系统,否则我可以利用现有的 html 框架...我是一个人团队。
- 最后,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: