视频演示!
没有WebGL!
- CSS里有transform属性,可以给元素施加三维变换,因此CSS是一种顶点着色器(暴论)
- SVG特有的二维图像滤镜可以操作像素颜色值,因此SVG是一种片段着色器(暴论)
- CSS里的filter属性能通过url()引用SVG滤镜,因此CSS和SVG合作为一种渲染管线(??)
可能W3C也是这么想的,于是定义了<feDiffuseLighting>提供漫反射分量的计算方法,<feSpecularLighting>提供高光分量的计算方法,加上元素自带的fill或background作为环境光分量,构成了完整的Phong光照模型……甚至后来忘了分量要怎么用<feComposite>的arithmetic模式合成,又加了个lighter模式,它真的,我哭死。
滤镜ABC
- SVG完全支持基于节点的滤镜图。
- SVG只支持二维图像,滤镜也只支持二维图像。
第一点是SVG滤镜除了种类更丰富之外,强于CSS滤镜函数的关键因素:中间结果可以以复杂方式连接、复用。SVG滤镜真的有这么Q弹吗?是的,这些滤镜函数CSS都有,但只用CSS滤镜无法做到用一个形状投影的同时在上层叠加另一个形状,这是数据流的分岔再合并,而CSS只支持链状滤镜图。
别看CSS有三维变换,CSS滤镜同样也只支持二维图像!这是标准规定的:滤镜特效生效于扁平化的元素内容,因此transform-style: preserve-3d在指定了filter属性的元素上也会看不出效果。要么在面上添加滤镜,要么在摄像机上添加滤镜。
在滤镜库里兜兜转转
我能找到的最好的关于SVG滤镜的参考资料——很可惜——是W3C的标准(2003版)。互联网上鲜见使用SVG滤镜的页面,大部分相关文章也只是浅尝辄止。(我这篇也是。)
下面隆重介绍PSVG(PhotoShoppable Vector Graphics):
- 创建图层:feFlood(新建颜色填充图层)、feImage(导入图片或形状)、feTurbulence(云彩)
- 基本操作:feOffset(移动)、feTile(创建图案)
- 合并图层:feBlend(混合模式)、feComposite(蒙版/布尔运算)、feMerge(批量合并图层)
- 调色:feColorMatrix(色相/饱和度/渐变映射)、feComponentTransfer(曲线/不透明度)
- 传统滤镜:feConvolveMatrix(计算)、feGaussianBlur(高斯模糊)、feDisplacementMap(置换)、feMorphology(腐蚀和膨胀)、feDropShadow(投影)
- 等高线与纹理:feDiffuseLighting(漫反射)、feSpecularLighting(高光)
CSS支持的blur()就是feGaussianBlur,drop-shadow就是feDropShadow,剩下的所有滤镜函数都可归入feColorMatrix。W3C说:色彩矩阵恐怖如斯,便让SVG用户手输矩阵。
Photoshop支持的图层混合模式,CSS和SVG基本上都支持。线性减淡(添加)不是混合模式,而是作为算术运算,实现在feComposite中。缺乏美术意义的混合模式,如划分、异或,没有实现。
没有办法在混合时控制图层的透明度,因此只能靠滤镜下调图层内容的透明度。
<!-- 不透明度70% --> <feComponentTransfer> <feFuncA type="linear" slope="0.7" /> </feComponentTransfer>
有多种方式实现通常的图层混合。
<feBlend in="layer1" in2="layer2" /><!-- in在上 --> <!-- 等价于 --> <feComposite in="layer1" operator="over" in2="layer2" /><!-- operator可省略 --> <!-- 等价于 --> <feMerge> <!-- 按绘制顺序,与SVG本体一致 --> <feMergeNode in="layer2"/> <feMergeNode in="layer1"/> </feMerge>
寄!
滤镜是二维的,怎么产生三维的光照效果?答案是凹凸贴图(bump map),作用和法线贴图是一样的。
滤镜中的光源点坐标相对于面,面相对于物体,光源在外,必须经过多重坐标转换才能得到正确的滤镜坐标值。MDN上的rotate3d矩阵和W3C标准里的矩阵不一样,该抄谁的呢?不要忘了浏览器坐标系y轴向下呀。从文章开头的视频里能看出,我肯定有几个面算错了,但是就这样吧,开摆!
XML很难写,矩阵很难写,滤镜……没有好用的节点编辑器。我找到的最好用的编辑器来自Йоксель,也是现在搜索SVG data URI encoder首位结果的作者。不过它缺少重要的功能:无法指定滤镜画布大小和primitiveUnits,没有专门的矩阵编辑工具,内置的文档讲了也白讲。
浏览器对滤镜的优化普遍不好,用得太多就会卡爆。CSS控制的三维物件,因为每个面都有对应的DOM元素,也容易卡爆。正经应用果然还得是WebGL。
抛开SVG不谈,Keith Clark早在2013年就提出了计算法向量生成光照和阴影贴图的技术。“简单的图形学”。
演示页面完整代码
<!DOCTYPE html> <title>CSS + SVG → lighting and bump maps</title> <style> body { width: 100vw; height: 100vh; margin: 0; display: flex; font-size: 100px; background: lightsteelblue; } section { width: 1em; height: 1em; margin: auto; transform-style: preserve-3d; } div { width: 1em; height: 1em; position: absolute; backface-visibility: hidden; background: lightslategray; } </style> <section title="@satgo1546: See http://satgo1546.mist.so/archives/431"> </section> <svg width="0" height="0"> <filter x="0" y="0" width="100%" height="100%" filterUnits="objectBoundingBox" primitiveUnits="objectBoundingBox"> <feTurbulence type="fractalNoise" baseFrequency=".5" numOctaves="2" /> <feDiffuseLighting surfaceScale="5" diffuseConstant="1" lighting-color="white"> <fePointLight /> </feDiffuseLighting> <feBlend in2="SourceGraphic" mode="overlay" /> <feComposite in2="SourceAlpha" operator="in" /> </filter> </svg> <script> const lights = [] const container = document.getElementsByTagName('section')[0] const filterTemplate = document.getElementsByTagName('filter')[0] for (let i = 0; i < 3; i++) { // 0 = xy-plane; 1 = xz-plane; 2 = yz-plane for (let j = 0; j < 2; j++) { // 0 = bottom face; 1 = top face const face = container.appendChild(document.createElement('div')) face.style.transform = ['', 'rotateX(-.25turn) rotateZ(.5turn)', 'rotateY(.25turn)'][i] + `translateZ(${j - .5}em) rotateY(${(1 - j) * .5}turn)` const filter = filterTemplate.insertAdjacentElement('beforebegin', filterTemplate.cloneNode(true)) face.style.filter = `url("#${filter.id = 'face-' + i + j}")` lights.push(filter.getElementsByTagName('fePointLight')[0]) } } function animate(angle) { angle *= .001 let axisX = 5, axisY = 1, axisZ = 4 // rotation axis container.style.transform = `perspective(6em) rotate3d(${axisX}, ${axisY}, ${axisZ}, ${angle}rad)` const axisLength = Math.hypot(axisX, axisY, axisZ) axisX /= axisLength axisY /= axisLength axisZ /= axisLength const lightX0 = -.25, lightY0 = -1, lightZ0 = 1 // world coordinate of the light source const sin = Math.sin(-angle), cos = Math.cos(-angle) const lightX = (1 + (1 - cos) * (axisX * axisX - 1)) * lightX0 + (-axisZ * sin + axisX * axisY * (1 - cos)) * lightY0 + (axisY * sin + axisX * axisZ * (1 - cos)) * lightZ0 const lightY = (axisZ * sin + axisX * axisY * (1 - cos)) * lightX0 + (1 + (1 - cos) * (axisY * axisY - 1)) * lightY0 + (-axisX * sin + axisY * axisZ * (1 - cos)) * lightZ0 const lightZ = (-axisY * sin + axisX * axisZ * (1 - cos)) * lightX0 + (axisX * sin + axisY * axisZ * (1 - cos)) * lightY0 + (1 + (1 - cos) * (axisZ * axisZ - 1)) * lightZ0 for (let i = 0; i < 3; i++) { for (let j = 0; j < 2; j++) { const face = lights[i * 2 + j] let lightDX = lightX, lightDY = lightY, lightDZ = lightZ if (i === 1) lightDY = lightZ, lightDZ = lightY if (i === 2) lightDX = lightZ, lightDZ = lightX lightDZ -= j - .5 if (!j) lightDX = -lightDX, lightDZ = -lightDZ if (i) lightDX = -lightDX face.x.baseVal = lightDX face.y.baseVal = lightDY face.z.baseVal = lightDZ } } requestAnimationFrame(animate) } requestAnimationFrame(animate) </script>
orztech.com的证书过期了,网页样式全都加载不出,这下回归原始丛林了XD