Paint in 3D——在模型上绘图的插件,三维模型实时绘图系统——在模型表面绘制纹理
摘要 三维模型实时绘图系统是一种交互式图形技术,允许用户在三维物体表面直接进行绘画操作。系统通过将三维空间交互映射到模型表面的纹理坐标,并实时更新纹理数据来实现这一功能。核心模块包括输入处理、坐标转换、纹理管理和渲染输出等。Unity中的实现通常基于可编程渲染管线和计算着色器,解决空间坐标到纹理坐标映射、纹理实时更新和绘制效果模拟等关键技术问题。该系统在游戏开发、教育软件、工业设计和医疗可视化等领
7.8 三维模型实时绘图系统——在模型表面绘制纹理
7.8.1 三维绘图技术原理与系统架构
三维模型实时绘图系统代表了交互式图形技术的一个重要分支,它允许用户在三维物体的表面直接进行绘画操作,将传统的二维绘画体验扩展到三维空间。这种技术的核心在于将三维空间中的交互动作映射到模型表面的纹理坐标上,并在渲染管线中实时更新纹理数据。
从技术原理上分析,三维绘图系统需要解决几个关键问题:首先是空间坐标到纹理坐标的准确映射,这涉及到射线检测和UV坐标计算;其次是纹理数据的实时更新,需要高效的GPU纹理操作;最后是绘制效果的物理模拟,包括笔刷形状、压力感应和混合模式等。
在Unity生态中,三维绘图插件通常基于可编程渲染管线和计算着色器实现。系统架构通常包含几个核心模块:输入处理模块负责捕捉用户的交互输入(如鼠标、触控或VR控制器);几何处理模块将三维空间中的绘制点映射到模型表面;纹理管理模块负责纹理的创建、更新和存储;渲染模块则负责将更新后的纹理应用到模型上并实时显示。
三维绘图技术的实现方法有多种,包括基于像素的绘制、基于顶点的绘制和基于纹理空间的绘制。基于像素的方法最常用,它通过将绘制操作转换为纹理像素的修改来实现。这种方法需要将三维空间中的绘制点转换到纹理空间,然后在纹理上应用笔刷效果。现代实现通常使用计算着色器或渲染到纹理技术来加速这一过程。
从性能优化的角度看,三维绘图系统面临着内存和计算资源的双重挑战。纹理数据通常需要较高的分辨率才能保证绘制质量,这会导致较大的内存占用。实时更新纹理数据需要频繁的GPU操作,可能成为性能瓶颈。因此,有效的纹理压缩、增量更新和LOD技术是系统设计的关键考虑因素。
三维绘图系统的应用场景非常广泛:在游戏开发中,它可以用于角色自定义、环境标记或谜题交互;在教育软件中,可以用于三维模型标注和解剖学习;在工业设计领域,可以用于产品原型标记和评审;在医疗可视化中,可以用于医学影像标注和手术规划。
7.8.2 核心功能模块与实现原理
系统架构设计
一个完整的三维绘图系统需要多层级的架构设计,包括输入处理、坐标转换、纹理管理和渲染输出等模块。以下是系统的核心架构实现:
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
namespace ThreeDPaintingSystem
{
/// <summary>
/// 笔刷类型枚举
/// </summary>
public enum BrushType
{
Circle,
Square,
Texture,
Eraser,
Spray,
Custom
}
/// <summary>
/// 混合模式枚举
/// </summary>
public enum BlendMode
{
Replace,
Add,
Multiply,
Overlay,
AlphaBlend
}
/// <summary>
/// 绘制模式枚举
/// </summary>
public enum PaintMode
{
Paint,
Erase,
Smudge,
Clone
}
/// <summary>
/// 笔刷配置
/// </summary>
[System.Serializable]
public class BrushConfig
{
[Header("基本属性")]
public BrushType brushType = BrushType.Circle;
public Color brushColor = Color.red;
[Range(1, 100)]
public float brushSize = 10f;
[Range(0, 1)]
public float brushHardness = 0.5f;
[Range(0, 1)]
public float brushOpacity = 1f;
[Header("高级属性")]
public Texture2D brushTexture;
[Range(0, 1)]
public float brushSpacing = 0.1f;
[Range(0, 360)]
public float brushRotation = 0f;
public bool enablePressure = false;
[Range(0, 1)]
public float minSizePressure = 0.5f;
[Range(0, 1)]
public float minOpacityPressure = 0.3f;
[Header("混合设置")]
public BlendMode blendMode = BlendMode.AlphaBlend;
[Range(0, 1)]
public float blendStrength = 1f;
/// <summary>
/// 获取笔刷配置的哈希值
/// </summary>
public int GetHash()
{
unchecked
{
int hash = 17;
hash = hash * 23 + brushType.GetHashCode();
hash = hash * 23 + brushColor.GetHashCode();
hash = hash * 23 + brushSize.GetHashCode();
hash = hash * 23 + brushHardness.GetHashCode();
hash = hash * 23 + brushOpacity.GetHashCode();
hash = hash * 23 + (brushTexture != null ? brushTexture.GetHashCode() : 0);
hash = hash * 23 + brushSpacing.GetHashCode();
hash = hash * 23 + brushRotation.GetHashCode();
hash = hash * 23 + enablePressure.GetHashCode();
hash = hash * 23 + minSizePressure.GetHashCode();
hash = hash * 23 + minOpacityPressure.GetHashCode();
hash = hash * 23 + blendMode.GetHashCode();
hash = hash * 23 + blendStrength.GetHashCode();
return hash;
}
}
}
/// <summary>
/// 绘制画布信息
/// </summary>
public class PaintingCanvas
{
public Renderer targetRenderer;
public int textureWidth = 1024;
public int textureHeight = 1024;
public TextureFormat textureFormat = TextureFormat.ARGB32;
public FilterMode filterMode = FilterMode.Bilinear;
// 纹理资源
public RenderTexture paintTexture;
public Texture2D originalTexture;
public Texture2D currentTexture;
// 状态信息
public bool isInitialized = false;
public Vector2 lastPaintUV = Vector2.zero;
public float lastPaintTime = 0f;
public PaintingCanvas(Renderer renderer, int width = 1024, int height = 1024)
{
targetRenderer = renderer;
textureWidth = width;
textureHeight = height;
InitializeCanvas();
}
/// <summary>
/// 初始化画布
/// </summary>
public void InitializeCanvas()
{
if (targetRenderer == null)
{
Debug.LogError("目标渲染器为空,无法初始化画布");
return;
}
// 获取原始纹理
Material material = targetRenderer.sharedMaterial;
if (material != null && material.HasProperty("_MainTex"))
{
originalTexture = material.GetTexture("_MainTex") as Texture2D;
}
// 创建绘制纹理
CreatePaintTexture();
// 创建当前纹理副本
CreateCurrentTexture();
isInitialized = true;
Debug.Log($"画布初始化完成,尺寸:{textureWidth}x{textureHeight}");
}
/// <summary>
/// 创建绘制纹理
/// </summary>
private void CreatePaintTexture()
{
// 创建RenderTexture用于实时绘制
paintTexture = new RenderTexture(textureWidth, textureHeight, 0, RenderTextureFormat.ARGB32);
paintTexture.name = $"{targetRenderer.name}_PaintTexture";
paintTexture.enableRandomWrite = true;
paintTexture.Create();
// 初始化纹理内容
InitializeTextureContent();
}
/// <summary>
/// 初始化纹理内容
/// </summary>
private void InitializeTextureContent()
{
RenderTexture activeRT = RenderTexture.active;
RenderTexture.active = paintTexture;
// 清空纹理为透明
GL.Clear(false, true, Color.clear);
// 如果有原始纹理,复制到绘制纹理
if (originalTexture != null)
{
Graphics.Blit(originalTexture, paintTexture);
}
RenderTexture.active = activeRT;
}
/// <summary>
/// 创建当前纹理副本
/// </summary>
private void CreateCurrentTexture()
{
currentTexture = new Texture2D(textureWidth, textureHeight, textureFormat, false);
currentTexture.name = $"{targetRenderer.name}_CurrentTexture";
currentTexture.filterMode = filterMode;
currentTexture.wrapMode = TextureWrapMode.Clamp;
// 从RenderTexture复制数据
UpdateCurrentTexture();
}
/// <summary>
/// 更新当前纹理
/// </summary>
public void UpdateCurrentTexture()
{
if (paintTexture == null || currentTexture == null)
{
return;
}
RenderTexture activeRT = RenderTexture.active;
RenderTexture.active = paintTexture;
currentTexture.ReadPixels(new Rect(0, 0, textureWidth, textureHeight), 0, 0);
currentTexture.Apply();
RenderTexture.active = activeRT;
}
/// <summary>
/// 应用纹理到材质
/// </summary>
public void ApplyTextureToMaterial()
{
if (targetRenderer == null || currentTexture == null)
{
return;
}
Material material = targetRenderer.sharedMaterial;
if (material != null)
{
material.SetTexture("_MainTex", currentTexture);
}
}
/// <summary>
/// 清理资源
/// </summary>
public void Cleanup()
{
if (paintTexture != null)
{
paintTexture.Release();
paintTexture = null;
}
if (currentTexture != null)
{
UnityEngine.Object.Destroy(currentTexture);
currentTexture = null;
}
}
}
/// <summary>
/// 三维绘图系统主控制器
/// </summary>
[RequireComponent(typeof(Camera))]
public class ThreeDPaintingController : MonoBehaviour
{
[Header("绘图设置")]
public BrushConfig brushConfig = new BrushConfig();
public PaintMode paintMode = PaintMode.Paint;
[Header("画布设置")]
[Range(256, 4096)]
public int defaultTextureSize = 1024;
public LayerMask paintableLayers = -1;
[Header("输入设置")]
public KeyCode paintKey = KeyCode.Mouse0;
public KeyCode eraseKey = KeyCode.Mouse1;
public KeyCode modifyBrushKey = KeyCode.LeftShift;
[Range(0.01f, 0.5f)]
public float paintDistance = 0.1f;
[Header("性能设置")]
public bool useComputeShader = true;
public bool enableUndoRedo = true;
[Range(1, 50)]
public int maxUndoSteps = 20;
// 内部组件
private Camera paintingCamera;
private ComputeShader paintComputeShader;
private Material paintMaterial;
// 画布管理
private Dictionary<Renderer, PaintingCanvas> paintingCanvases;
private PaintingCanvas currentCanvas;
private Renderer currentTarget;
// 输入状态
private bool isPainting = false;
private Vector2 lastMousePosition;
private float paintPressure = 1f;
// 撤销/重做系统
private Stack<Texture2D> undoStack;
private Stack<Texture2D> redoStack;
// 绘制状态
private Vector2 lastPaintUV;
private float lastPaintTime;
private float paintDistanceAccumulator = 0f;
/// <summary>
/// 初始化绘图系统
/// </summary>
private void Initialize()
{
paintingCamera = GetComponent<Camera>();
if (paintingCamera == null)
{
Debug.LogError("需要摄像机组件");
enabled = false;
return;
}
// 创建绘图材质
Shader paintShader = Shader.Find("Hidden/ThreeDPaint");
if (paintShader == null)
{
Debug.LogError("找不到绘图着色器");
enabled = false;
return;
}
paintMaterial = new Material(paintShader);
paintMaterial.hideFlags = HideFlags.HideAndDontSave;
// 加载计算着色器
if (useComputeShader)
{
paintComputeShader = Resources.Load<ComputeShader>("PaintCompute");
if (paintComputeShader == null)
{
Debug.LogWarning("计算着色器未找到,将使用回退方法");
}
}
// 初始化数据结构
paintingCanvases = new Dictionary<Renderer, PaintingCanvas>();
undoStack = new Stack<Texture2D>();
redoStack = new Stack<Texture2D>();
Debug.Log("三维绘图系统初始化完成");
}
/// <summary>
/// 更新每帧
/// </summary>
private void Update()
{
HandleInput();
// 绘制调试信息
if (Input.GetKeyDown(KeyCode.F1))
{
DebugDrawCanvasInfo();
}
}
/// <summary>
/// 处理输入
/// </summary>
private void HandleInput()
{
// 检查鼠标点击
bool startPaint = Input.GetKeyDown(paintKey);
bool continuePaint = Input.GetKey(paintKey);
bool endPaint = Input.GetKeyUp(paintKey);
bool startErase = Input.GetKeyDown(eraseKey);
bool continueErase = Input.GetKey(eraseKey);
bool endErase = Input.GetKeyUp(eraseKey);
// 修改笔刷大小
if (Input.GetKey(modifyBrushKey))
{
float scroll = Input.GetAxis("Mouse ScrollWheel");
if (Mathf.Abs(scroll) > 0.01f)
{
brushConfig.brushSize = Mathf.Clamp(
brushConfig.brushSize + scroll * 20f,
1f,
100f
);
Debug.Log($"笔刷大小: {brushConfig.brushSize}");
}
}
// 处理绘制/擦除
if (startPaint || startErase)
{
OnPaintStart(startErase);
}
else if (continuePaint || continueErase)
{
OnPaintContinue(startErase);
}
else if (endPaint || endErase)
{
OnPaintEnd();
}
// 处理撤销/重做
if (Input.GetKeyDown(KeyCode.Z) && Input.GetKey(KeyCode.LeftControl))
{
if (Input.GetKey(KeyCode.LeftShift))
{
Redo();
}
else
{
Undo();
}
}
// 保存鼠标位置
lastMousePosition = Input.mousePosition;
}
/// <summary>
/// 开始绘制
/// </summary>
private void OnPaintStart(bool isErase)
{
// 射线检测找到绘制目标
Ray ray = paintingCamera.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, Mathf.Infinity, paintableLayers))
{
Renderer hitRenderer = hit.collider.GetComponent<Renderer>();
if (hitRenderer == null)
{
return;
}
// 获取或创建画布
currentTarget = hitRenderer;
if (!paintingCanvases.ContainsKey(hitRenderer))
{
PaintingCanvas newCanvas = new PaintingCanvas(
hitRenderer,
defaultTextureSize,
defaultTextureSize
);
paintingCanvases[hitRenderer] = newCanvas;
}
currentCanvas = paintingCanvases[hitRenderer];
// 计算UV坐标
Vector2 uv = hit.textureCoord;
// 保存状态
if (enableUndoRedo)
{
SaveUndoState();
}
// 设置绘制模式
paintMode = isErase ? PaintMode.Erase : PaintMode.Paint;
// 开始绘制
isPainting = true;
paintDistanceAccumulator = 0f;
// 执行第一次绘制
PaintAtUV(uv, isErase);
Debug.Log($"开始绘制在: {hitRenderer.name}, UV: {uv}");
}
}
/// <summary>
/// 继续绘制
/// </summary>
private void OnPaintContinue(bool isErase)
{
if (!isPainting || currentCanvas == null)
{
return;
}
// 射线检测
Ray ray = paintingCamera.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, Mathf.Infinity, paintableLayers))
{
Renderer hitRenderer = hit.collider.GetComponent<Renderer>();
if (hitRenderer != currentTarget)
{
// 切换到新的绘制目标
OnPaintEnd();
OnPaintStart(isErase);
return;
}
// 计算UV坐标
Vector2 uv = hit.textureCoord;
// 计算自上次绘制以来的距离
float distance = Vector2.Distance(uv, lastPaintUV) * currentCanvas.textureWidth;
paintDistanceAccumulator += distance;
// 根据笔刷间距决定是否绘制
float requiredDistance = brushConfig.brushSize * brushConfig.brushSpacing;
if (paintDistanceAccumulator >= requiredDistance || requiredDistance < 1f)
{
// 执行绘制
PaintAtUV(uv, isErase);
// 重置距离累加器
paintDistanceAccumulator = 0f;
}
}
}
/// <summary>
/// 结束绘制
/// </summary>
private void OnPaintEnd()
{
if (isPainting)
{
isPainting = false;
currentCanvas.ApplyTextureToMaterial();
Debug.Log("绘制结束");
}
}
/// <summary>
/// 在UV坐标处绘制
/// </summary>
private void PaintAtUV(Vector2 uv, bool isErase)
{
if (currentCanvas == null || currentCanvas.paintTexture == null)
{
return;
}
// 保存绘制状态
lastPaintUV = uv;
lastPaintTime = Time.time;
// 计算实际笔刷参数
float actualSize = brushConfig.brushSize;
float actualOpacity = brushConfig.brushOpacity;
if (brushConfig.enablePressure)
{
// 模拟压力感应
actualSize = Mathf.Lerp(
brushConfig.brushSize * brushConfig.minSizePressure,
brushConfig.brushSize,
paintPressure
);
actualOpacity = Mathf.Lerp(
brushConfig.brushOpacity * brushConfig.minOpacityPressure,
brushConfig.brushOpacity,
paintPressure
);
}
// 选择绘制方法
if (useComputeShader && paintComputeShader != null)
{
PaintWithComputeShader(uv, actualSize, actualOpacity, isErase);
}
else
{
PaintWithRenderTexture(uv, actualSize, actualOpacity, isErase);
}
// 更新当前纹理
currentCanvas.UpdateCurrentTexture();
}
/// <summary>
/// 使用计算着色器绘制
/// </summary>
private void PaintWithComputeShader(Vector2 uv, float size, float opacity, bool isErase)
{
// 设置计算着色器参数
int kernel = paintComputeShader.FindKernel("PaintKernel");
paintComputeShader.SetTexture(kernel, "PaintTexture", currentCanvas.paintTexture);
paintComputeShader.SetVector("PaintUV", uv);
paintComputeShader.SetFloat("PaintSize", size);
paintComputeShader.SetFloat("PaintHardness", brushConfig.brushHardness);
paintComputeShader.SetVector("PaintColor", isErase ? Color.clear : brushConfig.brushColor);
paintComputeShader.SetFloat("PaintOpacity", opacity);
paintComputeShader.SetInt("BrushType", (int)brushConfig.brushType);
paintComputeShader.SetInt("BlendMode", (int)brushConfig.blendMode);
// 计算线程组大小
int threadGroupsX = Mathf.CeilToInt(size / 8f);
int threadGroupsY = Mathf.CeilToInt(size / 8f);
// 执行计算着色器
paintComputeShader.Dispatch(kernel, threadGroupsX, threadGroupsY, 1);
}
/// <summary>
/// 使用渲染纹理绘制
/// </summary>
private void PaintWithRenderTexture(Vector2 uv, float size, float opacity, bool isErase)
{
// 创建临时渲染纹理
RenderTexture tempRT = RenderTexture.GetTemporary(
currentCanvas.textureWidth,
currentCanvas.textureHeight,
0,
RenderTextureFormat.ARGB32
);
// 设置绘制材质参数
paintMaterial.SetVector("_PaintUV", uv);
paintMaterial.SetFloat("_PaintSize", size);
paintMaterial.SetFloat("_PaintHardness", brushConfig.brushHardness);
paintMaterial.SetColor("_PaintColor", isErase ? Color.clear : brushConfig.brushColor);
paintMaterial.SetFloat("_PaintOpacity", opacity);
paintMaterial.SetInt("_BrushType", (int)brushConfig.brushType);
paintMaterial.SetInt("_BlendMode", (int)brushConfig.blendMode);
// 如果有笔刷纹理,设置它
if (brushConfig.brushTexture != null)
{
paintMaterial.SetTexture("_BrushTexture", brushConfig.brushTexture);
paintMaterial.SetFloat("_BrushRotation", brushConfig.brushRotation * Mathf.Deg2Rad);
}
// 绘制到临时纹理
Graphics.Blit(currentCanvas.paintTexture, tempRT, paintMaterial);
// 复制回绘制纹理
Graphics.Blit(tempRT, currentCanvas.paintTexture);
// 释放临时纹理
RenderTexture.ReleaseTemporary(tempRT);
}
/// <summary>
/// 保存撤销状态
/// </summary>
private void SaveUndoState()
{
if (!enableUndoRedo || currentCanvas == null || currentCanvas.currentTexture == null)
{
return;
}
// 创建纹理副本
Texture2D undoTexture = new Texture2D(
currentCanvas.textureWidth,
currentCanvas.textureHeight,
currentCanvas.textureFormat,
false
);
undoTexture.SetPixels(currentCanvas.currentTexture.GetPixels());
undoTexture.Apply();
// 保存到撤销栈
undoStack.Push(undoTexture);
// 限制栈大小
if (undoStack.Count > maxUndoSteps)
{
Texture2D oldest = undoStack.Pop();
UnityEngine.Object.Destroy(oldest);
}
// 清空重做栈
while (redoStack.Count > 0)
{
Texture2D redoTexture = redoStack.Pop();
UnityEngine.Object.Destroy(redoTexture);
}
}
/// <summary>
/// 执行撤销
/// </summary>
public void Undo()
{
if (!enableUndoRedo || undoStack.Count == 0 || currentCanvas == null)
{
return;
}
// 保存当前状态到重做栈
Texture2D redoTexture = new Texture2D(
currentCanvas.textureWidth,
currentCanvas.textureHeight,
currentCanvas.textureFormat,
false
);
redoTexture.SetPixels(currentCanvas.currentTexture.GetPixels());
redoTexture.Apply();
redoStack.Push(redoTexture);
// 恢复上一个状态
Texture2D undoTexture = undoStack.Pop();
// 应用到绘制纹理
Graphics.Blit(undoTexture, currentCanvas.paintTexture);
// 更新当前纹理
currentCanvas.UpdateCurrentTexture();
currentCanvas.ApplyTextureToMaterial();
Debug.Log($"撤销完成,剩余撤销步骤: {undoStack.Count}");
}
/// <summary>
/// 执行重做
/// </summary>
public void Redo()
{
if (!enableUndoRedo || redoStack.Count == 0 || currentCanvas == null)
{
return;
}
// 保存当前状态到撤销栈
Texture2D undoTexture = new Texture2D(
currentCanvas.textureWidth,
currentCanvas.textureHeight,
currentCanvas.textureFormat,
false
);
undoTexture.SetPixels(currentCanvas.currentTexture.GetPixels());
undoTexture.Apply();
undoStack.Push(undoTexture);
// 恢复重做状态
Texture2D redoTexture = redoStack.Pop();
// 应用到绘制纹理
Graphics.Blit(redoTexture, currentCanvas.paintTexture);
// 更新当前纹理
currentCanvas.UpdateCurrentTexture();
currentCanvas.ApplyTextureToMaterial();
Debug.Log($"重做完成,剩余重做步骤: {redoStack.Count}");
}
/// <summary>
/// 清除当前画布
/// </summary>
public void ClearCurrentCanvas()
{
if (currentCanvas == null)
{
return;
}
if (enableUndoRedo)
{
SaveUndoState();
}
// 清空绘制纹理
RenderTexture activeRT = RenderTexture.active;
RenderTexture.active = currentCanvas.paintTexture;
GL.Clear(false, true, Color.clear);
RenderTexture.active = activeRT;
// 如果有原始纹理,恢复它
if (currentCanvas.originalTexture != null)
{
Graphics.Blit(currentCanvas.originalTexture, currentCanvas.paintTexture);
}
// 更新并应用
currentCanvas.UpdateCurrentTexture();
currentCanvas.ApplyTextureToMaterial();
Debug.Log("画布已清除");
}
/// <summary>
/// 保存纹理到文件
/// </summary>
public void SaveTextureToFile(string filePath)
{
if (currentCanvas == null || currentCanvas.currentTexture == null)
{
Debug.LogError("没有活动的画布可以保存");
return;
}
// 确保文件路径正确
if (!filePath.EndsWith(".png"))
{
filePath += ".png";
}
// 保存为PNG
byte[] pngData = currentCanvas.currentTexture.EncodeToPNG();
System.IO.File.WriteAllBytes(filePath, pngData);
Debug.Log($"纹理已保存到: {filePath}");
}
/// <summary>
/// 加载纹理到当前画布
/// </summary>
public void LoadTextureToCanvas(string filePath)
{
if (currentCanvas == null)
{
Debug.LogError("没有活动的画布可以加载");
return;
}
if (!System.IO.File.Exists(filePath))
{
Debug.LogError($"文件不存在: {filePath}");
return;
}
// 保存撤销状态
if (enableUndoRedo)
{
SaveUndoState();
}
// 加载纹理
byte[] fileData = System.IO.File.ReadAllBytes(filePath);
Texture2D loadedTexture = new Texture2D(2, 2);
loadedTexture.LoadImage(fileData);
// 调整尺寸以匹配画布
if (loadedTexture.width != currentCanvas.textureWidth ||
loadedTexture.height != currentCanvas.textureHeight)
{
Texture2D resizedTexture = ResizeTexture(loadedTexture,
currentCanvas.textureWidth,
currentCanvas.textureHeight);
UnityEngine.Object.Destroy(loadedTexture);
loadedTexture = resizedTexture;
}
// 应用到绘制纹理
Graphics.Blit(loadedTexture, currentCanvas.paintTexture);
// 更新当前纹理
currentCanvas.UpdateCurrentTexture();
currentCanvas.ApplyTextureToMaterial();
UnityEngine.Object.Destroy(loadedTexture);
Debug.Log($"纹理已从 {filePath} 加载");
}
/// <summary>
/// 调整纹理尺寸
/// </summary>
private Texture2D ResizeTexture(Texture2D source, int width, int height)
{
RenderTexture rt = RenderTexture.GetTemporary(width, height);
RenderTexture.active = rt;
Graphics.Blit(source, rt);
Texture2D result = new Texture2D(width, height);
result.ReadPixels(new Rect(0, 0, width, height), 0, 0);
result.Apply();
RenderTexture.active = null;
RenderTexture.ReleaseTemporary(rt);
return result;
}
/// <summary>
/// 绘制画布信息调试
/// </summary>
private void DebugDrawCanvasInfo()
{
Debug.Log("=== 三维绘图系统状态 ===");
Debug.Log($"活动画布: {(currentCanvas != null ? currentCanvas.targetRenderer.name : "无")}");
Debug.Log($"绘制状态: {isPainting}");
Debug.Log($"撤销栈大小: {undoStack.Count}");
Debug.Log($"重做栈大小: {redoStack.Count}");
Debug.Log($"笔刷大小: {brushConfig.brushSize}");
Debug.Log($"笔刷颜色: {brushConfig.brushColor}");
Debug.Log("========================");
}
/// <summary>
/// 启用时初始化
/// </summary>
private void OnEnable()
{
Initialize();
}
/// <summary>
/// 禁用时清理
/// </summary>
private void OnDisable()
{
CleanupResources();
}
/// <summary>
/// 清理资源
/// </summary>
private void CleanupResources()
{
// 清理所有画布
foreach (var canvas in paintingCanvases.Values)
{
canvas.Cleanup();
}
paintingCanvases.Clear();
// 清理材质
if (paintMaterial != null)
{
UnityEngine.Object.Destroy(paintMaterial);
paintMaterial = null;
}
// 清理撤销/重做栈
while (undoStack.Count > 0)
{
Texture2D texture = undoStack.Pop();
UnityEngine.Object.Destroy(texture);
}
while (redoStack.Count > 0)
{
Texture2D texture = redoStack.Pop();
UnityEngine.Object.Destroy(texture);
}
}
}
}
三维绘图着色器实现
以下是实现三维绘图的核心着色器代码:
Shader "Hidden/ThreeDPaint"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_PaintUV ("Paint UV", Vector) = (0, 0, 0, 0)
_PaintSize ("Paint Size", Float) = 10
_PaintHardness ("Paint Hardness", Float) = 0.5
_PaintColor ("Paint Color", Color) = (1, 0, 0, 1)
_PaintOpacity ("Paint Opacity", Float) = 1
_BrushType ("Brush Type", Int) = 0
_BlendMode ("Blend Mode", Int) = 0
_BrushTexture ("Brush Texture", 2D) = "white" {}
_BrushRotation ("Brush Rotation", Float) = 0
}
SubShader
{
Tags { "RenderType"="Opaque" }
Cull Off
ZWrite Off
ZTest Always
Pass
{
Name "PaintPass"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 3.0
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _MainTex_TexelSize;
float2 _PaintUV;
float _PaintSize;
float _PaintHardness;
float4 _PaintColor;
float _PaintOpacity;
int _BrushType;
int _BlendMode;
sampler2D _BrushTexture;
float _BrushRotation;
v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
// 圆形笔刷
float CircleBrush(float2 uv, float2 center, float radius, float hardness)
{
float distance = length(uv - center);
float normalizedDistance = distance / radius;
// 硬边
if (hardness >= 0.99)
{
return step(normalizedDistance, 1.0);
}
// 软边
float softEdge = 1.0 - hardness;
return smoothstep(1.0, 1.0 - softEdge, 1.0 - normalizedDistance);
}
// 方形笔刷
float SquareBrush(float2 uv, float2 center, float size, float hardness)
{
float2 diff = abs(uv - center) * 2.0 / size;
float distance = max(diff.x, diff.y);
if (hardness >= 0.99)
{
return step(distance, 1.0);
}
float softEdge = 1.0 - hardness;
return smoothstep(1.0, 1.0 - softEdge, 1.0 - distance);
}
// 纹理笔刷
float TextureBrush(float2 uv, float2 center, float size, sampler2D brushTex, float rotation)
{
// 计算相对于中心的UV
float2 localUV = (uv - center) * 2.0 / size;
// 应用旋转
float sinRot = sin(rotation);
float cosRot = cos(rotation);
float2 rotatedUV;
rotatedUV.x = localUV.x * cosRot - localUV.y * sinRot;
rotatedUV.y = localUV.x * sinRot + localUV.y * cosRot;
// 转换到纹理空间
float2 texUV = rotatedUV * 0.5 + 0.5;
// 检查是否在纹理范围内
if (texUV.x < 0.0 || texUV.x > 1.0 || texUV.y < 0.0 || texUV.y > 1.0)
{
return 0.0;
}
// 采样纹理
float4 texColor = tex2D(brushTex, texUV);
return texColor.a;
}
// 混合函数
float4 ApplyBlendMode(float4 source, float4 paint, float opacity, int mode)
{
float4 result = source;
switch (mode)
{
case 0: // Replace
result = lerp(source, paint, opacity);
break;
case 1: // Add
result = source + paint * opacity;
result.a = source.a;
break;
case 2: // Multiply
result = source * lerp(float4(1,1,1,1), paint, opacity);
break;
case 3: // Overlay
if (source.r < 0.5)
result.r = 2.0 * source.r * lerp(1.0, paint.r, opacity);
else
result.r = 1.0 - 2.0 * (1.0 - source.r) * (1.0 - lerp(1.0, paint.r, opacity));
if (source.g < 0.5)
result.g = 2.0 * source.g * lerp(1.0, paint.g, opacity);
else
result.g = 1.0 - 2.0 * (1.0 - source.g) * (1.0 - lerp(1.0, paint.g, opacity));
if (source.b < 0.5)
result.b = 2.0 * source.b * lerp(1.0, paint.b, opacity);
else
result.b = 1.0 - 2.0 * (1.0 - source.b) * (1.0 - lerp(1.0, paint.b, opacity));
break;
case 4: // AlphaBlend (默认)
float alpha = paint.a * opacity;
result.rgb = source.rgb * (1.0 - alpha) + paint.rgb * alpha;
result.a = source.a;
break;
}
return result;
}
fixed4 frag(v2f i) : SV_Target
{
// 采样原始纹理
float4 originalColor = tex2D(_MainTex, i.uv);
// 计算到绘制点的距离(纹理空间)
float2 pixelPos = i.uv * _MainTex_TexelSize.zw;
float2 paintPixelPos = _PaintUV * _MainTex_TexelSize.zw;
float pixelDistance = length(pixelPos - paintPixelPos);
// 转换为UV空间距离
float uvDistance = pixelDistance * _MainTex_TexelSize.x;
float brushRadius = _PaintSize * _MainTex_TexelSize.x;
// 如果像素在笔刷范围内
if (uvDistance <= brushRadius)
{
// 计算笔刷强度
float brushStrength = 0.0;
switch (_BrushType)
{
case 0: // 圆形
brushStrength = CircleBrush(i.uv, _PaintUV, brushRadius, _PaintHardness);
break;
case 1: // 方形
brushStrength = SquareBrush(i.uv, _PaintUV, _PaintSize * _MainTex_TexelSize.x * 2.0, _PaintHardness);
break;
case 2: // 纹理
brushStrength = TextureBrush(i.uv, _PaintUV, _PaintSize * _MainTex_TexelSize.x * 2.0, _BrushTexture, _BrushRotation);
break;
case 3: // 橡皮擦
brushStrength = CircleBrush(i.uv, _PaintUV, brushRadius, _PaintHardness);
// 橡皮擦使用透明颜色
_PaintColor = float4(0, 0, 0, 0);
break;
default: // 默认圆形
brushStrength = CircleBrush(i.uv, _PaintUV, brushRadius, _PaintHardness);
break;
}
// 应用笔刷强度
float4 paintColor = _PaintColor;
paintColor.a *= brushStrength * _PaintOpacity;
// 应用混合模式
return ApplyBlendMode(originalColor, paintColor, paintColor.a, _BlendMode);
}
// 不在笔刷范围内,返回原始颜色
return originalColor;
}
ENDCG
}
}
Fallback Off
}
计算着色器实现(可选)
对于更高效的绘制操作,可以使用计算着色器:
// PaintCompute.compute
#pragma kernel PaintKernel
RWTexture2D<float4> PaintTexture;
float2 PaintUV;
float PaintSize;
float PaintHardness;
float4 PaintColor;
float PaintOpacity;
int BrushType;
int BlendMode;
// 与片段着色器相同的辅助函数
float CircleBrush(float2 uv, float2 center, float radius, float hardness)
{
float distance = length(uv - center);
float normalizedDistance = distance / radius;
if (hardness >= 0.99)
{
return step(normalizedDistance, 1.0);
}
float softEdge = 1.0 - hardness;
return smoothstep(1.0, 1.0 - softEdge, 1.0 - normalizedDistance);
}
float4 ApplyBlendMode(float4 source, float4 paint, float opacity, int mode)
{
float4 result = source;
switch (mode)
{
case 0: // Replace
result = lerp(source, paint, opacity);
break;
case 1: // Add
result = source + paint * opacity;
result.a = source.a;
break;
case 2: // Multiply
result = source * lerp(float4(1,1,1,1), paint, opacity);
break;
case 3: // Overlay
if (source.r < 0.5)
result.r = 2.0 * source.r * lerp(1.0, paint.r, opacity);
else
result.r = 1.0 - 2.0 * (1.0 - source.r) * (1.0 - lerp(1.0, paint.r, opacity));
if (source.g < 0.5)
result.g = 2.0 * source.g * lerp(1.0, paint.g, opacity);
else
result.g = 1.0 - 2.0 * (1.0 - source.g) * (1.0 - lerp(1.0, paint.g, opacity));
if (source.b < 0.5)
result.b = 2.0 * source.b * lerp(1.0, paint.b, opacity);
else
result.b = 1.0 - 2.0 * (1.0 - source.b) * (1.0 - lerp(1.0, paint.b, opacity));
break;
case 4: // AlphaBlend
float alpha = paint.a * opacity;
result.rgb = source.rgb * (1.0 - alpha) + paint.rgb * alpha;
result.a = source.a;
break;
}
return result;
}
[numthreads(8,8,1)]
void PaintKernel(uint3 id : SV_DispatchThreadID)
{
// 获取纹理尺寸
uint width, height;
PaintTexture.GetDimensions(width, height);
// 计算当前像素的UV坐标
float2 uv = float2(id.x / (float)width, id.y / (float)height);
// 计算到绘制点的距离
float distance = length(uv - PaintUV);
float brushRadius = PaintSize / width;
// 如果像素在笔刷范围内
if (distance <= brushRadius)
{
// 采样当前颜色
float4 currentColor = PaintTexture[id.xy];
// 计算笔刷强度
float brushStrength = CircleBrush(uv, PaintUV, brushRadius, PaintHardness);
// 应用笔刷
float4 paintColor = PaintColor;
paintColor.a *= brushStrength * PaintOpacity;
// 应用混合模式
float4 newColor = ApplyBlendMode(currentColor, paintColor, paintColor.a, BlendMode);
// 写入新颜色
PaintTexture[id.xy] = newColor;
}
}
7.8.3 系统配置与应用指南
系统配置流程
在Unity中配置三维绘图系统需要遵循一系列步骤以确保正确的绘制效果和性能:
-
环境准备:确保项目使用兼容的渲染管线。系统支持内置渲染管线和URP,但需要相应的着色器适配。确认所有需要绘制的对象有正确的UV映射和材质设置。
-
系统初始化:创建主摄像机并添加ThreeDPaintingController组件。系统会自动配置所需的着色器和计算资源。确保摄像机的位置和角度适合绘制操作。
-
材质兼容性检查:检查场景中需要绘制的对象的材质。系统会为每个绘制目标创建纹理副本,因此需要确保原始材质支持纹理替换。对于复杂材质,可能需要额外的配置。
-
笔刷配置:根据绘制需求配置笔刷参数。圆形笔刷适合基本绘制,纹理笔刷可以创建特殊效果。调整笔刷大小、硬度和不透明度以获得理想的绘制效果。
-
图层配置:设置paintableLayers以指定哪些层级的对象可以绘制。通常包括角色、道具、环境等需要绘制的对象,排除UI、特效等不需要绘制的对象。
-
性能优化:根据目标平台调整性能设置。启用计算着色器可以显著提高绘制性能,但需要硬件支持。调整纹理尺寸平衡质量和内存使用。启用撤销/重做功能会增加内存使用。
示例场景配置
以下是一个完整的场景配置示例,演示如何设置三维绘图系统:
using UnityEngine;
namespace ThreeDPaintingSystem
{
/// <summary>
/// 三维绘图场景配置器
/// </summary>
public class ThreeDPaintSceneConfigurator : MonoBehaviour
{
[Header("场景对象")]
public GameObject[] paintableObjects;
public Camera paintingCamera;
public Light sceneLight;
[Header("默认笔刷")]
public BrushConfig defaultBrushConfig;
public Texture2D[] brushTextures;
[Header("UI引用")]
public UnityEngine.UI.Image brushPreview;
public UnityEngine.UI.Slider brushSizeSlider;
public UnityEngine.UI.Slider brushOpacitySlider;
public UnityEngine.UI.Dropdown brushTypeDropdown;
public UnityEngine.UI.Dropdown blendModeDropdown;
private ThreeDPaintingController paintingController;
private Color[] paletteColors = new Color[]
{
Color.red,
Color.green,
Color.blue,
Color.yellow,
Color.magenta,
Color.cyan,
Color.white,
Color.black
};
private void Start()
{
ConfigurePaintingScene();
}
/// <summary>
/// 配置绘图场景
/// </summary>
public void ConfigurePaintingScene()
{
// 创建或获取绘图控制器
if (paintingCamera == null)
{
paintingCamera = Camera.main;
}
paintingController = paintingCamera.GetComponent<ThreeDPaintingController>();
if (paintingController == null)
{
paintingController = paintingCamera.AddComponent<ThreeDPaintingController>();
}
// 配置控制器
paintingController.brushConfig = defaultBrushConfig != null ?
defaultBrushConfig : new BrushConfig();
paintingController.defaultTextureSize = 1024;
paintingController.paintableLayers = LayerMask.GetMask("Default", "Paintable");
// 配置场景对象
ConfigurePaintableObjects();
// 配置UI
ConfigureUI();
// 配置光照
if (sceneLight != null)
{
ConfigureLightingForPainting();
}
Debug.Log("三维绘图场景配置完成");
}
/// <summary>
/// 配置可绘制对象
/// </summary>
private void ConfigurePaintableObjects()
{
foreach (GameObject obj in paintableObjects)
{
if (obj == null)
{
continue;
}
// 确保对象有渲染器
Renderer renderer = obj.GetComponent<Renderer>();
if (renderer == null)
{
renderer = obj.AddComponent<MeshRenderer>();
}
// 确保对象有碰撞器
Collider collider = obj.GetComponent<Collider>();
if (collider == null)
{
MeshCollider meshCollider = obj.AddComponent<MeshCollider>();
meshCollider.convex = true;
}
// 设置图层
obj.layer = LayerMask.NameToLayer("Paintable");
// 确保材质支持纹理替换
Material material = renderer.sharedMaterial;
if (material != null && !material.HasProperty("_MainTex"))
{
// 替换为支持纹理的标准材质
Material newMaterial = new Material(Shader.Find("Standard"));
renderer.sharedMaterial = newMaterial;
}
Debug.Log($"可绘制对象已配置: {obj.name}");
}
}
/// <summary>
/// 配置UI
/// </summary>
private void ConfigureUI()
{
if (brushPreview != null)
{
UpdateBrushPreview();
}
if (brushSizeSlider != null)
{
brushSizeSlider.value = paintingController.brushConfig.brushSize;
brushSizeSlider.onValueChanged.AddListener(OnBrushSizeChanged);
}
if (brushOpacitySlider != null)
{
brushOpacitySlider.value = paintingController.brushConfig.brushOpacity;
brushOpacitySlider.onValueChanged.AddListener(OnBrushOpacityChanged);
}
if (brushTypeDropdown != null)
{
brushTypeDropdown.ClearOptions();
brushTypeDropdown.AddOptions(new System.Collections.Generic.List<string>
{
"圆形",
"方形",
"纹理",
"橡皮擦"
});
brushTypeDropdown.value = (int)paintingController.brushConfig.brushType;
brushTypeDropdown.onValueChanged.AddListener(OnBrushTypeChanged);
}
if (blendModeDropdown != null)
{
blendModeDropdown.ClearOptions();
blendModeDropdown.AddOptions(new System.Collections.Generic.List<string>
{
"替换",
"叠加",
"相乘",
"覆盖",
"Alpha混合"
});
blendModeDropdown.value = (int)paintingController.brushConfig.blendMode;
blendModeDropdown.onValueChanged.AddListener(OnBlendModeChanged);
}
}
/// <summary>
/// 为绘图配置光照
/// </summary>
private void ConfigureLightingForPainting()
{
// 使用柔和的光照以更好地显示颜色
sceneLight.type = LightType.Directional;
sceneLight.color = Color.white;
sceneLight.intensity = 1.2f;
sceneLight.shadows = LightShadows.Soft;
sceneLight.shadowStrength = 0.6f;
// 调整环境光
RenderSettings.ambientMode = UnityEngine.Rendering.AmbientMode.Trilight;
RenderSettings.ambientSkyColor = new Color(0.5f, 0.5f, 0.6f);
RenderSettings.ambientEquatorColor = new Color(0.4f, 0.4f, 0.4f);
RenderSettings.ambientGroundColor = new Color(0.3f, 0.3f, 0.3f);
Debug.Log("绘图光照已配置");
}
/// <summary>
/// 更新笔刷预览
/// </summary>
private void UpdateBrushPreview()
{
if (brushPreview == null)
{
return;
}
// 创建笔刷预览纹理
int previewSize = 128;
Texture2D previewTexture = new Texture2D(previewSize, previewSize);
Color bgColor = new Color(0.2f, 0.2f, 0.2f, 1f);
Color brushColor = paintingController.brushConfig.brushColor;
// 绘制预览
for (int y = 0; y < previewSize; y++)
{
for (int x = 0; x < previewSize; x++)
{
float u = x / (float)previewSize;
float v = y / (float)previewSize;
float distance = Vector2.Distance(new Vector2(u, v), new Vector2(0.5f, 0.5f));
float brushRadius = 0.4f;
if (distance <= brushRadius)
{
float hardness = paintingController.brushConfig.brushHardness;
float normalizedDistance = distance / brushRadius;
float alpha = 1.0 - smoothstep(1.0 - hardness, 1.0, normalizedDistance);
previewTexture.SetPixel(x, y, new Color(
brushColor.r,
brushColor.g,
brushColor.b,
alpha * paintingController.brushConfig.brushOpacity
));
}
else
{
previewTexture.SetPixel(x, y, bgColor);
}
}
}
previewTexture.Apply();
brushPreview.sprite = Sprite.Create(
previewTexture,
new Rect(0, 0, previewSize, previewSize),
new Vector2(0.5f, 0.5f)
);
}
/// <summary>
/// 选择颜色
/// </summary>
public void SelectColor(int colorIndex)
{
if (colorIndex >= 0 && colorIndex < paletteColors.Length)
{
paintingController.brushConfig.brushColor = paletteColors[colorIndex];
UpdateBrushPreview();
Debug.Log($"已选择颜色: {paletteColors[colorIndex]}");
}
}
/// <summary>
/// 设置自定义颜色
/// </summary>
public void SetCustomColor(Color color)
{
paintingController.brushConfig.brushColor = color;
UpdateBrushPreview();
}
/// <summary>
/// 笔刷大小改变
/// </summary>
private void OnBrushSizeChanged(float value)
{
paintingController.brushConfig.brushSize = value;
Debug.Log($"笔刷大小: {value}");
}
/// <summary>
/// 笔刷不透明度改变
/// </summary>
private void OnBrushOpacityChanged(float value)
{
paintingController.brushConfig.brushOpacity = value;
UpdateBrushPreview();
Debug.Log($"笔刷不透明度: {value}");
}
/// <summary>
/// 笔刷类型改变
/// </summary>
private void OnBrushTypeChanged(int index)
{
paintingController.brushConfig.brushType = (BrushType)index;
// 如果选择纹理笔刷,设置默认纹理
if (index == 2 && brushTextures != null && brushTextures.Length > 0)
{
paintingController.brushConfig.brushTexture = brushTextures[0];
}
UpdateBrushPreview();
Debug.Log($"笔刷类型: {(BrushType)index}");
}
/// <summary>
/// 混合模式改变
/// </summary>
private void OnBlendModeChanged(int index)
{
paintingController.brushConfig.blendMode = (BlendMode)index;
Debug.Log($"混合模式: {(BlendMode)index}");
}
/// <summary>
/// 清除当前画布
/// </summary>
public void ClearCanvas()
{
paintingController.ClearCurrentCanvas();
}
/// <summary>
/// 保存绘图
/// </summary>
public void SavePainting()
{
string fileName = $"Painting_{System.DateTime.Now:yyyyMMdd_HHmmss}";
string filePath = System.IO.Path.Combine(Application.dataPath, "SavedPaintings", fileName);
paintingController.SaveTextureToFile(filePath);
}
/// <summary>
/// 撤销
/// </summary>
public void Undo()
{
paintingController.Undo();
}
/// <summary>
/// 重做
/// </summary>
public void Redo()
{
paintingController.Redo();
}
/// <summary>
/// 切换绘制/擦除模式
/// </summary>
public void TogglePaintErase()
{
paintingController.paintMode = paintingController.paintMode == PaintMode.Paint ?
PaintMode.Erase : PaintMode.Paint;
Debug.Log($"当前模式: {paintingController.paintMode}");
}
}
}
通过以上完整的三维绘图系统实现,开发者可以在Unity中创建出功能丰富的三维模型绘制工具。系统提供了从基础绘制到高级功能(如撤销/重做、多种笔刷、混合模式等)的完整解决方案,支持实时绘制和高质量的视觉效果。三维绘图技术不仅为游戏开发提供了新的交互方式,还在教育、设计和艺术创作等领域有着广泛的应用前景。
更多推荐

所有评论(0)