So verwenden Sie Oasis, um schnell Schlaganfall-Effekte zu erzielen

Vorwort

Striche können in äußere Striche, innere Striche und Nachbearbeitungsstriche unterteilt werden . Der äußere Strich zeigt nur die äußere Kontur des gesamten Modells, und die inneren Details des Modells können nicht gestreichelt werden, wie z. B. innere Elemente wie Augen und Augenbrauen; der innere Strich kann nicht nur die äußere Kontur, sondern auch die innere Kontur darstellen der Augen, Augenbrauen und Gürtel. ; Der Nachbearbeitungshub dient eigentlich dazu, die Mutation der Farbe zu erkennen. Unabhängig davon, ob das Modell Unebenheiten aufweist, solange sich die Farbe ändert, wird es als Umriss gezeichnet.

In diesem Artikel werden zwei Strichmethoden vorgestellt: Die eine besteht darin, zuerst das Modell zu rendern, das Sie streichen möchten, dann das Modell entlang der normalen Linie zu erweitern und den Stricheffekt (z. B. reines Schwarz) als Material des Modells zu rendern, in dem Sie können Verwenden Sie die Vorlage Der zweite Testdurchgang und Frontal Culling rendert nur Striche, der andere ist die Bildnachbearbeitung, die 2D-Grafikalgorithmen verwendet, um eine Kantenerkennung auf dem gesamten Bildschirm durchzuführen. In Oasis gibt es entsprechende Komponenten, die einfach und schnell implementiert werden können.

Verwenden mehrerer Renderer, um innere und äußere Striche zu erzielen

Ab v0.6 unterstützt die Engine kein Shader-Multi-Pass, daher stellt dieser Artikel die Methode des Multi-Renderers vor, um den gleichen Zweck zu erreichen

Das Rendern von 3D-Modellen in Oasis kann mit Renderer-Komponenten implementiert werden. Die Renderer-Komponenten bestehen aus Netzen und Materialien . Die Netze sind für das Speichern von Metadaten wie Scheitelpunkten und Normalen verantwortlich, und die Materialien sind für die Pixelfärbung verantwortlich. Wir müssen den Renderer im Allgemeinen nicht manuell einstellen, indem wir das glTF-Modell laden , wird der Renderer mitgeliefert. Wenn wir Striche rendern, müssen wir nur eine Renderer-Komponente hinzufügen, da mehrere Renderer-Komponenten dasselbe Mesh oder Material gemeinsam nutzen können, das Mesh wird von glTF abgerufen und das Material wird erstellt, indem auf das Tutorial für benutzerdefiniertes Material verwiesen wird , um eine Volltonfarbe zu erstellen Das Strichmaterial reicht aus .

Der Code für den neuen Renderer lautet wie folgt:

const renderers: MeshRenderer[] = [];
// 从 glTF 中获取所有渲染器组件
rootEntity.getComponentsIncludeChildren(MeshRenderer, renderers);

// 新增描边渲染器组件
renderers.forEach((renderer) => {
   const entity = renderer.entity;
   // 新增渲染器组件
   const borderRenderer = entity.addComponent(MeshRenderer);
   // 网格 - 共享网格数据
   borderRenderer.mesh = renderer.mesh;
   // 设置描边材质,具体的会在下文中介绍
   borderRenderer.setMaterial(borderMaterial);
});
复制代码

Äußerer Strich

Der äußere Strich kann anhand des Stencil-Test-Tutorials implementiert werden.Das Grundprinzip besteht darin, zu entscheiden, ob das Fragment gerendert werden soll, indem der Wert des Stencil-Puffers verglichen wird.

1. Füllen Sie die Vorlage aus

Um den Renderstatus in Oasis zu manipulieren, muss dem renderState des Materials lediglich ein Wert zugewiesen werden . Gemäß der obigen Idee öffnen wir zuerst den Vorlagenstatus des ursprünglichen Renderers und erzwingen das Schreiben von 1, da der Standardvorlagenwert von canvas 0 ist, sodass das erste Rendern sicherstellt, dass der Pixelpositionsvorlagenwert des Renderns 1 ist, und der Rest der Stelle ist 0:

const material = renderer.getBorderMaterial();
const stencilState = material.renderState.stencilState;

// 开启模版测试
stencilState.enabled = true;
// 设置参考值为 1
stencilState.referenceValue = 1;
// 通过时将“参考值”写入模版缓冲。因为默认“总是通过”,所以强制写入1
stencilState.passOperationFront = StencilOperation.Replace;
复制代码

Der schwarze Teil in der Abbildung unten zeigt an, dass der Vorlagenwert 1 ist, und der weiße Teil ist der Standardwert 0:
Bild.png

2. Strecken

Als nächstes müssen wir alle Scheitelpunkte des Modells entlang der normalen Richtung erweitern.Sie können sich auf das benutzerdefinierte Material-Tutorial beziehen und gl_Position im Vertex - Shader skalieren :

const vertex = `
  attribute vec3 POSITION;
  attribute vec3 NORMAL;

  uniform float u_width;
  uniform mat4 u_MVPMat;
  uniform mat4 u_modelMat;
  uniform mat4 u_viewMat;
  uniform mat4 u_projMat;
  uniform mat4 u_normalMat;

  void main() {
     vec4 mPosition = u_modelMat * vec4(POSITION, 1.0);
     // 得到世界坐标系下的归一化法线矢量
     vec3 mNormal = normalize( mat3(u_normalMat) * NORMAL );
     // 将模型位置沿着法线方向缩放 u_width
     mPosition.xyz += mNormal * u_width;
     gl_Position = u_projMat * u_viewMat * mPosition;
  }
`;
复制代码

描边材质只需要在片元着色器中设置纯色即可:

const fragment = `
uniform vec3 u_color;

void main(){
  gl_FragColor = vec4(u_color, 1);
}
`;
复制代码

创建完描边材质后,我们将它绑定到新增的渲染器组件上,我们会发现缩放的渲染器组件生效了,但是会挡在原来的渲染器前面,如下图:
border-1.gif
这是因为我们还没有对描边材质配置模版状态,这时候生效的是深度测试,因为撑大后的模型离屏幕更近,所以覆盖了原来的渲染器。

3. 模板测试

第一个渲染器已经将 1 填入了模板缓冲,我们可以在渲染撑大的模型的时候,只渲染那些不等于1的像素,即将模板测试的比较函数设置为不相等

如下图所示,利用模板测试只渲染了撑大的部分,即描边部分:
image.png
描边材质的模板测试代码如下:

// 保证最后再渲染描边材质
material.renderQueueType = RenderQueueType.Transparent + 1;

// 双面渲染防止一些破面
material.renderState.rasterState.cullMode = CullMode.Off;
const stencilState = material.renderState.stencilState;
// 开启模版测试
stencilState.enabled = true;

// 设置比较值
stencilState.referenceValue = 1;

// 通过条件:referenceValue != 模板缓冲值
stencilState.compareFunctionFront = CompareFunction.NotEqual;
stencilState.compareFunctionBack = CompareFunction.NotEqual;
复制代码

最终达到的外描边效果:
image.pngimage.png

内描边

因为模板测试只能渲染撑大的那一部分,所以无法显示内轮廓,比如腰带等等,我们一般使用第二遍 shader pass 采用正面剔除的方法来实现内描边。

1. 渲染正面

正常渲染模型,不需要设置任何模版状态或者深度状态。

2. 撑大

撑大部分的代码还是和前面一样,沿着法线缩放,参考前面即可。

3. 渲染背面
// 保证最后渲染描边材质
material.renderQueueType = RenderQueueType.Transparent + 1;
// 剔除正面,只渲染背面
material.renderState.rasterState.cullMode = CullMode.Front;
复制代码

渲染撑大后的背面,能够保证撑大的模型不会因为深度测试挡住本来的模型,也不会因为模板测试只渲染模型的外轮廓,内描边效果如下图,可以看到,模型的内部轮廓也显示了:
image.pngimage.png

使用后处理实现描边

在前言里面提到过,除了使用多渲染器方案实现内外描边效果外,还可以使用后处理方案对图像进行边缘检测。

后处理的优点:

  • 在某些场景下(如模型面数非常多)能够提高性能。
  • 能够整体处理描边效果,解决多模型描边重合的问题。
  • 能够解决模型撑大后破面的问题(如正方体撑大后因为法线拐角超过90度,会导致破面)。

1. 渲染离屏纹理

Oasis 使用 RenderTarget 来渲染离屏纹理,即不渲染到屏幕,而是渲染到一张纹理上,然后利用这张离屏纹理来实现各种后处理特效,比如描边。

const { width, height } = engine.canvas;
const renderColorTexture = new RenderColorTexture(engine, width, height);
const renderTarget = new RenderTarget(engine, width, height, renderColorTexture);
复制代码

2.添加脚本

RenderTarget 是过程式,我们需要利用脚本的生命周期将整个渲染管线串起来。

我们可以给相机创建一个脚本,在渲染场景前设置 renderTarget ,代表我们想将场景渲染到纹理上;在渲染后,我们将 renderTarget 清空,并设置了和全屏平面一样的 Layer1,代表我们想将离屏纹理绘制到屏幕上,并进行描边后处理:

 class PostScript extends Script {
  renderTarget: RenderTarget;

  onBeginRender(camera: Camera): void {
    // 绘制场景到纹理上
    camera.renderTarget = this.renderTarget;
    camera.cullingMask = Layer.Layer0;
  }

  onEndRender(camera: Camera): void {
    camera.renderTarget = null;
    // 绘制到屏幕上,并在screenMaterial里面进行描边后处理
    camera.cullingMask = Layer.Layer1;
    // 再绘制一遍,只绘制全屏纹理
    camera.render();
  }
}


// 创建全屏 Plane, 用来实现全屏后处理特效。
const screen = rootEntity.createChild("screen"));
const screenRenderer = screen.addComponent(MeshRenderer);
screen.layer = Layer.Layer1;
screenRenderer.mesh = PrimitiveMesh.createPlane(engine, 2, 2);
// 自定义材质,shader 里面运行边缘检测算法。 
const material = this.getScreenMaterial(engine);
screenRenderer.setMaterial(material);

const renderColorTexture = new RenderColorTexture(engine, width, height);
const renderTarget = new RenderTarget(engine, width, height, renderColorTexture);
// 传入默认管线得到的离屏纹理。
material.shaderData.setTexture("u_texture", renderColorTexture);

// 添加脚本
const screenRenderer = screen.addComponent(MeshRenderer);
screenRenderer.renderTarget = renderTarget;

复制代码

可以看到,我们创建了一个掩码为 Layer1 的平面, 只用来渲染一个全屏平面,如果直接渲染离屏纹理,看到的效果就是场景直接渲染在屏幕上,但是我们想要描边效果,就需要对这张纹理做一点算法处理。

3. sobel 边缘检测

边缘检测的方法很多,这里用 sobel 卷积因子来举例。我们把后处理中的每一个像素亮度分别进行纵向、横向的卷积运算,即每个像素的周边9个像素亮度分别乘以下图这些权重:
image.png
G_x

image.png
G_y

黄色和红色为分别对应的权重,可以看到,如果一个像素的周边信号(如亮度)变化越陡峭,则卷积结果越大,我们可以利用这个特性来检测边缘,因为边缘那几个像素的亮度变化都是比较陡峭的。

4. shader 代码

基于 sobel 的边缘检测算法,我们可以通过离屏纹理,绘制描边效果,shader 代码如下:

// 描边颜色
uniform vec3 u_color;
// 离屏纹理
uniform sampler2D u_texture;
// 纹素大小
uniform vec2 u_texSize;

varying vec2 v_uv;

// 对应像素的亮度
float luminance(vec4 color) {
    return  0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b; 
}

// 前面提到的 sobel 卷积运算,得到陡峭程度。
float sobel() {
  // sobel 卷积因子
  float Gx[9] = float[](
              -1.0,  0.0,  1.0,
              -2.0,  0.0,  2.0,
              -1.0,  0.0,  1.0);
  float Gy[9] = float[](
              -1.0, -2.0, -1.0,
              0.0,  0.0,  0.0,
              1.0,  2.0,  1.0);		
      
  float texColor;
  float edgeX = 0.0;
  float edgeY = 0.0;
  vec2 uv[9];

  // 周边的9个像素的纹理坐标。
  uv[0] = v_uv + u_texSize.xy * vec2(-1, -1);
  uv[1] = v_uv + u_texSize.xy * vec2(0, -1);
  uv[2] = v_uv + u_texSize.xy * vec2(1, -1);
  uv[3] = v_uv + u_texSize.xy * vec2(-1, 0);
  uv[4] = v_uv + u_texSize.xy * vec2(0, 0);
  uv[5] = v_uv + u_texSize.xy * vec2(1, 0);
  uv[6] = v_uv + u_texSize.xy * vec2(-1, 1);
  uv[7] = v_uv + u_texSize.xy * vec2(0, 1);
  uv[8] = v_uv + u_texSize.xy * vec2(1, 1);

  for (int i = 0; i < 9; i++) {
    // 计算下对应纹素的亮度
    texColor = luminance(texture2D(u_texture, uv[i]));
    // 横向卷积
    edgeX += texColor * Gx[i];
    // 纵向卷积
    edgeY += texColor * Gy[i];
  }
                  
    return abs(edgeX) + abs(edgeY);
}

void main(){
  float sobelFactor = sobel();
  // sobel 越大,说明陡峭程度越大,越接近描边颜色。
  gl_FragColor = mix( texture2D(u_texture, v_uv), vec4(u_color,1.0), sobelFactor);  
}
复制代码

得到渲染结果如下:
image.png

结语

本篇所有相关代码和效果可以通过 Playground 查看。

  • Ab Oasis 0.6 hat Oasis die entsprechenden Vollbild-Nachbearbeitungskomponenten und Shader-Multi-Pass-Fähigkeiten nicht eingekapselt.Benutzer müssen mehrere Renderer erstellen oder Off-Screen-Texturen, Skripte und Vollbild-Plane erstellen, was a wenig unpraktisch. Wir werden das entsprechende in Zukunft kapseln. Die Fähigkeit, Benutzern zu ermöglichen, sich auf die Algorithmusimplementierung zu konzentrieren und Prozessschritte zu rationalisieren.
  • Es gibt viele Methoden und Details für die Umsetzung des Strichs, wie z. B. die Dicke des Strichs im Linsenraum, die Zackigkeit des Strichs, die gebrochene Fläche des Strichs usw. Im Allgemeinen können Sie die geeignete Methode für bestimmte auswählen Anwendungsszenarien zu implementieren oder anzupassen.

Zu guter Letzt

Um der Mehrheit der Benutzer der Oasis-Engine einen effizienteren und bequemeren Arbeitsablauf zu bieten, ist die Öffnung des Editors für die Außenwelt einer der Kernpunkte unseres Oasis-Teams. Um Ihnen besser dienen zu können, haben wir einen Fragebogen zum Thema " Interaktive Grafik-Engine-Forschung " vorbereitet ( glückliche Benutzer erhalten von uns vorbereitete exquisite Preise ). Wir freuen uns auf Ihr Feedback~
170837-256.png

Ich denke du magst

Origin juejin.im/post/7084165043264159775
Empfohlen
Rangfolge