CSS + SVG → 三维光照和凹凸贴图 ~ Web平台文档稀疏的一角 ~

视频演示!

没有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>

CSS + SVG → 三维光照和凹凸贴图 ~ Web平台文档稀疏的一角 ~》上有1条评论

  1. satgo1546 文章作者

    orztech.com的证书过期了,网页样式全都加载不出,这下回归原始丛林了XD

    回复

发表评论

电子邮件地址不会被公开。 必填项已用*标注