Three.js卡通材质实现简明教程

news2024/11/23 21:07:28

继 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 <common>#include <lights_pars_begin> 包含一些通用和灯光助手,并且我们可以访问场景的有向光!

#include <common>
#include <lights_pars_begin>

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 <common>
#include <lights_pars_begin>

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 <lights_pars_begin> 吗? 在这个 #include中,Three.js已经给了我们 ambientLightColor,我们所要做的就是将它应用到 gl_FragColor :

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 <common>
#include <shadowmap_pars_vertex>

void main() {
    #include <beginnormal_vertex>
    #include <defaultnormal_vertex>

    #include <begin_vertex>

    #include <worldpos_vertex>
    #include <shadowmap_vertex>

    // ... rest stays the same
}

toon.frag:

#include <common>
#include <packing>
#include <lights_pars_begin>
#include <shadowmap_pars_fragment>
#include <shadowmask_pars_fragment>

void main() {
    // ...rest stays the same
}

通过这些包含,我们现在可以访问 orientalLightShadows 数组和函数 getShadow。 从这里,我们使用适当的定向光阴影调用 getShadow 函数,内置 Three.js 着色器将根据已经为灯光生成的阴影贴图计算给定顶点的阴影。 可以在shadowmap_pars_fragment.glsl.js 中找到此函数的源代码。

这就是最终的 toon.frag 片段着色器的样子,其中突出显示了阴影计算。

#include <common>
#include <packing>
#include <lights_pars_begin>
#include <shadowmap_pars_fragment>
#include <shadowmask_pars_fragment>

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 软件中使用平滑着色创建模型,请确保导出具有计算法线的模型

原文链接:Three.js卡通材质实现 — BimAnt

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/714846.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

mysql索引之Hash

在存储引擎中Memory引擎是支持Hash索引的&#xff0c;Hash索引跟java中的HashMap很像&#xff0c;有很多槽&#xff0c;存的也是键值对&#xff0c;键值为索引列&#xff0c;值为这条数据的行指针&#xff0c;通过指针就可以找到数据。 但是Hash索引应用的并不多&#xff0c;原…

一篇文章解释清楚IOC和DI

背景 众所周知我们要学习Spring&#xff0c;必不可少的就是IOC和AOP&#xff0c;那就让我们了解一下什么是IOC&#xff0c;开启下面的学习吧。 过程 什么是IOC&#xff1f; Ioc—Inversion of Control&#xff0c;即“控制反转”&#xff0c;不是什么技术&#xff0c;而是一…

VSCode 2019 “对COM组件的调用返回了错误HRESULT E_FAIL” 的解决

问题&#xff1a; VSCode使用 “MFC应用”模板创建项目时&#xff0c;出现&#xff1a;文件夹打不开&#xff0c;并弹出 “对COM组件的调用返回了错误HRESULT E_FAIL” 错误 解决方案&#xff1a; 1. 以管理员身份打开Developer Command Prompt for VS 2019&#xff08;vs2…

敏捷开发发展和优缺点

目录 1 概述1.1 四种开发模式1.1.1 瀑布式开发1.1.2 螺旋模型1.1.3 迭代式开发1.1.4 敏捷开发 1.2 开发模式对比 2 敏捷开发2.1 敏捷宣言2.1.1 敏捷宣言解读2.1.2 敏捷宣言价值观 2.2 敏捷准则2.2.1 目的&#xff1a;是客户满意2.2.2 态度&#xff1a;欢迎需求变更2.2.3 关注&a…

加油,也可以更智慧

摘要&#xff1a;智慧加油站及油库管理系统的应用引擎是结合了华为云Roma Exchange能力&#xff0c;提升应用开发、部署和升级效率&#xff0c;支撑应用快速开发、远程部署。 停车、加油、驶离…… 从开车进场到离场&#xff0c;2分钟内即可完成“即加即走”的无感加油支付有没…

如何自动批量查询手机号归属地?

我们在工作生活中可能会收集到很多用户的手机号&#xff0c;我们如果想获取手机号归属地&#xff0c;只能一个个人工查询。如果数据量较多的情况就会比较耗费时间。有没有什么方法可以自动查询手机号归属地呢&#xff1f;当然可以&#xff0c;并且这个方法还是免费的。 首先&a…

qt-线程竞争共享资源和读写锁--QReadWriteLock

目录 一、线程竞争的概念2、什么是线程竞争2、什么是线程竞争共享资源&#xff1f; 二、读写锁1、读写锁的概念2、读写锁的工作原理如下&#xff1a;3、使用读写锁的示例&#xff08;QReadWriteLock&#xff09; 三、总结&#xff1a; 一、线程竞争的概念 2、什么是线程竞争 …

网络安全进阶学习第五课——文件上传漏洞

文章目录 一、常见文件上传点二、任意文件上传漏洞三、任意文件上传危害四、webshell五、上传木马所需条件六、木马上传流程七、上传绕过1、绕过JS验证1&#xff09;Burpsuite剔除响应JS。2&#xff09;浏览器审计工具剔除JS 2、绕过MIME-Type验证1&#xff09;利用抓包工具&am…

Session 反序列化漏洞

将$_SESSION中保存的所有数据序列化存储到PHPSESSID对应的文件中有三种存取格式&#xff1a; &#xff08;1&#xff09;默认使用php&#xff1a;键名|键值&#xff08;经过序列化函数处理的值&#xff09; name|s:6:"1FonlY"; &#xff08;2&#xff09;php_seri…

Multi-level Wavelet-CNN for Image Restoration论文总结

论文&#xff1a;Multi-level Wavelet-CNN for Image Restoration 源码&#xff1a;GitHub - lpj0/MWCNN: Multi-level Wavelet-CNN for Image Restoration 目录 一、背景和出发点 二、创新点 三、MWCNN具体实现 四、DWT与池化运算和膨胀卷积相关性证明 五、DWT、IWT代码实…

阿里云服务器白嫖教程

阿里云服务器白嫖教程 第一步:打开百度第二步:进入阿里云官方,注册登录账号第三步:点击免费试用第四步:点击立即试用第五步:选择操作系统第五步:选择到期释放设置![在这里插入图片描述](https://img-blog.csdnimg.cn/d02f4582dd5943319441df9ccbae60f0.png)第六步:同意协议,并立…

3D深度视觉与myCobot 320机械臂无序抓取

今天我记录使用myCobot320 M5跟FS820-E1深度相机进行一个无序抓取物体的分享。 为什么会选择深度相机和机械臂做一个案例呢&#xff1f; 2D相机&#xff08;最常见使用的相机&#xff09;可以捕捉二维图像&#xff0c;也就是在水平和垂直方向上的像素值。它们通常用于拍摄静态…

F#奇妙游(5):计算π的值

F#到底有什么用&#xff1f; 奇妙游写到第五篇&#xff0c;前面的几篇都是开场白&#xff1a; 一个用F#编写WinForm的例子donet命令行工具&#xff0c;也就是F#的开发环境关于函数和函数式编程的碎碎念函数式编程的核心概念&#xff1a;值 下面&#xff0c;我们开始正式来搞…

数据库左、右、内、逗号、全连接(mysql不包含全连接)方式

1、准备数据 学生有归属班级 学生表 班级表 2、执行查询语句 2.1执行左关联 select * from student stu left join class cla on (stu.class_idcla.class_id); 结果如下 2.2执行右关联 2.3、执行内连接 2.4执行逗号分隔表的连接方式 和内连接的查询结果是一样的

实训笔记7.3

实训笔记7.3 7.3一、座右铭二、单例模式三、IDEA集成开发环境的安装和基本使用四、Debug断点调试4.1 作用有两个4.2 用法&#xff1a;4.3 IDEA设置step into进入JDK源码4.4 step over4.5 step into 五、Java中的网络编程5.1 网络编程的三个核心要素5.2 通过Java实现网络编程 7.…

第三章 搜索与图论(二)——最短路问题

文章目录 单源最短路朴素Dijkstra堆优化版DijkstraBellman Ford算法SPFASPFA求负环 多源汇最短路Floyd 最短路练习题849. Dijkstra求最短路 I850. Dijkstra求最短路 II853. 有边数限制的最短路851. spfa求最短路852. spfa判断负环854. Floyd求最短路 源点表示起点&#xff0c;汇…

Linux系统远程挂载Mac OS系统目录方法

打开mac文件共享功能 开启共享服务 进入系统偏好设置中的共享选项。勾中文件共享&#xff08;如下图&#xff09;&#xff0c;之后右边的文件共享的绿灯会点亮&#xff0c;并显示“文件共享&#xff1a;打开”。 添加共享目录 点击在文件共享界面&#xff08;如下图&#x…

【狂神】MySQL - 数据库级别的外键

1. 外键 FOREIGN KEY (了解) 测试数据 &#xff1a; 学生表 CREATE TABLE IF NOT EXISTS student (id INT(4) NOT NULL AUTO_INCREMENT COMMENT 学号,name VARCHAR(30) NOT NULL DEFAULT 匿名 COMMENT 姓名,pwd VARCHAR(20) NOT NULL DEFAULT 123456 COMMENT 密码,sex VARC…

【数据结构与算法】 完成用十字链表存储的稀疏矩阵的加法运算

题目&#xff1a; Qestion: 完成用十字链表存储的稀疏矩阵的加法运算。 主要思路&#xff1a; 获取两个稀疏矩阵总有多少个非零元素&#xff0c;记作cnt。当cnt 不为零时一直循环&#xff0c;每循环一次i&#xff0c;也就是行循环&#xff0c;每循环一次就转移至下一行。先从…

Git常用指令总结

1、git init&#xff1a;初始化一个Git仓库&#xff1b; 2、git clone&#xff1a;从远程仓库克隆代码到本地&#xff1b; 直接使用网址 git clone <url>or 用a代替网址 git remote add a <url>git clone a3、git add&#xff1a;添加文件到暂存区&#xff1b; 文件…