我一直很喜欢 3D 几何图形。 我一发现 CSS 对 3D 变换的支持变得越来越好,就开始使用它了。 虽然用变换来创建 2D 形状,然后在 3D 中移动/旋转它们来创建多面体感觉很自然,但最初有一些东西让我很头疼。 我认为我可以写一些关于让我感到惊讶的事情以及我遇到的挑战,这样你就可以避免这些问题了。
3D 渲染上下文
我清楚地记得,我第一次遇到这个问题是在一个晚上,当时我突然好奇,就想写一个快速测试,看看浏览器如何处理平面交集。 测试包含两个平面元素
<div class='plane'></div>
<div class='plane'></div>
它们的大小相同,绝对定位在屏幕中央,并设置了背景,以便它们可见
$dim: 40vmin;
.plane {
position: absolute;
top: 50%; left: 50%;
margin: -.5*$dim;
width: $dim; height: $dim;
background: #ee8c25;
}
场景是整个 body
元素,覆盖整个视窗,并设置了 perspective
,以便更远的元素看起来更小,更近的元素看起来更大
body {
margin: 0;
height: 100vh;
perspective: 40em;
}
为了真正测试平面交集,第二个平面元素被赋予了 rotateY()
变换和一个不同的背景
.plane:last-child {
transform: rotateY(60deg);
background: #d14730;
}
结果令人失望。 似乎没有一个浏览器可以正确地处理平面交集
查看 CodePen 上 Ana Tudor (@thebabydino) 制作的 测试平面交集(错误!)。
但我错了。 这正是我的代码应该产生的结果。 我应该做的是将这两个平面放在同一个 3D 渲染上下文 中。 如果你不熟悉 3D 渲染上下文,它们与 堆叠上下文 没有太大区别。 就像我们不能通过 z-index
来排列不在同一个堆叠上下文中的元素一样,如果 3D 变换的元素不在同一个 3D 渲染上下文,就不能在 3D 中排列或使它们相交。
确保它们在同一个 3D 渲染上下文中最简单的方法是将它们放在另一个元素中
<div class='assembly'>
<div class='plane'></div>
<div class='plane'></div>
</div>
然后将这个容器元素绝对定位在场景中央,并在它上面设置 transform-style: preserve-3d
div { position: absolute; }
.assembly {
top: 50%; left: 50%;
transform-style: preserve-3d;
}
这解决了问题
查看 CodePen 上 Ana Tudor (@thebabydino) 制作的 测试平面交集(正确)。
如果你使用 Firefox 查看上面的演示,你仍然看不到平面如预期的那样相交,因为 Firefox 仍然没有解决这个问题。 但是你应该看到它们在 WebKit 浏览器和 Edge 中相交。 **更新**:此问题已在 Firefox 55+ 中修复。
现在你可能想知道为什么要费心添加那个容器元素,仅仅在场景(在本例中是 body
元素)上添加 transform-style: preserve-3d
不行吗? 好吧,在这个特定的例子中,如果我们在初始演示中只添加这一条规则,而不添加其他任何规则,它确实有效(除非你在 Firefox 54 或更早版本中查看它)
查看 CodePen 上 Ana Tudor (@thebabydino) 制作的 测试平面交集(有效,但…)。
如果我们要在实际网页上使用 3D,我们的场景可能不会是 body
元素,而且我们可能想在场景中添加其他属性。 这些属性可能会干扰 3D 效果。
破坏 3D(或导致扁平化)的事项
假设我们的场景应该是页面中的另一个 div
,并且我们周围还有其他东西
查看 CodePen 上 Ana Tudor (@thebabydino) 制作的 两个平面在较小的场景中 #0。
我还添加了第二个平面上的几个变换,以便更清楚地表明它正在从场景中出来。 这正是我们不希望发生的。 我们希望能够阅读文本,与可能存在的控件进行交互,等等。
overflow
1) 第一个想到的方法是简单地在场景上设置 overflow: hidden
。 但是,当我们这样做的时候,我们失去了美丽的 3D 交集

这是因为给 overflow
设置除 visible
以外的任何值,都会强制 transform-style
的值变为 flat
,即使我们已经明确地将其设置为 preserve-3d
。 因此,使用容器确实意味着要写更多的代码,但可以为我们节省很多麻烦。

这就是为什么我现在将场景中的所有东西都放在一个容器元素中,即使该元素没有在 3D 中变换。 例如,考虑以下演示
查看 CodePen 上 Ana Tudor (@thebabydino) 制作的 蓝色六边形螺旋糖果(纯 CSS 3D)。
所有旋转的六边形柱都放在一个 .helix
元素中
<div class='helix'>
<div class='col'>
<!-- all the hexagons inside a column -->
</div>
<!-- the other columns -->
</div>
这个 .helix
元素没有其他样式(直接设置或继承),除了确保整个组件绝对定位在视窗中央,并且所有柱都处于同一个 3D 渲染上下文中
div {
position: absolute;
transform-style: preserve-3d;
}
.helix { top: 50%; left: 50%; }
这是因为我将 overflow: hidden
设置在场景(在本例中是 body
元素)上,因为六边形的大小不依赖于视窗,所以我不知道它们是否会延伸到外面(并导致滚动条,我不希望出现这种情况)。
我承认,在我吸取教训之前,曾不止一次遇到过这个问题。 说句公道话,在某些情况下,overflow: hidden
的效果可能并不明显。
transform-style: preserve-3d
告诉浏览器,它所设置的元素的 3D 变换的子元素不应该被扁平化到其父元素(我们设置 transform-style: preserve-3d
的元素)的平面中。 因此,即使直观地来说,它也有一定的道理,即在同一个元素上设置 overflow: hidden
也会撤销这种效果,并阻止子元素突破其父元素的平面。
但是,有时 3D 变换的子元素仍然可以在其父元素的平面内。 考虑以下情况:我们有一张卡片,上面有两面
<div class='card'>
<div class='face'>front</div>
<div class='face'>back</div>
</div>
我们将它们都绝对定位在场景(在本例中是 body
元素)的中央,给卡片及其面设置相同的尺寸,在卡片上设置 transform-style: preserve-3d
,在面上设置 backface-visibility: hidden
,并围绕其垂直轴旋转第二个面半圈
$dim: 40vmin;
div {
position: absolute;
width: $dim; height: $dim;
}
.card {
top: 50%; left: 50%;
margin: -.5*$dim;
transform-style: preserve-3d;
}
.face {
backface-visibility: hidden;
background: #ee8c25;
&:last-child {
transform: rotateY(.5turn);
background: #d14730;
}
}
演示可以在下面看到
查看 CodePen 上 Ana Tudor (@thebabydino) 制作的 卡片 #0。
两面仍然在其父元素的平面内,只是背面围绕其垂直轴旋转了半圈。 它面对相反的方向,但仍然在同一个平面内。 到目前为止,一切看起来都很棒。
现在假设我们不希望面是矩形的。 改变它的最简单方法是在卡片上设置 border-radius: 50%
。 但是,这似乎什么也没做。
因此,我们在上面设置 overflow: hidden
查看 CodePen 上 Ana Tudor (@thebabydino) 制作的 卡片 #2。
糟糕,这破坏了我们的 3D 卡片! 由于我们不能这样做,我们需要对面的角进行圆角处理
.face { border-radius: 50%; }
查看 CodePen 上 Ana Tudor (@thebabydino) 制作的 卡片 #3。
在这种情况下,解决问题的方法甚至比造成问题的方法更简单。 但是,如果我们想要另一个形状,例如正八边形呢? 用两个元素(或一个元素和一个伪元素)就可以很容易地实现正八边形
<div class='octagon'>
<div class='inner'></div>
</div>
我们给它们都设置相同的尺寸,将 .inner
元素旋转 45deg
,赋予它一个背景,以便我们能看到它,然后在 .octagon
元素上设置 overflow: hidden
$dim: 65vmin;
div { width: $dim; height: $dim; }
.octagon { overflow: hidden; }
.inner {
transform: rotate(45deg);
background: #ee8c25;
}
结果可以在下面的 CodePen 中看到
查看 CodePen 上 Ana Tudor (@thebabydino) 制作的 如何:基本正八边形(纯 CSS)。
如果我们添加文本…
<div class='octagon'>
<div class='inner'>octagon</div>
</div>
问题是它在一个角落被剪掉了,所以我们把它放大,用 text-align: center
水平对齐它,并通过给它一个与 .octagon
(或 .inner
)元素的尺寸相同的行高,将其垂直居中
.inner {
font: 10vmin/ #{$dim} sans-serif;
text-align: center;
}
现在它看起来好多了,但文本仍然是旋转的,因为我们在 .inner
元素上设置了旋转
查看 CodePen 上 Ana Tudor (@thebabydino) 制作的 带文本的八边形 #1。
为了解决这个问题,我们在 .octagon
元素上添加了一个反向旋转(角度相同,但方向相反,因此是负数)
.octagon { transform: rotate(-45deg); }
我们得到了一个带文本的八边形!
查看 CodePen 上 Ana Tudor (@thebabydino) 制作的 带文本的八边形 - 最终版!。
现在让我们看看如何将它应用到想要一个八面体面的卡片上。我们不能在卡片本身设置overflow: hidden
(使其扮演.octagon
元素的角色,而面部则像.inner
元素一样),因为这会破坏东西,而且我们不再有一个漂亮的具有两个不同面的 3D 卡片了。
查看 CodePen 上 Ana Tudor(@thebabydino)的 卡片 #4。
相反,我们需要让每个面部扮演.octagon
元素的角色,并使用伪元素扮演内部元素的角色。
.face {
overflow: hidden;
transform: rotate(45deg);
backface-visibility: hidden;
&:before {
left: 0;
transform: rotate(-45deg);
background: #ee8c25;
content: 'front';
}
&:last-child {
transform: rotateY(.5turn) rotate(45deg);
&:before {
background: #d14730;
content: 'back'
}
}
}
这给了我们我们一直想要的结果。
查看 CodePen 上 Ana Tudor(@thebabydino)的 卡片 #5。
clip-path
2) 另一个可能导致类似问题的属性是clip-path
。回到我们的卡片示例,我们不能通过在.card
元素本身应用clip-path
来使其呈三角形,因为我们需要它有一个 3D 变换后的子元素,即第二个面。我们应该将其应用于卡片面部而不是。
.face { clip-path: polygon(100% 50%, 0 0, 0 100%); }
请注意,clip-path
属性仍然需要-webkit-
前缀才能用于 WebKit 浏览器,在 Firefox 47-53 中将about:config
中的layout.css.clip-path-shapes.enabled
标志设置为true
(该标志在 Firefox 54+ 中默认设置为true
),并且在 Edge 中尚不支持(但您可以投票实施)。
添加上面的代码行后的结果将如下所示。
查看 CodePen 上 Ana Tudor(@thebabydino)的 卡片 #6。
没有 3D 问题,但看起来真的很别扭。如果卡片从正面看是一个指向右边的三角形,那么从背面看它应该指向左边。但事实并非如此,它也指向右边。解决这个问题的一个方法是为每个面使用不同的clip-path
值。使用相同的指向右边的三角形剪裁正面,并使用另一个指向左边的三角形剪裁背面。
.face:last-child { clip-path: polygon(0 50%, 100% 0, 100% 100%); }
结果正是我们想要的。
查看 CodePen 上 Ana Tudor(@thebabydino)的 卡片 #7。
请注意,我还更改了text-align
值:正面的默认值为left
,背面的值为right
。
或者,我们也可以在背面添加一个scaleX(-1)
到变换链中(如果您需要提醒缩放是如何工作的,请查看此交互式演示)。
.face:last-child { transform: rotateY(.5turn) scaleX(-1); }
查看 CodePen 上 Ana Tudor(@thebabydino)的 卡片 #8。
在这种情况下,形状看起来很好,但文本是反向的。这意味着我们实际上将文本和背景放置在一个伪元素上,在该伪元素上,我们在.face
元素上反转了缩放比例。反转一个因子为f
的缩放比例意味着设置另一个因子为1/f
的缩放比例。在我们的例子中,f
因子为-1
,所以我们在伪元素上寻找的缩放值是1/-1 = -1
。
.face:last-child:before {
transform: scaleX(-1);
background: #d14730;
text-align: right;
content: 'back';
}
最终结果可以在此笔中看到。
查看 CodePen 上 Ana Tudor(@thebabydino)的 卡片 #9。
设置为none
以外的任何值的遮罩属性也可以强制transform-style
的已用值为flat
,就像overflow
或clip-path
当设置为visible
和none
以外的值时一样。
opacity
3) 这是一个意料之外的。
它也是对规范的相对较新的更改,以便opacity
小于1
的效果对 3D 渲染上下文的影响与对堆叠上下文的影响一致。这就是为什么小于 1 的opacity
实际上并没有强制 Edge 或 Safari 压平……直到现在!然而,它确实在 Chrome、Opera 和 Firefox 中产生了这种影响。
考虑以下演示,一群立方体在 3D 中一起旋转。
查看 CodePen 上 Ana Tudor(@thebabydino)的 立方体组合 #0。
在结构上,这意味着一个包含一堆.cube
元素的.assembly
元素,每个元素都有6
个面。
<div class='assembly'>
<div class='cube'>
<div class='cube__face'></div>
<!-- five more cube faces -->
</div>
<!-- more cubes, each with 6 faces -->
</div>
现在假设我们想要使立方体半透明。我们不能这样做。
.cube { opacity: .5; }
这会导致.cube
元素上的transform-style
值被强制为flat
,即使我们已经将其设置为preserve-3d
,这会导致立方体面被压平到其.cube
父元素的平面中。目前只在 Chrome、Opera 和 Firefox 中,但其他浏览器将来也会实施这一点。


我们也不能在.assembly
元素上设置opacity: .5
,因为我们也已经在其上设置了transform-style
为preserve-3d
。所以,同样,结果将在不同浏览器之间不一致,因为新规范强制压平,而一些仍然遵循旧规范(没有强制压平)。
我们可以做的是在立方体面的元素上设置opacity: .5
,这样就不会遇到任何麻烦。
查看 CodePen 上 CSS-Tricks(@css-tricks)的
立方体组合 #3。
在 CodePen 上。
我们也可以将其设置在场景元素上,但请注意,这也会影响我们可能拥有的任何场景background
或伪元素。它也不会使各个立方体或面部半透明,而是整个组合。它也不允许我们为不同的立方体设置不同的不透明度值。


filter
4) 这又是另一个让我感到惊讶的地方,与opacity
不同,它并不新,而且结果在所有浏览器中都是一致的。让我们再看看立方体示例。假设我们想要通过hue-rotate()
为每个立方体设置一个随机不同的色调。在立方体或组合上设置filter
值为none
以外的值会导致扁平化的表示。
$n: 20; // number of cubes
@for $i from 0 to $n {
$angle: random(360)*1deg;
.cube:nth-child(#{$i + 1}) {
filter: hue-rotate($angle);
}
}
请注意,直到最近,filter
仍然需要-webkit-
前缀才能用于所有 WebKit 浏览器,而且,虽然目前所有主要桌面浏览器现在都支持它没有前缀,但大多数移动浏览器仍然需要此前缀。
这确实可以为每个立方体设置随机色调,但它也会将它们压平。

在这种情况下,解决方案是在循环中在立方体面上设置filter
。
$n: 20; // number of cubes
@for $i from 0 to $n {
$angle: random(360)*1deg;
.cube:nth-child(#{$i + 1}) .cube__face {
filter: hue-rotate($angle);
}
}
这给了我们我们想要的东西,立方体以随机色调呈现,并且仍然是 3D 的,而不是扁平化的。
查看 CodePen 上 CSS-Tricks(@css-tricks)的
立方体组合 #6。
在 CodePen 上。
我们也不能在整个组合上设置filter
。考虑我们想要模糊整个组合的情况。假设我们这样做。
.assembly { filter: blur(4px); }

我们可以做的是尝试在面部元素上应用blur()
过滤器,尽管结果不会完全符合预期,因为我们模糊的是各个面,而不是立方体本身。它看起来也很糟糕,Blink 浏览器出现了一些闪烁、丢失面部,并且blur()
过滤器的速度明显降低,而 Edge 则完全搞乱了。Firefox 在这里似乎表现最好。
我们也可以尝试将其应用于场景,尽管看起来很糟糕(有时会出现闪烁,面部在 Chrome 中消失,在 Firefox 中消失,整个组合也完全消失,而 Edge 则根本不显示任何东西)。
我感到惊讶,因为下一个更简单的旋转立方体演示也对场景应用了blur()
过滤器,并且它在 Blink 浏览器和 Edge 中似乎大部分情况下都能正常工作。然而,在 Firefox 中什么都没有显示。
查看 CodePen 上 CSS-Tricks(@css-tricks)的
粘性立方体(纯 CSS 3D,不含 Firefox)。
在 CodePen 上。
总的来说,过滤器与 3D 的组合通常会出现问题,因此我建议谨慎使用。
mix-blend-mode
5) 假设我们有一个带有彩虹色背景的.container
元素。在这个元素内部,我们有一个带有图像背景的.mover
元素,比如一个黑莓派。类名可能已经泄露了这个信息,但我们对.mover
元素的位置进行动画,并将其设置为mix-blend-mode: overlay
。这使得我们的移动元素看起来不同,具体取决于它父元素的背景的哪一部分在它上面。
查看CodePen上的示例:轨道上的派,混合模式(混合模式!),由Ana Tudor(@thebabydino)创建。
Edge 浏览器尚不支持混合模式,因此本节中的演示示例在 Edge 浏览器中无法正常运行。但是,您可以为mix-blend-mode
实现投票。另外请注意,目前,由于Blink 浏览器的一个错误,您可能不应该将.container
元素设置为body
或html
元素。这个错误会导致当.container
元素是body
或html
元素时,.mover
元素上的混合模式被忽略。Firefox 和 Safari 浏览器没有这个问题。
好吧,但这仅仅是二维的。如果我们的移动元素是一个带有图像面的立方体,一个在三维空间中旋转的立方体呢?
查看CodePen上的示例:
轨道上的派立方体,混合模式 #0,由CSS-Tricks(@css-tricks)创建。
在 CodePen 上。
到目前为止,一切都很好,但我们还没有任何混合效果。我们在立方体上设置mix-blend-mode: overlay
,现在我们有了混合效果,但它破坏了我们的三维效果,立方体的面被扁平化到立方体的平面上了!
查看CodePen上的示例:
轨道上的派立方体,混合模式 #1,由CSS-Tricks(@css-tricks)创建。
在 CodePen 上。
同样,这是因为我们在立方体上应用三维变换并对其子元素进行三维变换,因此我们希望立方体对于transform-style
属性的值为preserve-3d
。但是,在立方体上设置mix-blend-mode: overlay
会强制transform-style
属性的实际值为flat
,因此立方体的面会被扁平化到其父元素的平面。不过,这种情况在 Firefox 浏览器中不会发生,尽管规范中说明应该会发生这种情况。
我们可以尝试在立方体的面上设置mix-blend-mode: overlay
,但这似乎不起作用。立方体被扁平化了,也没有混合效果。
另一个解决方案是在容器和移动的立方体之间添加一个.scene
元素,并在该元素上设置perspective
和mix-blend-mode
属性。
查看CodePen上的示例:轨道上的派立方体,混合模式 #3,由Ana Tudor(@thebabydino)创建。
这似乎解决了所有问题!
我喜欢这些文章。网络技术和底层图形编程概念之间的交叉令人惊叹。
感谢这篇文章!
我刚刚开始了一个新的 CSS3D 实验系列(https://github.com/xem/CSS3Dprototypes#readme),这篇文章对我继续实验非常有帮助!
如果您对其他 CSS3D 技巧感兴趣,我还有一些其他技巧:
– 3Ddoodle:https://github.com/xem/3Ddoodle#readme
– mode7:https://github.com/xem/mode7#readme
干杯!
这里有一些非常棒的内容,我一直很喜欢三维变换。不过,在很多情况下,性能仍然存在一些问题……同时运行几个演示示例,我的可怜的 Chromebook 几乎无法滚动。
非常有帮助的文章。我应该早几天就读这篇文章。特别是渲染上下文部分对我的最新实验http://codepen.io/csanchezriballo/pen/GpORbb非常有帮助。
虽然有些晚了,但这篇文章会非常有用。谢谢。
我是一个新手。CSS 的功能太棒了……
感谢这篇文章。
如果这么多浏览器没有这么多错误就好了……当使用 overflow:scroll|auto 时,Chrome 浏览器完全忽略了 backface-visibility 属性。查看这个错误:https://bugs.chromium.org/p/chromium/issues/detail?id=363609