网上大多实现3D模型绘制通过遍历所有像素进行笔刷范围内检测,对于大图和高模而言,效率太低,无法使用。
基于Shader的实现方案通过将模型在渲染表面上额外多绘制一次,将UV处理后直接当作位置,世界空间的位置作为额外内容,作为顶点着色色器输出。在像素着色器中进行笔刷范围检测。然后将该渲染表面作为模型材质贴图。
效果如下:
目前作者在unity中实现。读者明白原理后很方便在其他引擎中实现。
模型uv必须要无重复,如果uv重复的话会导致在一处绘制之后,另一处同样受到影响。基本所有的3D绘制都有这个要求。
首先,获取被绘制的模型材质中使用的贴图,通过该贴图创建相同的渲染表面,并将材质中的贴图引用改为该渲染表面。
Renderer renderer = GetComponent<Renderer>();
Material material = renderer.material;
resourceTexture = material.GetTexture("_MainTex") as Texture2D;
if(null == resourceTexture)
{
Debug.LogError("只支持修改Texture2D");
return;
}
//创建渲染表面
renderTexture = new RenderTexture(resourceTexture.width, resourceTexture.height, 0, RenderTextureFormat.ARGB32);
material.SetTexture("_MainTex", renderTexture);
Graphics.Blit(resourceTexture, renderTexture);
其次,使用Shader(custom/uvRender)创建绘制的材质,并在每一帧中添加渲染命令,将被绘制模型通过该材质渲染到渲染表面上
//创建绘制材质
if(null == paintMaterial)
{
paintMaterial = new Material(Shader.Find("Custom/UVRender"));
}
//加入绘制指令
renderCommand = new CommandBuffer();
renderCommand.SetRenderTarget(renderTexture);
renderCommand.DrawRenderer(renderer, paintMaterial, 0, -1);
Camera.main.AddCommandBuffer(CameraEvent.AfterEverything, renderCommand);
然后在每一帧更新笔刷的位置信息即可。为了方便,作者这边直接使用球体作为笔刷的位置范围。
void Update()
{
Vector3 position = paintCircle.position;
float scale = paintCircle.localScale.x / 2;
paintMaterial.SetVector("_CirclePoint", new Vector4(position.x, position.y, position.z, scale));
}
这样脚本部分的工作就完成了。
1.直接创建unity的unLight Shader。
2.Shader中添加命令,关闭三角形背面剔除,我们要保证所有面片都被绘制,某些模型面片在用uv作为位置时法线朝里会3.导致被剔除。
3.Shader中将混合模式改为SrcAlpha和OneMinusSrcAlpha,模拟画刷的层层叠加,越来越深的效果。
4.在顶点着色器中将uv进行处理作为位置输出,z,w都强制设为1避免被深度剔除(读者也可通过其他方式保证)。这里的处理是将uv转换为透视空间,如果不转换的话,绘制出来的模型只会占据渲染表面的右下角。Y轴方向也会是反的。将世界坐标存储,用于像素着色器中进行范围检测。
v2f vert (appdata v)
{
v2f o;
o.worldPosition = mul(unity_ObjectToWorld, v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.vertex = float4(v.uv * 2 - 1, 1.0, 1.0);
o.vertex.y = -o.vertex.y;
//o.vertex = float4(v.uv, 1.0, 1.0);
return o;
}
5.在像素着色器中,对世界坐标与球心位置计算距离,判断像素是否在画刷范围内。如果在画刷范围内直接返回一定的颜色值(读者也可通过距离作为uv对贴图进行采样返回特别的样式),不在的话则不绘制。
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
float3 dir = _CirclePoint.xyz - i.worldPosition.xyz;
float distance = length(dir);
if(distance < _CirclePoint.w)
{
float radio = distance / _CirclePoint.w * 0.5;
radio = 1 - radio * radio;
return float4(0, 0, 1, radio / 10);
}
clip(-1);
return float4(0, 0, 0, 0);
}
项目地址
从事或者对图形学有兴趣的可以加个关注,有助交流。后面我也会分享一些相关的技术方案