Three.js卡通材质实现教程(three.js 模型动画)
liebian365 2025-04-07 15:39 6 浏览 0 评论
继 Harry Alisavakis 令人惊叹的汤着色器之后,我想使用 Three.js 重新创建类似的卡通着色效果。 我从 Roystan 的卡通着色器教程开始,它是为 Unity 编写的。 在这篇文章中,我将把 Roystan 教程中概述的原则翻译成 Three.js。 下面描述的着色器为创建更加风格化的着色器提供了良好的基础。
点击这里访问具有完整卡通着色器实现的Github存储库。
推荐:用 NSDT设计器 快速搭建可编程3D场景。
1、Three.js 着色器概述
本教程需要了解着色器的一般工作原理以及在 Three.js 中的具体工作原理。 我们将使用自定义顶点和片段着色器创建 ShaderMaterial。 简而言之,顶点着色器处理屏幕上顶点数据的位置,而片段着色器处理每个像素呈现的颜色。
需要记住的一些要点:
- attributes是着色器中可用的值,在网格的每个顶点上定义。 这些是位置、UV 等。
- uniforms是传递给整个网格着色器的值。 这些信息包括增量时间、摄像机位置或场景中灯光的信息。
- varyings是从一个着色器传递到另一个着色器的值。 通常这些包括只能在顶点着色器中计算的位置数据,并传递给片段着色器。
2、卡通着色器理论
卡通着色器背后的想法非常简单,但效果却很强大。 虽然我们可以使用很多效果,但对于这个基本的卡通着色器,我们将重点关注创建卡通外观的五个主要方面:
- 平面色基
- 单色核心阴影
- 镜面反射
- 边缘光
- 收到的阴影
让我们开始吧!
3、平面色基
首先,我们从两个基本着色器开始:一个顶点着色器,用于设置顶点在剪辑空间中的正确位置;以及一个片段着色器,用于设置给定颜色。 这会导致我们的网格形状被正确绘制,但整个网格是单色的。
import toonVertexShader from './toon.vert'
import toonFragmentShader from './toon.frag'
const toonShaderMaterial = new THREE.ShaderMaterial({
vertexShader: toonVertexShader,
fragmentShader: toonFragmentShader,
})
const mesh = new THREE.Mesh(
new THREE.SphereGeometry(1, 1, 1),
toonShaderMaterial
)
toon.vert:
void main() {
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
vec4 viewPosition = viewMatrix * modelPosition;
vec4 clipPosition = projectionMatrix * viewPosition;
gl_Position = clipPosition;
}
toon.frag:
void main() {
gl_FragColor = vec4(vec3(0.39, 0.58, 0.93), 1.0);
}
效果如下:
由于我们要向 THREE.ShaderMaterial 添加自定义着色器,因此我们首先需要指定使用该材质的网格应该是什么颜色。
虽然我们可以直接在着色器中对颜色进行硬编码,但更好的方法是将其作为统一的颜色传递给着色器。 然后我们还可以将颜色作为属性添加到 dat.GUI 控件中,以便我们可以在运行时更改它。
toon.frag:
uniform vec3 uColor;
void main() {
gl_FragColor = vec4(uColor, 1.0);
}
4、核心阴影
为了获得清晰的阴影外观,我们需要清楚地区分我们认为照亮的网格区域和我们考虑的阴影区域。 为了实现这种效果,我们需要场景的光照信息。
值得庆幸的是,Three.js 为我们提供了开箱即用的光照信息,我们只需要知道如何添加它即可。
首先,我们需要指出,我们的ShaderMaterial需要通过将 lights 属性设置为true来接收光照信息。
其次,在 ShaderMaterial 中,我们通过
...THREE.UniformsLib.lights 传入预定义的灯光uniforms。 这些uniforms确保我们的着色器知道如何接收照明信息。
scene.js:
const toonShaderMaterial = new THREE.ShaderMaterial({
lights: true,
uniforms: {
...THREE.UniformsLib.lights,
uColor: { value: THREE.Color('#6495ED') }
},
vertexShader: toonVertexShader,
fragmentShader: toonFragmentShader,
})
第三,我们要在顶点着色器中计算 varying vec3 vNormal向量并将其传递给片段着色器。 我们需要这个向量来计算给定点的阴影强度。
varying vec3 vNormal;
void main() {
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
vec4 viewPosition = viewMatrix * modelPosition;
vec4 clipPosition = projectionMatrix * viewPosition;
vNormal = normalize(normalMatrix * normal);
gl_Position = clipPosition;
}
最后,在我们的片段着色器中,我们需要使用 #include
#include
#include
uniform vec3 uColor;
varying vec3 vNormal;
void main() {
gl_FragColor = vec4(uColor, 1.0);
}
4.1 定向光
场景中的每个定向光都具有以下结构,该结构在我们上面包含的着色器块中定义。
struct DirectionalLight {
vec3 direction;
vec3 color;
};
为了计算阴影需要在网格上的位置,我们需要计算出照射到我们可以看到的每个点的漫射光的强度。 为此,我们采用光线方向与任何给定点法线的点积。
为了建立直觉,当两个向量指向同一方向时,点积(dot product)为 1;当向量彼此垂直时,点积趋向 0;当它们的角度增加超过 90° 时,点积趋向 -1。 这意味着法线直接指向光源的网格部分应该具有最大的光强度,而垂直或远离光源的部分则不会得到任何光。
由于点积返回从 -1 到 1 的值,并且我们希望阴影和非阴影之间有一个清晰的截止点,因此我们将使用 smoothstep 函数将值的范围限制在 0 和 1 之间
将定向光颜色乘以该光的强度,我们就得到了需要乘以网格体基色的定向光。
片段着色器应类似于下面的代码,其中突出显示新行:
#include
#include
uniform vec3 uColor;
varying vec3 vNormal;
void main() {
float NdotL = dot(vNormal, directionalLights[0].direction);
float lightIntensity = smoothstep(0.0, 0.01, NdotL);
vec3 directionalLight = directionalLights[0].color * lightIntensity;
gl_FragColor = vec4(uColor * directionalLight, 1.0);
}
结果如下:
4.2 环境光
阴影现在看起来太暗了,这是因为场景的环境光被忽略了。 在上面的代码中,我们实际上说了
- 如果表面被照亮→使用基色
- 如果表面处于阴影中→不使用颜色(即黑色)
由于我们不想要黑色阴影,因此我们还需要考虑环境光。
还记得我们向后几步添加的 #include
gl_FragColor = vec4(uColor * (ambientLightColor + directionalLight), 1.0);
5、镜面反射
虽然核心阴影仅取决于定向光的位置,但镜面反射还取决于观看者的位置,更具体地说是相机的位置。 我们已经将这些数据作为 viewPosition 存在于顶点着色器中。 这是从相机到顶点的矢量,因此为了获得镜面反射光的方向,我们需要做的是将其反转并标准化。
然后我们将值作为 varying vec3 vViewDir 传递给片段着色器。
为了获得镜面反射的强度,我们首先计算出半矢量,即定向光矢量和观察方向中间的矢量。 然后我们取半向量和法向量的点积。 这个点积 NdotH 告诉我们给定点的镜面反射强度。 当我们将其乘以光强度时,我们就可以得到该点定向光的镜面反射有多强。
然后,我们通过应用 pow 和 smoothstep 函数来调整镜面反射强度。 这里我们引入另一个称为 uGlossiness 的uniform,它指定镜面反射应该有多大。 它可以通过 dat.GUI 控件进行调整。
为了更好地描述如何计算镜面反射强度,我强烈建议阅读 Blinn-Phong 镜面反射模型。
以下是此步骤的代码更改以及生成的着色器效果:
toon.vert:
varying vec3 vNormal;
varying vec3 vViewDir;
void main() {
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
vec4 viewPosition = viewMatrix * modelPosition;
vec4 clipPosition = projectionMatrix * viewPosition;
vNormal = normalize(normalMatrix * normal);
vViewDir = normalize(-viewPosition.xyz);
gl_Position = clipPosition;
}
toon.frag:
uniform float uGlossiness;
varying vec3 vViewDir;
// other includes, uniforms and varyings...
void main() {
// directional light ...
// specular reflection
vec3 halfVector = normalize(directionalLights[0].direction + vViewDir);
float NdotH = dot(vNormal, halfVector);
float specularIntensity = pow(NdotH * lightIntensity, 1000.0 / uGlossiness);
float specularIntensitySmooth = smoothstep(0.05, 0.1, specularIntensity);
vec3 specular = specularIntensitySmooth * directionalLights[0].color;
gl_FragColor = vec4(uColor * (directionalLight + ambientLightColor + specular), 1.0);
}
结果如下:
6、边缘照明
我们要应用的最后一个照明效果是边缘照明。 这种类型的照明是一种很酷的效果,当物体被背光或强光从侧面照亮时就会发生这种效果。 对于我们的卡通着色器,我们将伪造这种效果,但它在物理上不会非常准确。
为了获得物体的轮廓,我们希望目标表面的法线几乎垂直于相机。 通过取表面法线向量和视图方向的点积并反转它,我们得到的值对于直接面向相机的表面为 0,对于背向相机的表面则接近 1。
float rimDot = 1.0 - dot(vViewDir, vNormal);
为了仅显示不在阴影中的区域中的边缘照明,我们将该值与 NdotL 相乘,正如我们在第一步中介绍的那样,NdotL 指定表面是在灯光中还是在阴影中。 在获得边缘光强度后,我们对其进行平滑处理以获得清晰的截止效果。 最后,我们将其乘以定向光的颜色并将其添加到 gl_FragColor:
//toon.frag
varying vec3 vNormal;
varying vec3 vViewDir;
void main() {
// directional light, specular reflection...
// rim lighting
float rimDot = 1.0 - dot(vViewDir, vNormal);
float rimAmount = 0.6;
float rimThreshold = 0.2;
float rimIntensity = rimDot * pow(NdotL, rimThreshold);
rimIntensity = smoothstep(rimAmount - 0.01, rimAmount + 0.01, rimIntensity);
vec3 rim = rimIntensity * directionalLights[0].color;
gl_FragColor = vec4(uColor * (directionalLight + ambientLightColor + specular + rim), 1.0);
}
结果如下:
7、接收阴影
我们的材质对场景中的定向光完全做出反应,但它不会接收阻挡光线的物体的阴影。 幸运的是,Three.js 还可以帮助我们访问在着色器内为此光创建的阴影贴图。
为了让你的对象接收阴影,首先渲染器必须启用阴影贴图,并且定向光必须投射阴影。
//scene.js
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // not necessary but it makes the shadows a little nicer
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 4096; // increases the shadow mapSize so the shadows are sharper
directionalLight.shadow.mapSize.height = 4096;
你还需要一个带有castShadow = true的对象来阻挡进入卡通着色对象的光线。
接下来,我们需要在顶点和片段着色器中使用 Three.js 中的一些实用程序,以便将阴影贴图数据正确传递到片段着色器。
toon.vert:
#include
#include
void main() {
#include
#include
#include
#include
#include
// ... rest stays the same
}
toon.frag:
#include
#include
#include
#include
#include
void main() {
// ...rest stays the same
}
通过这些包含,我们现在可以访问 orientalLightShadows 数组和函数 getShadow。 从这里,我们使用适当的定向光阴影调用 getShadow 函数,内置 Three.js 着色器将根据已经为灯光生成的阴影贴图计算给定顶点的阴影。 可以在
shadowmap_pars_fragment.glsl.js 中找到此函数的源代码。
这就是最终的 toon.frag 片段着色器的样子,其中突出显示了阴影计算。
#include
#include
#include
#include
#include
uniform vec3 uColor;
uniform float uGlossiness;
varying vec3 vNormal;
varying vec3 vViewDir;
void main() {
// shadow map
DirectionalLightShadow directionalShadow = directionalLightShadows[0];
float shadow = getShadow(
directionalShadowMap[0],
directionalShadow.shadowMapSize,
directionalShadow.shadowBias,
directionalShadow.shadowRadius,
vDirectionalShadowCoord[0]
);
// directional light
float NdotL = dot(vNormal, directionalLights[0].direction);
float lightIntensity = smoothstep(0.0, 0.01, NdotL * shadow);
vec3 directionalLight = directionalLights[0].color * lightIntensity;
// specular reflection
vec3 halfVector = normalize(directionalLights[0].direction + vViewDir);
float NdotH = dot(vNormal, halfVector);
float specularIntensity = pow(NdotH * lightIntensity, 1000.0 / uGlossiness);
float specularIntensitySmooth = smoothstep(0.05, 0.1, specularIntensity);
vec3 specular = specularIntensitySmooth * directionalLights[0].color;
// rim lighting
float rimDot = 1.0 - dot(vViewDir, vNormal);
float rimAmount = 0.6;
float rimThreshold = 0.2;
float rimIntensity = rimDot * pow(NdotL, rimThreshold);
rimIntensity = smoothstep(rimAmount - 0.01, rimAmount + 0.01, rimIntensity);
vec3 rim = rimIntensity * directionalLights[0].color;
gl_FragColor = vec4(uColor * (ambientLightColor + directionalLight + specular + rim), 1.0);
}
效果如下:
8、结束语
就像罗伊斯坦在上面链接的教程中所说的那样,卡通着色本质上是实现一个应用阶跃函数的照明模型,以便在光和阴影之间有清晰的截止。 尽管如此,这个风格化的着色器仍然可以调整以获得额外的效果。
我在开发这个着色器时注意到的几点可能有用:
- 多边形数量将影响投射的阴影对象的类型(实际上与着色器无关)
- 平滑着色的对象将具有更好的核心阴影、镜面反射和边缘照明。
- 如果你在其他 3D 软件中使用平滑着色创建模型,请确保导出具有计算法线的模型
原文链接:
http://www.bimant.com/blog/three-toon-shader-material/
相关推荐
- msp的昌伟哥哥(伟昌怎么样)
-
佩戴HoloLens的多个用户可以使用场景共享特性来获取集合视野,并可以与固定在空间中某个位置的同一全息对象进行交互操作。这一切是通过空间锚共享(AnchorSharing)来实现的。为了使用共享服...
- VOculus Rift、Gear VR平台开发者合作申请指南
-
编译/游戏陀螺案山子OculusHome平台——OculusRift和三星Gear主要的应用平台,包括PC版和移动版都可以使用。而现在使用的OculusShare平台,据悉将来也会整合到Ocu...
- 游戏中的"状态机”和"行为树”是什么?
-
状态机是一种模型,用于描述对象在不同状态下的行为和转换。在游戏里,状态机通常用于控制角色或NPC在不同状态下的行为。比如说,一个角色可以有多个状态,比如“待机”、“行走”、“攻击”、“受伤”等,每个状...
- JetBrains Rider现已支持PS5和Xbox主机游戏开发
-
IT之家3月27日消息,Rider是一款由JetBrains出品的跨平台.NETIDE,在2024.3版本中,JetBrainsRider增加了对PlayStation5...
- Unity WebGL 应用开发总结(unity webgl发布)
-
UnityWebGL应用开发总结1.开发环境软件版本Unity2020.1.0f1PyCharm2022.3.2Python3.7.32.编译WebGL对Unity项目进行WebGL编译时...
- 【6.Physics和动画】5.动画(动画电影)
-
5.动画现在,角色可以移动了,但在移动时形象一直不变,对于玩家来说比较生硬,本节中我们让角色在移动时能够播放动画。Unity2D游戏中,角色动画一般采用帧动画的形式来实现。所谓帧动画就是在每一帧显...
- unity3d开发教程-开发环境搭建(unity3d开源项目)
-
一、安装Unity1、从官网下载UnityHub:https://unity.com/download,选择[DownloadforWindows]下载完成后,双击打开安装。一直点...
- 【2.UI元素】3.Panel and Button(ui界面元素)
-
3.PanelandButton3.1PanelPanel(面板)本质上就是预先设置好的Image。可以作为其他UI元素的父级。在层级窗口右击选择UI->Panel即可创建。...
- 揭秘!你玩的字节抖音小游戏制作流程公布
-
1.1注册字节开发者后台1.2Unity版本说明1.3检查AppID是否有效2.1创建项目2.2接入SDK3.1发布安卓Apk3.2发布双端WebGL3.3IOS15.4版本问题字节抖...
- 临时工闯下大祸《糖豆人》源代码更新时不慎泄露
-
这次《糖豆人》工作室Mediatonic的临时工闯下了大祸,在更新时一不留神把游戏的源代码给泄露了。当然,这次泄露之后,官方删除的动作也很快,但是没快过SteamDB创始人PavelDjundik...
- 为3D手游打造, Visual Studio Unity扩展下载
-
IT之家(www.ithome.com):为3D手游打造,VisualStudioUnity扩展下载7月30日消息,微软正式发布升级版VisualStudioToolsforUnity扩...
- 【2.C#基础】3.脚本入门(c# 脚本引擎)
-
3.脚本入门3.1脚本概要在上一节创建的脚本中,包含了一段模板代码,双击工程窗口中的脚本图标,系统自动打开代码编辑器(VSCode)可以看到代码如下图所示:说明:System.Collections...
- unity专题:unitask库(1)(unitypackage)
-
UniTask是一个Unity引擎中的异步编程库,它可以帮助你在Unity项目中编写更简洁、高性能的异步代码。UniTask以Promise/Task的编程模式为基础,提供了与C#...
- 零基础带你看游戏内灰度效果实现原理
-
前言在Unity中实现后处理效果有两种方式:一种是通过使用Unity官方提供的Post-Processing插件。另外一种方式就是使用脚本获取到渲染后帧缓冲区的图像,再通过shader写后处理的效果,...
- 团结引擎自定义Scene视图的层叠面板和工具栏
-
团结引擎提供的了功能,可以为Scene视图添加层叠面板和自定义工具栏,这里学习官方的经典案例。创建层叠面板。总结需要三个步骤:1、创建编辑器脚本(需存放在Editor目录下)2、继承Overlay类,...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- wireshark怎么抓包 (75)
- qt sleep (64)
- cs1.6指令代码大全 (55)
- factory-method (60)
- sqlite3_bind_blob (52)
- hibernate update (63)
- c++ base64 (70)
- nc 命令 (52)
- wm_close (51)
- epollin (51)
- sqlca.sqlcode (57)
- lua ipairs (60)
- tv_usec (64)
- 命令行进入文件夹 (53)
- postgresql array (57)
- statfs函数 (57)
- .project文件 (54)
- lua require (56)
- for_each (67)
- c#工厂模式 (57)
- wxsqlite3 (66)
- dmesg -c (58)
- fopen参数 (53)
- tar -zxvf -c (55)
- 速递查询 (52)