首页
/ three.js MeshToonNodeMaterial 深入解析:节点化卡通着色(Toon Shading)材质实战

three.js MeshToonNodeMaterial 深入解析:节点化卡通着色(Toon Shading)材质实战

2026-09-07 16:15:28作者:庞队千Virginia

在 three.js 的节点(Node)化渲染体系中,MeshToonNodeMaterial 是经典卡通着色材质 MeshToonMaterial 的 Node 版本,用于在基于节点的渲染管线中实现扁平、漫画风格的渐变着色。本文以 MeshToonNodeMaterial.html.md 的 API 文档为主体骨架,结合 MeshToonNodeMaterial.js 源码与其底层 ToonLightingModel.js 照明模型,以及官方示例 examples/webgpu_materials_toon.html,完整讲解它的构造函数、属性、默认值机制、光照模型替换原理,并给出可直接运行的自定义 gradientMap 卡通着色案例。读完你既能独立使用该材质,也能理解它在光照计算上与标准材质的本质差异。

一、MeshToonNodeMaterial 是什么:定位与使用场景

从源码中可以看到其类型定义:

class MeshToonNodeMaterial extends NodeMaterial {
	static get type() {
		return 'MeshToonNodeMaterial';
	}
	...
}

它继承自 NodeMaterial,属于 three.js 节点材质家族。与经典 MeshToonMaterial 不同,节点材质的着色逻辑不是被硬编码进一份固定的 GLSL 着色器,而是通过「节点(Node)图」在运行时被组合与编译,因此可以更灵活地接入 TSL(Three Shading Language)工作流。

在节点材质体系中,材料通常配对存在:

经典(内置)材质 节点版本
MeshBasicMaterial MeshBasicNodeMaterial
MeshLambertMaterial MeshLambertNodeMaterial
MeshPhongMaterial MeshPhongNodeMaterial
MeshStandardMaterial MeshStandardNodeMaterial
MeshPhysicalMaterial MeshPhysicalNodeMaterial
MeshToonMaterial MeshToonNodeMaterial

所有节点材质统一在 NodeMaterials.js 中被导出:

export { default as MeshToonNodeMaterial } from './MeshToonNodeMaterial.js';

而在 StandardNodeLibrary.js 中,它还被注册为对应经典材质字符串名的自动映射目标:

import MeshToonNodeMaterial from '../../../materials/nodes/MeshToonNodeMaterial.js';
...
this.addMaterial( MeshToonNodeMaterial, 'MeshToonMaterial' );

这意味在节点化管线中,指定经典 'MeshToonMaterial' 时渲染器可以自动找到并使用这个节点版本。MeshToonNodeMaterial 适合需要卡通/漫画风格扁平着色、需要自定义渐变分级(gradientMap)或希望在 TSL 中进一步改写光照结果的场景,它需要在 three.js 基于节点的渲染后端(官方示例使用 WebGPURenderer,构建产物见 build/three.webgpu.js)下工作。

二、构造函数与参数

new MeshToonNodeMaterial( parameters : Object )

源码中的构造实现非常简短:

constructor( parameters ) {
	super();
	this.isMeshToonNodeMaterial = true;
	this.lights = true;
	this.setDefaultValues( _defaultValues );
	this.setValues( parameters );
}

参数与继承机制parameters 是一个可选对象,其中可以包含材质外观相关的任一属性。与经典 MeshToonMaterial 相同,Color 类型的值可以用 Color#set 接受的任意方式传入。

值得强调的是第 6 行与 setDefaultValues 的组合:

const _defaultValues = /*@__PURE__*/ new MeshToonMaterial();

这是 Node 材质与经典材质共享参数的桥梁:MeshToonNodeMaterial 并不自己逐个声明 diffuse/gradient 等默认值,而是直接实例化一个经典 MeshToonMaterial 作为“默认值模板”,再交给基类 NodeMaterial 的 setDefaultValues() 复制。因此在构造参数、材质属性层面,MeshToonNodeMaterial 继承了 MeshToonMaterial 的全部外观参数集合(详见下文属性章节),这也是为何在官方示例中可以直接这样使用:

const material = new THREE.MeshToonNodeMaterial( {
	color: diffuseColor,
	gradientMap: gradientMap
} );

setDefaultValues() 之后调用 setValues( parameters ),保证用户显式传入的参数覆盖默认值。若完全不带参数构造,得到的就是一个默认的白色、无贴图、受光照影响的 toon 材质。

三、类属性详解

.isMeshToonNodeMaterial : boolean(只读)

类型判断标志,默认值为 true

this.isMeshToonNodeMaterial = true;

用于在拿到任意材质对象时快速确认其是否为 MeshToonNodeMaterial。同类惯例在各节点材质中均有体现,例如 MeshToonMaterial.js 中对应经典版本也定义了 isMeshToonMaterial

.lights : boolean

该属性默认值为 true,注释明确指出因为 toon 材质需要响应灯光:

this.lights = true;

它覆写(Override)了基类 NodeMaterial#lights。该开关与渲染器的光照节点紧密相关,在 NodeMaterial.jssetupLighting() 中可以看到它的实际消费逻辑:

const sceneLighting = this.lights === true && builder.renderer.lighting.enabled;
const lights = sceneLighting || this.lightsNode !== null;
...
if ( lightsNode && ( materialLightings.length > 0 || lightsNode.getScope().hasLights ) ) {
	const lightingModel = this.setupLightingModel( builder ) || null;
	outgoingLightNode = lightingContext( lightsNode, lightingModel, materialLightings, backdropNode, backdropAlphaNode );
}

简言之:只有 lightstrue(且渲染器光照开启)时,材质才会走完整的光照模型计算分支,这也是 toon 材质能显示明暗分级的前提。如果你希望材质不受场景灯光影响而完全扁平,可以将其设为 false

继承自 MeshToonMaterial 的可配置属性(默认值由 setDefaultValues 注入)

由于构造函数中通过 setDefaultValues( _defaultValues ) 引入了 MeshToonMaterial.js 的全部默认参数,MeshToonNodeMaterial 实例同样具备以下常用属性(完整字段表与说明可参见 MeshToonMaterial.html):

属性 类型 默认值 说明
color Color (1,1,1) 材质漫反射颜色
gradientMap Texture null 卡通渐变图;使用时应将 minFilter/magFilter 设为 NearestFilter(该贴图为非颜色数据,须保持 colorSpace = NoColorSpace
map Texture null 颜色贴图(颜色数据,通常应设 SRGBColorSpace),与 color 相乘
alphaMap Texture null 灰度透明贴图,控制表面透明度
aoMap / aoMapIntensity Texture / number null / 1 环境光遮蔽(需要第二套 UV)
bumpMap / bumpScale Texture / number null / 1 凹凸贴图(定义了 normalMap 时被忽略)
normalMap / normalScale / normalMapType Texture / Vector2 / enum null / (1,1) / TangentSpaceNormalMap 法线贴图相关
displacementMap / displacementScale / displacementBias Texture / number / number null / 1(注:见 MeshToonMaterial 默认)/ 0 顶点位移贴图
emissive / emissiveIntensity / emissiveMap Color / number / Texture (0,0,0) / 1 / null 自发光
lightMap / lightMapIntensity Texture / number null / 1 烘焙光照贴图(需要第二套 UV)
fog boolean true 是否受雾影响
wireframe 系列 boolean / 字符串 / number false / 'round' / 1 线框渲染相关

注:displacementScale 的默认值与文档中列出的经典材质行为一致,详见 MeshToonMaterial.html 相应条目。AO、光照等贴图需要第二套 UV(如通过 Geometry 的 uv2)才能生效。

其中对卡通效果最关键的是 gradientMap:它决定明暗过渡被压缩成多少个离散梯度级。

四、核心方法:setupLightingModel()

.setupLightingModel() : ToonLightingModel

在基类 NodeMaterial.js 中,setupLightingModel() 被定义为一个空实现的“接口函数”,需要子类材质实现以确定自己的照明模型:

/**
 * This method should be implemented by most derived materials
 * since it defines the material's lighting model.
 * @abstract
 */
setupLightingModel( /*builder*/ ) {
	// Interface function.
}

而 MeshToonNodeMaterial 的唯一覆写实现非常简单:

setupLightingModel( /*builder*/ ) {
	return new ToonLightingModel();
}

它将卡通渲染的“真正的算法”全部委托给了 ToonLightingModel。这个返回的照明模型对象会在上文 setupLighting() 中通过 lightingContext( lightsNode, lightingModel, ... ) 参与最终的出射光计算,lightingContext 会依次驱动该模型 direct() / indirect() 等钩子。因此对使用 MeshToonNodeMaterial 的开发者来说,理解 ToonLightingModel 的算法就等于理解了卡通着色何以“卡通”。

ToonLightingModel:卡通照明的源码级原理

ToonLightingModel 继承自通用 LightingModel(其官方文档见 ToonLightingModel.html)。核心函数是内部的 getGradientIrradiance

const getGradientIrradiance = /*@__PURE__*/ Fn( ( { normal, lightDirection, builder } ) => {
	// dotNL will be from -1.0 to 1.0
	const dotNL = normal.dot( lightDirection );
	const coord = vec2( dotNL.mul( 0.5 ).add( 0.5 ), 0.0 );

	if ( builder.material.gradientMap ) {
		const gradientMap = materialReference( 'gradientMap', 'texture' )
			.context( { getUV: () => coord } );
		return vec3( gradientMap.r );
	} else {
		const fw = coord.fwidth().mul( 0.5 );
		return mix( vec3( 0.7 ), vec3( 1.0 ),
			smoothstep( float( 0.7 ).sub( fw.x ), float( 0.7 ).add( fw.x ), coord.x ) );
	}
} );

算法要点可以归纳为三步:

  1. 法线与光方向的点积被归一化到 [0,1],作为沿贴图 U 轴的采样坐标:coord.x = dotNL * 0.5 + 0.5。这就把“该点受光强弱”映射成了一个一维坐标。
  2. 若设置了 gradientMap:用该坐标去采样材质的 gradientMap(只取红色通道 r),并把 UV 用 .context({ getUV: () => coord }) 强制覆盖为上述动态坐标——所以 gradientMap 本质上是一张宽度为 N 的一维查找表,N 越大、明暗分级的层数越多。
  3. 若未设置 gradientMap:走一个内置的默认两段式近似——用 smoothstep 在 0.7 阈值附近生成从 0.7 到 1.0 的硬过渡,配合 fwidth 做一点抗锯齿(AA)软化边缘,得到最小限度的漫画分色。

随后 direct() 将采样结果乘上灯光颜色,再乘以 Lambert 漫反射项累加到直接光:

direct( { lightDirection, lightColor, reflectedLight }, builder ) {
	const irradiance = getGradientIrradiance( { normal: normalView, lightDirection, builder } ).mul( lightColor );
	reflectedLight.directDiffuse.addAssign( irradiance.mul( BRDF_Lambert( { diffuseColor: diffuseColor.rgb } ) ) );
}

indirect() 则把环境/IBL 辐照度同样以 Lambert 方式累加并乘上环境光遮蔽:

indirect( builder ) {
	const { ambientOcclusion, irradiance, reflectedLight } = builder.context;
	reflectedLight.indirectDiffuse.addAssign( irradiance.mul( BRDF_Lambert( { diffuseColor } ) ) );
	reflectedLight.indirectDiffuse.mulAssign( ambientOcclusion );
}

这正是“离散少数几档明暗(a small number of discrete shades)创造出漫画式平坦观感”的实现证据,与传统 PBR 材质的连续 dot(N,L) 光照曲线形成了鲜明对比。

五、实战:从源码到可运行示例

官方为节点化 toon 材质提供了完整示例 examples/webgpu_materials_toon.html,其截图见下方。

官方示例截图

three.js WebGPU toon 材质示例运行结果,展示了 5x5x5 渐变球体阵列的卡通着色效果

在该示例中,材质通过 three/webgpu 入口导入并使用,全部材质均由 MeshToonNodeMaterial 构造:

import * as THREE from 'three/webgpu';

const gradientMap = new THREE.DataTexture( colors, colors.length, 1, THREE.RedFormat );
gradientMap.needsUpdate = true;

const material = new THREE.MeshToonNodeMaterial( {
	color: diffuseColor,
	gradientMap: gradientMap
} );

示例在场景中布置了大量渐变球体(每侧 5 个、共 5×5×5),分别沿 x 轴切换 gradientMap 的离散级数、沿 z 轴切换 diffuse 颜色,配合一个 TSL 的 toonOutlinePass( scene, camera )(见 ToonOutlinePassNode)得到描边+卡通填充的最终画面。

最小可运行示例

参照源码构造与照明模型,可以提炼出一个自包含的最小示例(核心逻辑见下方,完整场景搭建可对照官方示例源码):

import * as THREE from 'three/webgpu';

const renderer = new THREE.WebGPURenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 100 );
camera.position.set( 0, 0, 3 );

// 1) 构建 4 级卡通渐变查找表:数值从小到大 = 从暗到亮的分级台阶
const steps = 4;
const data = new Uint8Array( steps );
for ( let i = 0; i < steps; i ++ ) {
	data[ i ] = Math.round( ( i / ( steps - 1 ) ) * 255 );
}
const gradientMap = new THREE.DataTexture( data, steps, 1, THREE.RedFormat );
// DataTexture 默认即 NearestFilter;若使用普通 Texture/CanvasTexture,必须手动设置:
// gradientMap.minFilter = THREE.NearestFilter;
// gradientMap.magFilter = THREE.NearestFilter;
gradientMap.needsUpdate = true;

// 2) 创建节点化 toon 材质
const material = new THREE.MeshToonNodeMaterial( {
	color: 0xffaa33,
	gradientMap: gradientMap
} );

// 3) 使用受光照的几何体
const mesh = new THREE.Mesh( new THREE.SphereGeometry( 1, 64, 32 ), material );
scene.add( mesh );

scene.add( new THREE.AmbientLight( 0xffffff, 0.5 ) );
const dirLight = new THREE.DirectionalLight( 0xffffff, 2 );
dirLight.position.set( 1, 1, 1 );
scene.add( dirLight );

renderer.setAnimationLoop( () => {
	mesh.rotation.y += 0.01;
	renderer.render( scene, camera );
} );

代码要点与注意事项

  • 材质属性直接复用经典参数:得益于 setDefaultValues( _defaultValues ),上面传给构造函数的是 color/gradientMap 这类属于 MeshToonMaterial 的参数,节点材质内部会在渲染时把 material.gradientMap 作为采样来源,与 ToonLightingModel.jsif ( builder.material.gradientMap ) 的判断一一对应。
  • gradientMap 必须关闭插值过滤:文档与源码注释都强调需要 NearestFilter(经典实现见 MeshToonMaterial.js)。THREE.DataTexture 默认就使用 Nearest 滤波,因此示例中无需显式设置;若用普通纹理,则须手动将 minFiltermagFilter 都设为 NearestFilter,否则相邻灰度会被线性插值糊成一团,分级感消失。
  • colorSpace 处理gradientMap 属于非颜色数据,应保持默认的 NoColorSpace,不能设为 SRGBColorSpace;而 map/emissiveMap 这类颜色贴图则须按 Texture#colorSpace 约定设置,常见值为 SRGBColorSpace
  • 需要灯光:材质默认 lights = true,示例中同时放置了 AmbientLightDirectionalLight。没有灯光时,direct() 分支贡献为零,模型会趋于纯黑剪影。
  • 运行前提:该材质走节点化渲染路径,官方演示基于 WebGPURendererimport * as THREE from 'three/webgpu',请使用支持 WebGPU 的浏览器环境(并可启用 #enable-unsafe-webgpu 等标志,视浏览器版本而定)。

调整 steps(渐变级数)、color 以及光照方向,即可直观观察“离散明暗带”的变化——这正是 MeshToonNodeMaterial 与 MeshToonMaterial 在观感上一脉相承、而在实现上彻底节点化的核心价值。

六、小结与延伸阅读

回顾全文,MeshToonNodeMaterial 的工程价值可以总结为三点:

  • API 兼容性:通过 _defaultValues = new MeshToonMaterial()setDefaultValues(),把经典 toon 材质的全部外观参数无缝迁移到节点世界,迁移成本极低;
  • 关注点分离:自身代码极其精简(源码仅 60 余行),只负责声明类型标志、lights 开关与 setupLightingModel() 覆写,复杂的卡通明暗计算完全下沉到 ToonLightingModel
  • 可编程性:照明模型、渐变查找与 TSL 描述符的组合,使开发者可以在 examples/webgpu_materials_toon.html 的基础上自由替换 gradientMap、改写节点甚至自定义 LightingModel,实现更高阶的非写实渲染。

想继续深入,建议依次阅读:

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
916
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388