一个游戏工作室花费数月制作的高精度角色模型,上线不久就出现在竞争对手的场景里,这种事情在3D行业并不少见。与图片、视频不同,3D模型文件的复制成本几乎为零,而维权的最大障碍往往是作者无法证明“这个模型先是我的”。隐形几何水印技术正是为此而生:它把版权信息悄悄藏进模型的几何数据中,肉眼看不见、常规操作难以破坏,一旦发生纠纷就能提取出来作为证据。

什么是3D几何水印:原理与分类
3D模型水印的核心思想,是在不改变模型视觉效果的前提下,将特定信息(比如作者ID、时间戳、授权编号)编码进模型的几何表示中。一个完整的数字水印系统包含两个部分:嵌入器和提取器。嵌入器接收原始模型和待隐藏的比特序列,对几何数据做微小扰动;提取器则从可能已被篡改的模型中恢复出这段序列。
按照嵌入域的不同,主流方案可以分成两大类。第一类是空域方法,直接修改顶点坐标、顶点法线或三角面片的连接关系。它实现简单、计算量小,但抵抗简化、重网格化等攻击的能力较弱。第二类是变换域方法,先把模型的几何信息变换到频域(如拉普拉斯矩阵的特征向量空间),再对低频系数做修改。这类方法隐藏性好、鲁棒性强,是目前学术研究和商业方案的主流路线。
评价一个水印方案的优劣,通常看三个互相制约的指标:容量(能藏多少比特)、不可见性(扰动引起的失真有多大)和鲁棒性(抵抗攻击的能力)。三者不可能同时做到极致,实际工程中往往要在容量和不可见性上让步,换取足够的鲁棒性——毕竟一个一攻击就失效的水印毫无维权价值。
空域水印:基于顶点扰动的简单实现
空域方法里最容易理解的是顶点微扰法。思路是:选出一组顶点,按照水印比特序列对它们的坐标施加极小幅度的偏移。例如规定比特1表示顶点沿其法线方向外推ε,比特0表示内收ε,其中ε远小于模型的包围盒尺寸。提取时测量同一批顶点相对原始位置的偏移方向,即可还原比特序列。
这种方法的优点是嵌入和提取都只需要简单的向量运算,几十行代码就能跑通。但它有一个明显的软肋:提取过程必须依赖原始模型做比对,属于“非盲水印”。如果攻击者对模型做了一次网格简化或重新剖分,顶点的一一对应关系就被打乱,水印直接报废。
改进思路是让水印的判定不依赖单一顶点,而是依赖顶点群的统计特征。例如把模型按空间八叉树划分为若干区域,在每个区域内统计顶点到某基准平面距离的均值,用均值的变化编码比特。这样即使部分顶点被删改,统计量仍然相对稳定,抗简化攻击的能力明显提升。
变换域水印:基于拉普拉斯谱的鲁棒方案
变换域方法的代表作是网格谱水印。对一个三角网格模型,可以构建它的图拉普拉斯矩阵,其中每个顶点对应矩阵的一行一列,邻接权重由相邻顶点间的距离决定。对这个矩阵做特征值分解,得到的特征向量构成了网格的一组“频率基”:低阶特征向量描述模型的整体轮廓,高阶特征向量刻画细节。
嵌入时,把水印比特调制到若干低阶频率系数上。由于低频对应的是模型宏观形状,对其做小幅度修改在视觉上几乎无法察觉,但它又不会被平滑、简化这类攻击轻易抹掉——因为简化操作主要影响高频细节,宏观形状必须保留,否则模型就变形了。这就是谱水印鲁棒性的来源。
下面用一个简化的Python示例演示基于拉普拉斯谱的水印嵌入流程:
import numpy as np
import open3d as o3d
from scipy.sparse import csr_matrix
from scipy.sparse.linalg import eigsh
def build_laplacian(mesh):
"""构建图拉普拉斯矩阵 L = D - W"""
verts = np.asarray(mesh.vertices)
edges = np.asarray(mesh.triangles)
n = len(verts)
# 收集所有边
edge_set = set()
for tri in edges:
for i in range(3):
a, b = tri[i], tri[(i + 1) % 3]
edge_set.add((min(a, b), max(a, b)))
W = np.zeros((n, n))
for a, b in edge_set:
d = np.linalg.norm(verts[a] - verts[b])
W[a, b] = W[b, a] = 1.0 / (d + 1e-8)
D = np.diag(W.sum(axis=1))
return D - W, W
def embed_watermark(mesh, bits, k=30, strength=0.01):
L, _ = build_laplacian(mesh)
# 取前k个最小特征值对应的特征向量(低频基)
vals, vecs = eigsh(L, k=k, sigma=0, which='LM')
verts = np.asarray(mesh.vertices)
# 将水印比特调制到低频系数上
signal = verts - verts.mean(axis=0)
coeff = vecs.T @ signal
for i, bit in enumerate(bits):
delta = strength * coeff[i].max() if coeff[i].max() > 0 else strength
coeff[i] += delta if bit == 1 else -delta
# 逆变换回空域
new_verts = vecs @ coeff + verts.mean(axis=0)
mesh.vertices = o3d.utility.Vector3dVector(new_verts)
return mesh</code>实际使用时,特征值分解的计算量随顶点数增长很快,几十万顶点的高模需要在降采样后的代理网格上完成分解,再把扰动映射回原始网格。此外特征向量的符号在每次分解中可能翻转,工程上需要预先约定符号基准,否则提取阶段会解出错误比特。
鲁棒性对抗与工程选型建议
水印方案设计完成并不意味着万事大吉,攻击者能想到的手段包括:网格简化、顶点乱序、仿射变换(旋转、缩放、平移)、加噪平滑、裁剪局部重拼,甚至直接对模型做重新拓扑。一个合格的商业水印至少要扛住拓扑无关的几何攻击:提取前先对模型做泊松重建或迭代最近点配准,把待测模型归一化到与嵌入时相同的坐标系和尺度,再进行谱系数比对。
面对裁剪攻击,通行的做法是加入纠错编码和冗余嵌入。比如把64比特的版权信息用汉明码或RS码扩展到256比特,再分多个频段重复嵌入。提取时只要有一部分频段存活,纠错解码仍能恢复出完整信息。代价是容量下降,但版权标识通常只需要几十比特,这个交换是划算的。
选型方面,如果只是给内部资产做溯源标记,顶点统计特征类的空域方案足够用,实现成本最低;如果要对外分发的商业模型,建议采用谱域水印叠加盲提取设计,或者直接选用成熟的版权保护服务。还有一点值得强调:水印的法律效力依赖于完整的存证链,嵌入时间、密钥保管、原始模型哈希都应该同步记录到可信时间戳服务中,否则提取出的水印在诉讼中可能不被采信。技术方案与存证流程配套,才是3D模型版权保护的完整答案。