【Unity】利用三角函数曲线模拟2D水面波动效果(RwaImage像素点波动)

本文介绍如何在Unity中使用正弦曲线实现水波纹效果。通过调整振幅、角速度、初相和偏距参数,动态改变RawImage的像素颜色,形成波动的视觉效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这里用的是正弦曲线:y=Asin(ωx+φ)+k

  • A——振幅,当物体作轨迹符合正弦曲线的直线往复运动时,其值为行程的1/2
  • (ωx+φ)——相位,反映变量y所处的状态
  • φ——初相,x=0时的相位;反映在坐标系上则为图像的左右移动
  • k——偏距,反映在坐标系上则为图像的上移或下移
  • ω——角速度, 控制正弦周期(单位弧度内震动的次数)

如图,利用正弦曲线将RawImage且分为上下两部分, 去掉上半部分像素点颜色,形成波纹形状,动态横向移动正弦曲线,达到波纹波动效果。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(RawImage))]
public class RawImageWaterWave : MonoBehaviour
{
    //初始波浪高度(偏距)
    [SerializeField, Range(0, 1)] float heightPercent = 0.5f;
    //波浪高度(值越接近于0,波浪越矮)(振幅)
    [SerializeField] float waveHeightParame = 0.1f;
    //波浪宽度(值越接近于0,波浪越宽)(角速度)
    [SerializeField] float waveWidthParame = 7;
    //波动速度(初相)
    [SerializeField] float speed = 1;
    //默认颜色
    [SerializeField] Color32 defaultColor;

    //RawImage组件
    RawImage waveImage;
    //待显示图片
    Texture2D showTexture;
    //像素点
    Color32[] pixelsBaseArr;
    Color32[] pixelsDrawArr;

    void Start()
    {
        //获取组件
        waveImage = GetComponent<RawImage>();
        //备份贴图
        showTexture = waveImage.texture as Texture2D;
        showTexture = Instantiate(showTexture);
        waveImage.texture = showTexture;
        //初始像素点
        pixelsBaseArr = showTexture.GetPixels32();
        //变化像素点
        pixelsDrawArr = new Color32[pixelsBaseArr.Length];
    }

    void Update()
    {
        Wave();
    }

    public void Wave()
    {
        //拷贝初始像素点
        System.Array.Copy(pixelsBaseArr, pixelsDrawArr, pixelsBaseArr.Length);
        //遍历所有Texture像素点
        for (int x = 0; x < showTexture.width; x++)
        {
            //偏移系数
            float tmpOffectParame = waveHeightParame * Mathf.Sin(waveWidthParame * x / showTexture.width + Time.time * speed);
            //限制极值
            float tmpHeightParame = Mathf.Clamp(heightPercent + tmpOffectParame, 0, 1);
            //计算高度
            int tmpHeight = (int)(tmpHeightParame * showTexture.height);
            //高度大于指定高度的像素点,显示默认色值
            for (int y = tmpHeight; y < showTexture.height; y++)
            {
                //将showTexture的width与height转化为pixelsDrawArr像素点数组的下标
                pixelsDrawArr[y * showTexture.width + x] = defaultColor;
            }
        }
        //Texture赋值
        showTexture.SetPixels32(pixelsDrawArr);
        showTexture.Apply();
    }

}

 

1. 设置图片为可读写,

2. 如果有报错"SetPixels32 can only be called on a RGBA32 or BGRA32 texture but is being called on GraphicsFormat RGBA Compressed ETC2 UNorm",按照报错提示,修改图片格式即可。

 

 

<think>我们正在使用Unity2D,结合OpenCV和Dlib实现人脸识别,并模拟面部消瘦、眼窝凹陷和黑眼圈效果。以下是步骤和关键点: 1. **环境准备**: - 在Unity中导入OpenCV for Unity插件(如OpenCV for Unity Asset Store包)和Dlib的Unity封装(如DlibFaceLandmarkDetector)。 - 确保插件兼容Unity2D项目。注意:虽然我们做2D效果,但人脸识别和特征点检测是处理来自摄像头的图像(可以视为2D纹理)。 2. **人脸检测与特征点提取**: - 使用OpenCV进行人脸检测(返回人脸矩形区域)。 - 然后,在检测到的人脸区域内使用Dlib的68点人脸特征点检测模型(shape_predictor_68_face_landmarks.dat)获取面部特征点。 3. **实现面部消瘦效果**: - 思路:通过移动特征点来模拟面部消瘦。例如,将脸颊两侧的特征点向内收缩。 - 具体操作:选择脸颊两侧的特征点(例如,索引为1到16的点,但具体要看68点模型的分布,通常两侧是17-26和22-26等,需要查阅68点模型对应位置),然后向脸部中心移动一定比例。 - 实现:将选中的特征点的x坐标向脸部中心线(例如,鼻梁的中心)靠近。可以使用插值方法,使得移动效果自然。 4. **眼窝凹陷效果**: - 思路:在眼睛周围的特征点(通常是上眼皮)向下移动,同时可能配合颜色变化(阴影)来增强凹陷感。 - 具体操作:选择上眼皮的特征点(例如,左眼为37-42,右眼为43-48,注意索引可能因模型而异),将这些点向下(y坐标增加)移动一小段距离。 - 同时,在移动后的区域绘制阴影(颜色加深)来模拟凹陷的阴影效果。 5. **黑眼圈效果**: - 思路:在眼睛下方区域绘制深色(如青黑色)的椭圆形区域,并做模糊处理使其自然。 - 具体操作:根据下眼睑的特征点(例如,左眼下方的点41-42和47-48之间的区域,右眼类似)确定一个区域,然后在此区域绘制深色。可以使用OpenCV的绘图函数(如`cv2.fillPoly`)填充颜色,然后使用高斯模糊使边缘过渡自然。 6. **整体流程**: - 从摄像头获取图像(或使用一张图片)。 - 使用OpenCV检测人脸区域。 - 使用Dlib在检测到的人脸区域检测68个特征点。 - 根据需求修改特征点位置(消瘦、眼窝凹陷)并在图像上绘制黑眼圈效果。 - 将处理后的图像显示在Unity的UI上(如RawImage)。 7. **注意事项**: - 性能:实时处理可能对性能有要求,需要优化(例如降低图像分辨率,但不要过低以免影响特征点检测)。 - 效果自然:特征点的移动需要根据人脸的实际比例进行,避免过度变形。可以使用仿射变换或三角剖分(Delaunay Triangulation)来保证变形时皮肤的纹理自然。 8. **代码结构示例**(伪代码): ```csharp using OpenCVForUnity.CoreModule; using OpenCVForUnity.ImgprocModule; using OpenCVForUnity.UnityUtils; using DlibFaceLandmarkDetector; // 假设有这样一个命名空间 public class FaceEffect : MonoBehaviour { public RawImage display; private WebCamTexture webCamTexture; private FaceLandmarkDetector landmarkDetector; private CascadeClassifier faceCascade; void Start() { // 初始化摄像头 webCamTexture = new WebCamTexture(); display.texture = webCamTexture; webCamTexture.Play(); // 加载OpenCV人脸检测器(haar级联分类器) faceCascade = new CascadeClassifier(Application.dataPath + "haarcascade_frontalface_default.xml"); // 加载Dlib特征点检测器 landmarkDetector = new FaceLandmarkDetector(Application.dataPath + "shape_predictor_68_face_landmarks.dat"); } void Update() { // 从WebCamTexture获取图像 Mat rgbaMat = new Mat(webCamTexture.height, webCamTexture.width, CvType.CV_8UC4); Utils.webCamTextureToMat(webCamTexture, rgbaMat); // 转换为灰度图进行人脸检测 Mat gray = new Mat(); Imgproc.cvtColor(rgbaMat, gray, Imgproc.COLOR_RGBA2GRAY); // 人脸检测 MatOfRect faces = new MatOfRect(); faceCascade.detectMultiScale(gray, faces); // 遍历检测到的人脸 foreach (Rect face in faces.toArray()) { // 使用Dlib检测特征点(注意:Dlib的检测需要将Rect转换为Dlib的矩形) List<Vector2> landmarks = landmarkDetector.Detect(rgbaMat, face); // 面部消瘦:移动脸颊两侧的点 ApplyThinFaceEffect(ref landmarks); // 眼窝凹陷:移动上眼皮的点 ApplySunkenEyesEffect(ref landmarks); // 绘制特征点(可选,用于调试) // DrawLandmarks(rgbaMat, landmarks); // 黑眼圈效果:在眼睛下方绘制深色区域 ApplyDarkCirclesEffect(rgbaMat, landmarks); } // 显示处理后的图像 Texture2D texture = new Texture2D(rgbaMat.cols(), rgbaMat.rows(), TextureFormat.RGBA32, false); Utils.matToTexture2D(rgbaMat, texture); display.texture = texture; } void ApplyThinFaceEffect(ref List<Vector2> landmarks) { // 假设脸颊两侧的点索引为1到16(具体需要根据68点模型调整) // 实际68点模型中,脸颊两侧的点大约是2,3,4,5,6,7,8,9,10,11,12,13,14,15(左脸)和17,18,19,20,21,22,23,24,25,26,27,28(右脸)?这里需要根据模型确定 // 这里仅作示例,我们需要知道具体的68点索引分布 // 例如,我们取左侧脸颊点(索引1到16中靠脸颊的部分)和右侧对称点 // 实际68点模型:左脸轮廓点:0-16(共17个点),右脸对称但模型没有直接给出,需要对称处理?实际上68点模型是连续的,从0(下巴开始)到16(左下巴角),然后17(左颧骨)等。 // 更准确:我们通常取左右两侧中间位置的点(比如点3和点13)向中心移动。 // 这里简化:取左右两侧的点(比如左侧点2,3,4,5,6,7,8和右侧的10,11,12,13,14,15,16)注意:68点模型索引从0开始,下巴0,左脸轮廓1-16(逆时针)?具体需要查。 // 根据68点模型:点0-16是下颌线(下巴到左太阳穴再到右太阳穴?),实际上0是下巴中点,1是下巴左,2是下巴右,然后3是左下颌角,4右下颌角,然后5-16是左右脸颊轮廓?其实不是,68点模型有标准分布图。 // 标准68点模型: // 0-16:下巴轮廓(17个点) // 17-21:左眉毛 // 22-26:右眉毛 // 27-35:鼻梁和鼻尖 // 36-41:左眼 // 42-47:右眼 // 48-67:嘴巴 // 因此,对于脸颊,我们通常使用下颌轮廓点(0-16)来调整脸型。 // 我们取整个下颌轮廓(0-16)的点,然后向中心移动(除了下巴中点0,因为移动下巴中点会导致下巴变形,我们可能不想动它) // 移动方式:将每个点的x坐标向脸部中心线(即点27,鼻尖上方,但鼻尖是33?)移动。中心线x坐标可以用点27(鼻梁上点)的x坐标。 // 获取中心线x坐标(这里用点30,即鼻尖下方点,但点27是鼻梁上点,索引27) float centerX = landmarks[27].x; // 收缩因子(0.9表示向中心移动10%) float factor = 0.9f; // 遍历下颌点(索引0到16) for (int i = 1; i <= 16; i++) // 跳过0(下巴中点)或者不跳过?这里我们跳过0,因为移动下巴中点会改变下巴位置 { // 只移动左右两侧,不移动正下方的点(0,8,16?) // 实际上点0是下巴中点,8是左脸颊最宽处?16是右脸颊最宽处?具体看分布。 // 我们移动除了0和8、16以外的点?或者全部移动。 // 计算当前点到中心线的距离 float distanceToCenter = landmarks[i].x - centerX; // 向中心移动(乘以一个因子,使得距离减小) float newX = centerX + distanceToCenter * factor; landmarks[i] = new Vector2(newX, landmarks[i].y); } } void ApplySunkenEyesEffect(ref List<Vector2> landmarks) { // 左眼上眼皮:点37,38,39,40,41,42(实际上36-41是左眼,其中上眼皮是37,38,39,40?) // 标准:36为左眼左角,37为上眼皮中左,38为上眼皮中,39为上眼皮中右,40为右眼右角(内眼角?),41为下眼皮中右,42为下眼皮中左?不对,标准68点: // 左眼:36,37,38,39,40,41(顺时针,36是左眼角,37是上眼皮中间偏左,38是上眼皮中间偏右,39是右眼角,40是下眼皮中间偏右,41是下眼皮中间偏左) // 我们想要上眼皮下沉,所以移动上眼皮的点(37,38)向下(y坐标增加) // 注意:移动的距离不能太大,否则不自然。 // 移动上眼皮的点(37,38)和(43,44)为右眼的上眼皮中间点(右眼对应42,43,44,45,46,47) // 左眼上眼皮中间点:37和38 // 右眼上眼皮中间点:43和44 float dropAmount = 5.0f; // 下移的像素数,根据图像大小调整 // 左眼上眼皮中间两个点(37,38) landmarks[37] = new Vector2(landmarks[37].x, landmarks[37].y + dropAmount); landmarks[38] = new Vector2(landmarks[38].x, landmarks[38].y + dropAmount); // 右眼上眼皮中间两个点(43,44) landmarks[43] = new Vector2(landmarks[43].x, landmarks[43].y + dropAmount); landmarks[44] = new Vector2(landmarks[44].x, landmarks[44].y + dropAmount); } void ApplyDarkCirclesEffect(Mat rgbaMat, List<Vector2> landmarks) { // 在眼睛下方绘制深色区域(例如,左眼下方:点41,42和40,41之间的区域,但实际我们可以在下眼皮下方画一个椭圆) // 左眼下方区域:我们可以用点41(左眼下眼皮中间偏左)和点40(左眼下眼皮中间偏右)以及点29(鼻梁点)和点31(鼻翼)等,但为了简单,我们使用下眼皮点以及向下扩展的区域。 // 绘制两个椭圆,一个在左眼下方,一个在右眼下方。 // 左眼下方椭圆的中心:在左眼下眼皮中间点(点41和点40的中点)下方一点 Point leftEyeBottomCenter = new Point( (landmarks[40].x + landmarks[41].x) / 2, (landmarks[40].y + landmarks[41].y) / 2 + 5 // 再往下5像素 ); // 椭圆大小:宽度为左眼宽度的一半,高度为3-5像素 Size leftEllipseSize = new Size((landmarks[39].x - landmarks[36].x) / 4, 3); // 右眼类似 Point rightEyeBottomCenter = new Point( (landmarks[46].x + landmarks[47].x) / 2, (landmarks[46].y + landmarks[47].y) / 2 + 5 ); Size rightEllipseSize = new Size((landmarks[45].x - landmarks[42].x) / 4, 3); // 绘制椭圆(使用填充) Scalar darkColor = new Scalar(0, 0, 0, 100); // RGBA,黑色带透明度 Imgproc.ellipse(rgbaMat, leftEyeBottomCenter, leftEllipseSize, 0, 0, 360, darkColor, -1); Imgproc.ellipse(rgbaMat, rightEyeBottomCenter, rightEllipseSize, 0, 0, 360, darkColor, -1); // 模糊处理,使边缘自然 Mat blurred = new Mat(); Imgproc.GaussianBlur(rgbaMat, blurred, new Size(15, 15), 0); blurred.copyTo(rgbaMat); } } ``` 注意:以上代码是概念性的,实际使用中需要根据68点模型的具体索引调整。另外,特征点移动和效果绘制需要调试以达到自然效果。 9. **性能优化**: - 可以降低图像分辨率来提高处理速度。 - 也可以考虑使用多线程处理,但Unity中需要注意线程安全。 10. **效果调整**: - 上述效果参数(如移动距离、椭圆大小、颜色等)需要根据实际测试调整。 11. **引用**: - 我们使用了OpenCV for Unity插件进行人脸检测和图像处理[^1]。 - 使用了Dlib的68点模型进行特征点检测,这是人脸识别中常用的方法[^2]。 - 关于人脸特征点的具体分布,可以参考Dlib的官方文档。 12. **注意事项**: - 隐私保护:处理人脸图像时,确保遵守隐私法规。 - 效果自然:移动特征点可能会造成不自然的效果,因此需要仔细调整参数。 以上是一个基本的实现方案,具体实现中可能会遇到各种问题,需要根据实际情况调整。 §§相关问题§§ 1. 如何在Unity中降低摄像头图像的分辨率以提高处理速度? 2. 如何优化Dlib特征点检测的性能? 3. 除了移动特征点,还有哪些方法可以实现更自然的面部变形效果?</think>### 在Unity2D中结合OpenCV和Dlib实现人脸识别与面部特效 以下是实现面部消瘦、眼窝凹陷及黑眼圈特效的完整方案,分为环境配置、核心步骤和代码实现三部分: --- #### **1. 环境准备** - **OpenCV for Unity**:处理基础人脸检测和图像处理 ```bash # 从Asset Store或GitCode获取 https://gitcode.com/Open-source-documentation-tutorial/cb266 ``` - **Dlib FaceLandmark Detector**:精准获取68个人脸特征点 ```bash # 从GitHub获取Unity封装版 https://github.com/keijiro/DlibFaceLandmarkDetector ``` - **系统要求**:Unity 2019.4+,支持Burst编译器以优化性能 --- #### **2. 核心实现步骤** ##### **(1) 人脸检测与特征点定位** ```csharp // 初始化检测器 FaceLandmarkDetector landmarkDetector = new FaceLandmarkDetector(shapePredictorPath); Mat rgbaMat = new Mat(webCamTexture.height, webCamTexture.width, CvType.CV_8UC4); Utils.webCamTextureToMat(webCamTexture, rgbaMat); // 检测人脸和特征点 List<Rect> faces = faceCascade.detectMultiScale(grayMat); foreach (Rect face in faces) { List<Vector2> landmarks = landmarkDetector.Detect(rgbaMat, face); // 68个关键点索引参考: // 下巴(0-16), 左眉(17-21), 右眉(22-26), 鼻梁(27-35) // 左眼(36-41), 右眼(42-47), 嘴唇(48-67) [^3] } ``` ##### **(2) 面部消瘦效果** ```csharp void ApplyThinFaceEffect(ref List<Vector2> landmarks) { // 收缩脸颊关键点(索引1-15为下颌轮廓) float shrinkFactor = 0.85f; Vector2 faceCenter = (landmarks[2] + landmarks[14]) / 2; // 左右颧骨中点 for (int i = 1; i <= 15; i++) { Vector2 dir = faceCenter - landmarks[i]; landmarks[i] = landmarks[i] + dir * (1 - shrinkFactor); } } ``` > 原理:将下颌轮廓点向脸部中心收缩,模拟骨骼突出效果[^1] ##### **(3) 眼窝凹陷效果** ```csharp void ApplySunkenEyes(Mat img, List<Vector2> landmarks) { // 获取左眼区域(索引36-41) Point[] leftEyePoints = landmarks.GetRange(36, 6).ToPoints(); // 创建凹陷遮罩 Mat mask = new Mat(img.rows(), img.cols(), CvType.CV_8UC1, Scalar.all(0)); Imgproc.fillConvexPoly(mask, leftEyePoints, new Scalar(255)); // 应用深色渐变 Mat darkenMat = new Mat(); Core.addWeighted(img, 0.7, Scalar.all(50), 0.3, 0, darkenMat); darkenMat.copyTo(img, mask); } ``` > 技巧:使用透明度混合加深眼窝颜色,配合高斯模糊实现自然过渡 ##### **(4) 黑眼圈特效** ```csharp void ApplyDarkCircles(Mat img, List<Vector2> landmarks) { // 在下眼睑下方绘制椭圆(左眼索引41-42区域) Point center = new Point( (landmarks[41].x + landmarks[40].x) / 2, (landmarks[41].y + landmarks[40].y) / 2 + 10 ); Size axes = new Size(15, 8); // 椭圆轴长 Imgproc.ellipse(img, center, axes, 0, 0, 360, new Scalar(60, 60, 120, 180), // 青黑色RGBA -1); // 填充模式 // 边缘模糊处理 Imgproc.GaussianBlur(img, img, new Size(15, 15), 0); } ``` --- #### **3. 性能优化建议** 1. **异步处理**:将OpenCV/Dlib计算放在`Thread`中,避免主线程阻塞 2. **分辨率控制**:将摄像头输入降至480p(SD)处理 3. **区域更新**:只重绘面部ROI区域而非全画面 4. **缓存机制**:对连续帧复用特征点检测结果 --- #### **4. 效果增强方案** | 效果 | 实现方法 | 关键API | |------|----------|---------| | **动态消瘦** | 根据时间轴调整收缩系数 | `Mathf.Lerp()` | | **泪沟加深** | 在鼻翼-眼袋区域添加深色线条 | `Imgproc.line()` | | **骨骼阴影** | 在颧骨下方绘制三角形阴影 | `Imgproc.fillPoly()` | > **注意事项**: > - 特征点移动幅度需限制在面部宽度的5-10%避免失真 > - 人眼检测可参考Dlib的眨眼检测算法防止特效误触发[^2] > - 隐私合规:运行时需明确提示用户开启摄像头权限 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

萧然CS

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值