如何围绕3D模型绘制轮廓?


47

如何围绕3D模型绘制轮廓?我指的是最近的《神奇宝贝》游戏中的特效,它们周围似乎只有一个像素的轮廓:

在此处输入图片说明 在此处输入图片说明


您在使用OpenGL吗?如果是这样,您应该在Google上搜索如何使用OpenGL为模型绘制轮廓。
oxysoft 2014年

1
如果您指的是放置在其中的那些特定图像,我可以95%的确定性说这些是手绘2D精灵,而不是3D模型
Panda Pajama 2014年

3
@PandaPajama:不,几乎可以肯定是3D模型。我不希望手工绘制的精灵在某些帧中的硬线有些草率,无论如何,基本上这就是游戏内3D模型的外观。我想我不能保证这些特定图像的100%,但我无法想象为什么有人会努力伪造它们。
CA McCann 2014年

那是哪一场比赛?看起来很美。
Vegard 2014年

@Vegard背上有萝卜的生物是神奇宝贝游戏中的Bulbasaur。
Damian Yerrick 2014年

Answers:


28

我认为这里的任何其他答案都不会在《神奇宝贝X / Y》中达到效果。我无法确切知道它是如何完成的,但是我想出了一种方法,似乎它们在游戏中的作用差不多。

在《PokémonX / Y》中,轮廓线边缘和其他非轮廓线边缘都绘制了轮廓(如以下屏幕截图中Raichu的耳朵与他的头部交汇处)。

莱丘

在Blender中查看Raichu的网格,您可以看到(上面突出显示为橙色的)耳朵是与头部相交的单独的,断开的对象,从而使表面法线发生了突然变化。

基于此,我尝试根据法线生成轮廓,这需要通过两遍渲染:

第一遍:渲染没有轮廓的模型(纹理化和cel着色),然后将摄影机空间法线渲染到第二个渲染目标。

第二遍:从第一遍开始对法线进行全屏边缘检测滤镜。

下面的前两个图像显示了第一遍的输出。第三个是轮廓本身,最后一个是最终的组合结果。

德拉蒂尼

这是我在第二遍中用于边缘检测的OpenGL片段着色器。这是我能想到的最好的方法,但是可能会有更好的方法。它也可能没有很好的优化。

// first render target from the first pass
uniform sampler2D uTexColor;
// second render target from the first pass
uniform sampler2D uTexNormals;

uniform vec2 uResolution;

in vec2 fsInUV;

out vec4 fsOut0;

void main(void)
{
  float dx = 1.0 / uResolution.x;
  float dy = 1.0 / uResolution.y;

  vec3 center = sampleNrm( uTexNormals, vec2(0.0, 0.0) );

  // sampling just these 3 neighboring fragments keeps the outline thin.
  vec3 top = sampleNrm( uTexNormals, vec2(0.0, dy) );
  vec3 topRight = sampleNrm( uTexNormals, vec2(dx, dy) );
  vec3 right = sampleNrm( uTexNormals, vec2(dx, 0.0) );

  // the rest is pretty arbitrary, but seemed to give me the
  // best-looking results for whatever reason.

  vec3 t = center - top;
  vec3 r = center - right;
  vec3 tr = center - topRight;

  t = abs( t );
  r = abs( r );
  tr = abs( tr );

  float n;
  n = max( n, t.x );
  n = max( n, t.y );
  n = max( n, t.z );
  n = max( n, r.x );
  n = max( n, r.y );
  n = max( n, r.z );
  n = max( n, tr.x );
  n = max( n, tr.y );
  n = max( n, tr.z );

  // threshold and scale.
  n = 1.0 - clamp( clamp((n * 2.0) - 0.8, 0.0, 1.0) * 1.5, 0.0, 1.0 );

  fsOut0.rgb = texture(uTexColor, fsInUV).rgb * (0.1 + 0.9*n);
}

在渲染第一遍之前,我将法线的渲染目标清除为背对摄像机的矢量:

glDrawBuffer( GL_COLOR_ATTACHMENT1 );
Vec3f clearVec( 0.0, 0.0, -1.0f );
// from normalized vector to rgb color; from [-1,1] to [0,1]
clearVec = (clearVec + Vec3f(1.0f, 1.0f, 1.0f)) * 0.5f;
glClearColor( clearVec.x, clearVec.y, clearVec.z, 0.0f );
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

我在某处读到(我会在评论中放一个链接)Nintendo 3DS使用固定功能管线而不是着色器,所以我想这不可能完全在游戏中完成,但是现在我我坚信我的方法足够接近。


有关Nintendo 3DS硬件的信息:链接
KTC 2014年

非常好的解决方案!您如何考虑着色器的深度?(例如,如果一架飞机在另一架飞机的前面,则它们都具有相同的法线,因此不会绘制轮廓线)
ingham

@ingham这种情况很少出现在我不需要处理的自然角色上,看起来真实的游戏也不能处理它。在真实游戏中,有时法线相同时有时会看到轮廓消失,但我认为人们通常不会注意到它。
KTC

我有点怀疑3DS是否可以运行像这样的基于着色器的全屏效果。它的着色器支持基本(如果有的话)。
塔拉

17

此效果在使用cel阴影效果的游戏中特别常见,但实际上可以独立于cel阴影样式应用。

您所描述的称为“特征边缘渲染”,通常是突出显示模型的各种轮廓和轮廓的过程。有许多可用的技术和关于该主题的许多论文。

一种简单的技术是仅渲染轮廓边缘,即最外轮廓。这可以简单地完成,就像使用模具写操作渲染原始模型,然后仅在没有模具值的情况下以粗线框模式再次渲染它一样。请参阅此处的示例实现。

但是,这不会突出内部轮廓和折痕边缘(如您的图片所示)。通常,要有效地做到这一点,您需要提取有关网格边缘的信息(基于边缘任一侧的面法线的不连续性,并建立代表每个边缘的数据结构。

然后,您可以编写着色器以将这些边缘拉伸或渲染为基础模型上方(或与其结合)的常规几何形状。边缘的位置以及相对于视图矢量的相邻面的法线用于确定是否可以绘制特定的边缘。

您可以在互联网上找到有关各种示例的进一步讨论,详细信息和论文。例如:


1
我可以确认模具方法(来自flipcode.com)有效并且看起来非常不错。您可以在屏幕坐标中给出厚度,以便轮廓的厚度不取决于模型的大小(也不取决于模型的形状)。
Vegard 2014年

1
您没有提及的一种技术是后处理边框阴影效果,通常与cel阴影效果一起使用,该功能会查找具有高像素dz/dx和/或dz/dy
bcrist的

8

最简单的方法(通常在像素/片段着色器之前的较旧硬件上使用,并且仍在移动设备上使用)是复制模型,反转顶点缠绕顺序,以便模型从内向外显示(或者,如果需要,可以可以在3D资产创建工具(例如Blender)中执行此操作,方法是翻转表面法线(同样的操作),然后将整个副本略微扩展到其中心,最后对该副本进行全黑上色/纹理处理。如果是简单的模型(例如多维数据集),则会在原始模型周围产生轮廓。对于具有凹形的更复杂模型(例如下图中的模型),有必要手动调整重复模型,使其比原始模型稍微“笨拙”,例如Minkowski Sum。在3D中。您可以像沿Blender的“收缩/变形”变换那样,将每个顶点沿其法线稍微向外推以形成轮廓网格。

屏幕空间/像素着色器方法趋向于更慢且难以很好地实现,但是OTOH不会使您的世界中的顶点数量增加一倍。因此,如果您要进行高多边形工作,最好选择这种方法。鉴于现代的控制台和台式机具有处理几何的能力,我完全不用担心因子2 的问题。卡通风格=肯定是低多边形,因此最容易复制几何图形。

您可以在例如Blender中自己测试效果,而无需触摸任何代码。轮廓应如下图所示,注意其中一些是内部的,例如腋下。这里有更多细节。

在此处输入图片说明


1
您能否解释一下,“将整个副本略微扩展到中心位置”如何与这张图片相符,因为简单地围绕中心缩放比例不适用于手臂和其他非同心零件,也不适用于具有孔。
克罗姆斯特表示支持莫妮卡

@KromStern 在某些情况下,需要手动缩放顶点子集以适应。修改后的答案。
工程师

1
通常将顶点沿其局部表面法线轻推,但这会导致扩展的轮廓网格沿硬边分裂
DMGregory

谢谢!我认为翻转法线没有任何意义,因为重复项将被着色为平坦的纯色(即没有依赖法线的花式照明计算)。通过缩放比例,上色,然后剔除副本的正面,我达到了相同的效果。
蓝色

6

对于平滑模型(非常重要),此效果相当简单。在片段/像素着色器中,您需要着色的片段的法线。如果它非常接近垂直线(dot(surface_normal,view_vector) <= .01-您可能需要使用该阈值),则将片段着色为黑色,而不是其通常的颜色。

这种方法“消耗”了模型的一点轮廓。这可能是您想要的,也可能不是。从口袋妖怪的图片很难分辨这是否正在做。这取决于您是否希望轮廓包含在角色的任何轮廓中,还是希望轮廓包含轮廓(需要使用其他技巧)。

高光将在表面从正面过渡到背面的任何部分上,包括“内边缘”(例如绿色口袋妖怪的腿或头部)-其他一些技术不会在上面增加任何轮廓)。

使用这种方法,具有坚硬,不光滑边缘(如立方体)的对象将不会在所需位置接收高光。这意味着这种方法在某些情况下根本不是一种选择。我不知道口袋妖怪模型是否都光滑。


5

我看到的最常见的方式是通过模型上的第二次渲染传递。本质上,将其复制并翻转法线,然后将其推入顶点着色器。在着色器中,沿其法线缩放每个顶点。在像素/片段着色器中,绘制黑色。这将为您提供内部和外部轮廓,例如围绕嘴唇,眼睛等。这实际上是一个相当便宜的绘图调用,如果没有其他方面,通常比后处理线条便宜,具体取决于模型的数量及其复杂性。Guilty Gear Xrd使用此方法是因为它很容易通过顶点颜色控制线条的粗细。

我从同一游戏中学到的做内线的第二种方法。在UV贴图中,沿u轴或v轴对齐纹理,尤其是在需要内线的区域。沿任一轴绘制一条黑线,然后将UV坐标移入或移出该线以创建内线。

请观看来自GDC的视频,以获得更好的解释:https : //www.youtube.com/watch?v=yhGjCzxJV3E


5

绘制轮廓的方法之一是使用模型的法线向量。法线向量是垂直于其表面(指向远离表面)的向量。这里的技巧是将角色模型分为两部分。面向相机的顶点和背离相机的顶点。我们将它们分别称为FRONT和BACK。

对于轮廓,我们取回顶点,并沿其法线向量的方向稍微移动它们。考虑一下它,就像使我们背离镜头的角色部分变得更胖。完成此操作后,我们为他们分配我们选择的颜色,并且轮廓很漂亮。

在此处输入图片说明

Shader "Custom/OutlineShader" {
    Properties {
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _Outline("Outline Thickness", Range(0.0, 0.3)) = 0.002
        _OutlineColor("Outline Color", Color) = (0,0,0,1)
    }

    CGINCLUDE
    #include "UnityCG.cginc"

    sampler2D _MainTex;
    half4 _MainTex_ST;

    half _Outline;
    half4 _OutlineColor;

    struct appdata {
        half4 vertex : POSITION;
        half4 uv : TEXCOORD0;
        half3 normal : NORMAL;
        fixed4 color : COLOR;
    };

    struct v2f {
        half4 pos : POSITION;
        half2 uv : TEXCOORD0;
        fixed4 color : COLOR;
    };
    ENDCG

    SubShader 
    {
        Tags {
            "RenderType"="Opaque"
            "Queue" = "Transparent"
        }

        Pass{
            Name "OUTLINE"

            Cull Front

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            v2f vert(appdata v)
            {
                v2f o;
                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
                half3 norm = mul((half3x3)UNITY_MATRIX_IT_MV, v.normal);
                half2 offset = TransformViewToProjection(norm.xy);
                o.pos.xy += offset * o.pos.z * _Outline;
                o.color = _OutlineColor;
                return o;
            }

            fixed4 frag(v2f i) : COLOR
            {
                fixed4 o;
                o = i.color;
                return o;
            }
            ENDCG
        }

        Pass 
        {
            Name "TEXTURE"

            Cull Back
            ZWrite On
            ZTest LEqual

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            v2f vert(appdata v)
            {
                v2f o;
                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.color = v.color;
                return o;
            }

            fixed4 frag(v2f i) : COLOR 
            {
                fixed4 o;
                o = tex2D(_MainTex, i.uv.xy);
                return o;
            }
            ENDCG
        }
    } 
}

第41行:“消隐前线”(Cull Front)设置告诉着色器在面向前的顶点上进行消隐。这意味着我们将忽略此遍中的所有正面顶点。我们只剩下想要稍作操作的背面。

第51-53行:沿其法线向量移动顶点的数学运算。

第54行:将顶点颜色设置为我们在着色器属性中定义的选择颜色。

有用的链接:http : //wiki.unity3d.com/index.php/Silhouette-Outlined_Diffuse


更新资料

另一个例子

在此处输入图片说明

在此处输入图片说明

   Shader "Custom/CustomOutline" {
            Properties {
                _Color ("Color", Color) = (1,1,1,1)
                _Outline ("Outline Color", Color) = (0,0,0,1)
                _MainTex ("Albedo (RGB)", 2D) = "white" {}
                _Glossiness ("Smoothness", Range(0,1)) = 0.5
                _Size ("Outline Thickness", Float) = 1.5
            }
            SubShader {
                Tags { "RenderType"="Opaque" }
                LOD 200

                // render outline

                Pass {
                Stencil {
                    Ref 1
                    Comp NotEqual
                }

                Cull Off
                ZWrite Off

                    CGPROGRAM
                    #pragma vertex vert
                    #pragma fragment frag
                    #include "UnityCG.cginc"
                    half _Size;
                    fixed4 _Outline;
                    struct v2f {
                        float4 pos : SV_POSITION;
                    };
                    v2f vert (appdata_base v) {
                        v2f o;
                        v.vertex.xyz += v.normal * _Size;
                        o.pos = UnityObjectToClipPos (v.vertex);
                        return o;
                    }
                    half4 frag (v2f i) : SV_Target
                    {
                        return _Outline;
                    }
                    ENDCG
                }

                Tags { "RenderType"="Opaque" }
                LOD 200

                // render model

                Stencil {
                    Ref 1
                    Comp always
                    Pass replace
                }


                CGPROGRAM
                // Physically based Standard lighting model, and enable shadows on all light types
                #pragma surface surf Standard fullforwardshadows
                // Use shader model 3.0 target, to get nicer looking lighting
                #pragma target 3.0
                sampler2D _MainTex;
                struct Input {
                    float2 uv_MainTex;
                };
                half _Glossiness;
                fixed4 _Color;
                void surf (Input IN, inout SurfaceOutputStandard o) {
                    // Albedo comes from a texture tinted by color
                    fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
                    o.Albedo = c.rgb;
                    // Metallic and smoothness come from slider variables
                    o.Smoothness = _Glossiness;
                    o.Alpha = c.a;
                }
                ENDCG
            }
            FallBack "Diffuse"
        }

为什么在更新的示例中使用模板缓冲区?
塔拉

啊,我明白了。与第一个示例不同,第二个示例仅使用一种生成外部轮廓的方法。您可能要在答案中提及这一点。
塔拉

0

做到这一点的一种好方法是在帧缓冲区纹理上渲染场景,然后在对每个像素执行Sobel滤波的同时渲染该纹理,这是一种易于进行边缘检测的技术。这样,您不仅可以使场景像素化(将低分辨率设置为“帧缓冲”纹理),还可以访问每个像素值以使Sobel正常工作。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.