简述
之前有看到过一种用CubeMap构建出空间的效果,只是一直不知道叫什么名字。最近闲下来了想起了这玩意,就通过万能的谷歌搜到了这个技术的名字——Interior mapping,百度翻译是内部映射。
然后我又发现已经有大佬写的比较详细了,比如案例学习——Interior Mapping 室内映射(假室内效果)、以及一种假室内(Fake Interior)效果的实现。虽有珠玉在前,但是我还是想按照我自己的思路来记录一下。我找到了这个技术的论文地址,看了一下发现有点头大,因为它上面都是纯原理的东西,没有代码。于是我又翻到一篇比较按照论文的思路来写的代码Interior mapping,总算是解决了我的诸多不解和疑惑。
计算平面
根据图示我们需要找到pixel上下两个平面(这里用Y轴方向做示例),其中pixel的位置是已知的平面上的点,d为两个平面间距离也是已知的。根据pixel和d,我们可以求出上下两个平面的高度,分别是
R
o
o
f
=
c
e
i
l
(
y
/
d
)
?
d
Roof=ceil\left ( y/d\right )\ast d
Roof=ceil(y/d)?d、
F
l
o
o
r
=
(
c
e
i
l
(
y
/
d
)
?
1
)
?
d
Floor=\left ( ceil\left ( y/d\right )-1\right )\ast d
Floor=(ceil(y/d)?1)?d,其中
y
y
y为pixel的y坐标,
c
e
i
l
(
)
ceil\left ( \right )
ceil()会返回大于或等于输入值的最小整数。剩下的X轴方向和Z轴方向也是一个意思。 通过上面的计算我们获得了平面的高度,即Y轴方向平面的位置,接下来就可以求交了。接下来就是求直线和平面相交的问题,因为平面是无限大的所以只要直线不平行于平面那相交是必然的(现实的计算里基本上相交是必然的)。那么射线和平面相交该怎么算呢?这篇文章里面有详细的介绍A Minimal Ray-Tracer: Rendering Simple Shapes这里就不展开说了,直接用结论。
t
=
(
l
0
?
p
o
)
?
n
l
?
n
t=\frac{\left ( l_{0}-p_{o}\right )\cdot n}{l\cdot n}
t=l?n(l0??po?)?n? p点即为交点,其中t是
l
0
l_{0}
l0?到p点的距离,
l
0
l_{0}
l0?为射线起点即ro,
p
o
p_{o}
po?为平面坐标,n为平面法线。转换成代码(Y轴方向):
float3 rd=normalize(i.viewDir);
float3 ro=i.objectPos;
float3 dir=float3()
float3 wallPos=ceil(ro.y/_Distance)*_Distance*dir;
t0=dot(wallPos-ro.xyz,dir)/dot(rd,dir);
每个平面都会相交,只要找到最近的那个平面就行了,即t最小的结果,整合调整后的完整代码如下:
Shader "MyShader/InteriorMappingTest"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Distance("Distance",Float)=0.2
_WallColorA("Wall Color A",Color)=(1,0,1,1)
_WallColorB("Wall Color B",Color)=(1,1,0,1)
_RoofColor("Roof Color",Color)=(1,0,0,1)
_FloorColor("Floor Color",Color)=(0,1,1,1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 viewDir:TEXCOORD1;
float3 objectPos:TEXCOORD2;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float _Distance;
half3 _WallColorA;
half3 _WallColorB;
half3 _RoofColor;
half3 _FloorColor;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
float3 worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;
float3 worldViewDir=UnityWorldSpaceViewDir(worldPos);
o.viewDir=-mul(unity_WorldToObject,float4(worldViewDir,0));
o.objectPos=v.vertex.xyz;
return o;
}
static float3 up=float3(0,1,0);
static float3 right=float3(1,0,0);
static float3 forward=float3(0,0,1);
half3 intersectPlane(float3 dir,float3 rd,float4 ro,half3 colorA,half3 colorB,half3 baseCol, inout float t)
{
float t0=0;
if(dot(dir,rd)>0)
{
float3 wallPos=ceil(ro.w/_Distance)*_Distance*dir;
t0=dot(wallPos-ro.xyz,dir)/dot(rd,dir);
if(t0<t)
{
t=t0;
baseCol=colorA;
}
}
else
{
float3 wallPos=(ceil(ro.w/_Distance)-1)*_Distance*dir;
t0=dot(wallPos-ro.xyz,dir)/dot(rd,dir);
if(t0<t)
{
t=t0;
baseCol=colorB;
}
}
return baseCol;
}
fixed4 frag (v2f i) : SV_Target
{
float3 rd=normalize(i.viewDir);
float3 ro=i.objectPos-float3(0.5,0.5,0)+rd*0.001;
float t=10000;
half4 col=1;
col.rgb=intersectPlane(up,rd,float4(ro,ro.y),_RoofColor,_FloorColor,col.rgb,t);
col.rgb=intersectPlane(right,rd,float4(ro,ro.x),_WallColorA,_WallColorA,col.rgb,t);
col.rgb=intersectPlane(forward,rd,float4(ro,ro.z),_WallColorB,_WallColorB,col.rgb,t);
return col;
}
ENDCG
}
}
}
结果如图:
这里有几个需要注意的点:
- 这里根据论文将数据都转换到模型空间进行运算。
- 这个的视角方向是指向模型的所以要用反方向。
- 可以根据dot(dir,rd)的值判断是上表面还是下表面。
添加贴图
既然空间已经构建出来了,那加上贴图就是很简单的事情了,不同的轴向上使用不同的模型坐标去采样即可,代码如下:
Shader "MyShader/InteriorMappingTest"
{
Properties
{
_WallTexA("Wall Texture A",2D)="white"{}
_WallTexB("Wall Texture B",2D)="white"{}
_RoofTex ("Texture", 2D) = "white" {}
_FloorTex("Floor Texture",2D)="white"{}
_Distance("Distance",Float)=0.2
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 viewDir:TEXCOORD1;
float3 objectPos:TEXCOORD2;
};
sampler2D _WallTexA;
sampler2D _WallTexB;
sampler2D _RoofTex;
sampler2D _FloorTex;
float _Distance;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
float3 worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;
float3 worldViewDir=UnityWorldSpaceViewDir(worldPos);
o.viewDir=-mul(unity_WorldToObject,float4(worldViewDir,0));
o.objectPos=v.vertex.xyz;
return o;
}
static float3 up=float3(0,1,0);
static float3 right=float3(1,0,0);
static float3 forward=float3(0,0,1);
float2 getUV(float3 pos,float3 dir)
{
return pos.xy*dir.z+pos.xz*dir.y+pos.zy*dir.x;
}
half3 intersectPlane(float3 dir,float3 rd,float4 ro,sampler2D texA,sampler2D texB,half3 baseCol, inout float t)
{
float t0=0;
if(dot(dir,rd)>0)
{
float3 wallPos=ceil(ro.w/_Distance)*_Distance*dir;
t0=dot(wallPos-ro.xyz,dir)/dot(rd,dir);
if(t0<t)
{
t=t0;
float3 pos=ro+rd*t0;
pos=pos/_Distance;
baseCol=tex2D(texA,getUV(pos,dir));
}
}
else
{
float3 wallPos=(ceil(ro.w/_Distance)-1)*_Distance*dir;
t0=dot(wallPos-ro.xyz,dir)/dot(rd,dir);
if(t0<t)
{
t=t0;
float3 pos=ro+rd*t0;
pos=pos/_Distance;
baseCol=tex2D(texB,getUV(pos,dir));
}
}
return baseCol;
}
fixed4 frag (v2f i) : SV_Target
{
float3 rd=normalize(i.viewDir);
float3 ro=i.objectPos-float3(0.5,0.5,0)+rd*0.001;
float t=10000;
half4 col=1;
col.rgb=intersectPlane(up,rd,float4(ro,ro.y),_RoofTex,_FloorTex,col.rgb,t);
col.rgb=intersectPlane(right,rd,float4(ro,ro.x),_WallTexA,_WallTexA,col.rgb,t);
col.rgb=intersectPlane(forward,rd,float4(ro,ro.z),_WallTexB,_WallTexB,col.rgb,t);
return col;
}
ENDCG
}
}
}
使用Cubemap
上面的代码又是if又是每个面都采样一次的,消耗自然不会低。既然是个正方形,那么自然而然就想到了用CubeMap和射线AABB盒相交检测,Unity论坛上也有人做了,只是写得有点弯弯绕绕,而且用的世界空间,这里我对代码稍微进行了下修改,看起来更直观些。
Shader "Unlit/BoxProjection"
{
Properties
{
_Cube ("Reflection Cubemap", Cube) = "_Skybox" {}
_EnvBoxStart ("Env Box Start", Vector) = (0, 0, 0)
_EnvBoxSize ("Env Box Size", Vector) = (1, 1, 1,1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 viewDir:TEXCOORD1;
float3 objectPos:TEXCOORD2;
};
samplerCUBE _Cube;
float4 _Cube_ST;
float4 _EnvBoxStart;
float4 _EnvBoxSize;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
float3 worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;
float3 worldViewDir=UnityWorldSpaceViewDir(worldPos);
o.viewDir=-mul(unity_WorldToObject,float4(worldViewDir,0));
o.objectPos=v.vertex.xyz;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float3 viewDir=i.viewDir;
float3 objectPos=i.objectPos+half3(0.5,0.5,0);
float3 rbmax=(_EnvBoxStart+_EnvBoxSize-objectPos)/viewDir;
float3 rbmin=(_EnvBoxStart-objectPos)/viewDir;
float3 t2=max(rbmin,rbmax);
float fa=min(min(t2.x,t2.y),t2.z);
float3 posNobox=objectPos+viewDir*fa;
float3 reflectDir=posNobox-(_EnvBoxStart+_EnvBoxSize/2);
fixed4 col = texCUBE(_Cube,reflectDir);
return col;
}
ENDCG
}
}
}
我之前有看到网易的天谕静态反射部分用的就是类似的技术,Unity论坛上也是讨论用来做反射。
Tilling And Offset
这部分Unity论坛上也有大佬讨论和完整代码,完整代码我就不贴了,可以点链接去看。我只说我不懂那部分,或者说一开始没看懂的部分。
float2 roomUV = frac(i.uv);
float3 pos = float3(roomUV * 2.0 - 1.0, 1);
float3 id = 1.0 / i.viewDir;
float3 k = abs(id) - pos * id;
float kMin = min(min(k.x, k.y), k.z);
pos += kMin * i.viewDir;
他这个求交部分我很久没弄懂,直到我翻到了Shadertoy的正方形实例部分,于是豁然开朗。
它这个代码还对墙面的采样做了随机翻转降低了重复感。
剩下的比如添加玻璃效果、调整深度、添加灯光、添加人物、更甚至添加阴影什么的就不在本文的讨论范围里了,可以去看看开头那两篇知乎大佬的文章。这里再贴个英文的链接,里面有贴图(demo,我不知道用什么写的,我只用了他的贴图)的下载地址Interior Mapping: Rendering Real Rooms Without Geometry。
我把贴图传到了Github上。
|