之前有看到过一种用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);
//因为是向量所以w为0
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(0.5,0.5,0)使原点到中心
//加rd*0.001是避免检测到正对摄像机的外表面
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
}
}
}
结果如图:
这里有几个需要注意的点:
既然空间已经构建出来了,那加上贴图就是很简单的事情了,不同的轴向上使用不同的模型坐标去采样即可,代码如下:
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
}
}
}
上面的代码又是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论坛上也是讨论用来做反射。
这部分Unity论坛上也有大佬讨论和完整代码,完整代码我就不贴了,可以点链接去看。我只说我不懂那部分,或者说一开始没看懂的部分。
// room uvs
float2 roomUV = frac(i.uv);
// raytrace box from tangent view dir
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上。