unity 彙整 | Creative Coding TW - 互動程式創作台灣站 https://creativecoding.in/tag/unity/ 蒐集互動設計案例、教學與業界資源,幫助你一起進入互動程式創作的產業 Mon, 28 Nov 2022 00:18:30 +0000 zh-TW hourly 1 https://wordpress.org/?v=6.2.2 https://creativecoding.in/wp-content/uploads/2022/03/cropped-cct-logo-icon-2-32x32.png unity 彙整 | Creative Coding TW - 互動程式創作台灣站 https://creativecoding.in/tag/unity/ 32 32 【徵文賞-沉浸式內容】佳作|基於電腦圖學之及時渲染超音波檢測VR遊戲環境 – 林慶佳 https://creativecoding.in/2022/11/24/collection221011-immersive-1/ Thu, 24 Nov 2022 08:33:01 +0000 https://creativecoding.in/?p=3235 透過互動程式創作徵文賞,我們期望讓更多人認識並加入 Creative Coding 這個新奇有趣的領域。此文章為沈浸式內容組佳作,將介紹如何使用電腦圖學知識與工具,開發出如下的及時渲染超音波特效。

這篇文章 【徵文賞-沉浸式內容】佳作|基於電腦圖學之及時渲染超音波檢測VR遊戲環境 – 林慶佳 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
前言

鑒於APIL曾推出過類似的專案,藉由科技教育的方式讓使用者能用更少的成本進行訓練。本文將介紹如何使用電腦圖學知識與工具,開發出如下的及時渲染超音波特效。

APIL製作的模擬遊戲。

內容目錄

超音波原理
透視圖
邊緣高光
強度圖
Radical Blur
上層皮膚模擬
扇型裁切
內臟透視
Stencil masking
雙Pass覆寫深度
用戶操作
額外閱讀
參考文獻

本文章成品效果展示

建議先備知識

  1. Unity 基本操作概念。
  2. Shader lab 或 GLSL 、 HLSL 等渲染語言知識。
  3. 基本線性代數、三角函數知識。

超音波原理

由感測器發出聲波,返回的是該聲波垂直向下的頗面回聲(echo)圖,echo越強的像素越白。 因為器官表面不平,會產生音波漫射(diffuse),所以大部分是灰的。

  • 液體和空氣幾乎沒有回聲,所以是黑的。
  • 組織到另一個組織時,若材質差異大,會產生邊緣高光(specular)反射。
  • 漫射的光會造成雜訊、顆粒感(speckle)。
  • 聲波每經過一層會反射的物質,其強度減弱,產生陰影。
  • 離感測器最近的組織回聲最強,隨著滲透越深,強度衰弱(attenuation)。

透視圖

先準備正常內臟模型,寫個簡單的透明shader並用alpha控制透明度來代表透光程度,讓攝影機最終呈現如圖:

事前準備

可使用任意模型,例如本範例使用Unity Asset store上的Human Organ System PBR模型素材。

https://assetstore.unity.com/packages/3d/characters/humanoids/humans/human-organ-system-pbr-175755

使用Shader渲染模型切面

使用透明混合(blend)技術,可看到方塊內還包含著另一個圓與方塊。

圖源:[1]
Tags { 
            "RenderType" = "Transparent" 
            "Queue" = "Transparent"
            "IgnoreProjector" = "True" }
        LOD 100

        Pass
        {
            Blend SrcAlpha OneMinusSrcAlpha
            ZWrite Off
				}
				...
				fixed4 frag(v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                col.a = _Alpha;
                return _Alpha;
            }

混合Shader code

邊緣高光

藉由sample 該uv的顏色與上面一點的uv的顏色,相減後取得顏色變化差距。 (只取上面的顏色變化是因為我們假設聲波來源是上面)

先將頗面圖模糊後取得的線條比較清楚
v2f vert (appdata v)
            {
                v2f o;
                //假設音波來源是上中央
                float2 sourcePosition = float2(0.5, 1);
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                //上緣uv位置
                o.uvabove=v.uv.xy + (sourcePosition - v.uv.xy) * _MainTex_TexelSize.y *_EdgeSize;

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                fixed4 above_col=tex2D(_MainTex, i.uvabove);
                return col * abs(above_col-col)*10 ;
            }
            ENDCG

強度圖

將顏色由貼圖上方往下加,最後反轉後會得到如速度線的效果。

struct v2f {
    float4 pos : SV_POSITION;
    float2 uv : TEXCOORD0;
    float2 toSource : TEXCOORD1;
};

v2f vert(appdata_base v) {
    v2f output;
    // ...
    float2 sourcePosition = float2(0.5, 1);
    output.uv = v.texcoord.xy;
    output.toSource = sourcePosition - v.texcoord.xy;

    return output;
}

half4 frag(v2f input) : SV_Target {
    half4 output = half4(0, 0, 0, 1);
    float2 normalizedToSource = input.toSource / length(input.toSource);
    float2 texelToSource = float2(normalizedToSource * _MainTex_TexelSize.y);

    // _TexelSize.w is automatically assigned by Unity to the texture’s height
    for (int i = 0; i < _MainTex_TexelSize.w; i++) {
        output += tex2D(_MainTex, input.uv + texelToSource * i);
    }

    return output;
}

Radical Blur

原理:根據(x,y)找到對應的( r , θ) 座標,取鄰近像素座標 ( r , Θ) =(r , θ ± l / 2 ) 平均值輸出。

struct v2f
        {
            float4 pos : SV_POSITION;
            float2 uv : TEXCOORD0;
        };

        sampler2D _MainTex;
        float4 _MainTex_TexelSize;
        float4 _MainTex_ST;
        float _BlurSize;
        float2 _BlurCenter;
        float _Iteration
        v2f vert(appdata_img v)
        {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);
            o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
            return o;
        }

        float4 frag(v2f i) : SV_TARGET
        {
            float2 blurVector = (_BlurCenter.xy - i.uv.xy) * _BlurSize;
            float r = length((_BlurCenter.xy - i.uv.xy));
            float angle=acos((i.uv.x - _BlurCenter.x )/r);
            angle *=sign(i.uv.y - _BlurCenter.y);

            float4 acumulateColor = float4(0, 0, 0, 0);

            for (int it = -_Iteration; it <= _Iteration; it++)
            {
                float phi = angle + radians(it);
                float2 _arcuv = float2(_BlurCenter.x + r * cos(phi), _BlurCenter.y + r * sin(phi));

                acumulateColor += tex2D(_MainTex, _arcuv);
            }

            return acumulateColor / _Iteration;
        }

這邊使用acos取角度,其性質如下

上層皮膚模擬

超音波圖之所以上面通常都是白白一絲一絲的,因為那是人的表層皮膚、肌肉,容易產生echo,所以偏白。 文獻[1]教了我們基本的超音波效果模擬,為了讓圖更逼真,所以再來多做一些吧。

可以看到超音波上面的皮膚組織偏白。圖源:來自網路

使用tanh取得上層偏白的遮罩,再調整一下參數可得到以下曲線:

float whiteIntensity = 1 - tanh((1 - input.uv.y) * 9);
遮罩效果

搭配雜訊貼圖可得到以下效果:

再調整一下曲線讓他不要太白:

扇型裁切

我們需要的是右邊這種形狀的,需再加點角度範圍判斷。

參考文獻[3],畫出扇型遮罩。
使用兩個圓弧剪裁出扇型。
half4 frag(v2f i) : SV_Target
            {
                //Re-map this now rather than later
                float2 pos = - (i.uv * 2.0 - 1.0);

                //Calculate the angle of our current pixel around the circle
                float theta = degrees(atan2(pos.x, pos.y)) + 180.0;

                //Get circle and sector masks
                float circle = length(pos) <= 1.0 && length(pos) >=_innerDistance;
                float sector = (theta <= _EndAngle) && (theta >= _StartAngle);

                //Return the desired colour masked by the circle and sector
                return _Color * (circle * sector);
            }

結果:

內臟透視

為了讓使用者能知道內臟的位置,本專案提供透視選項,能讓指定內臟顯示於最上層。 這邊嘗試了(1)Stencil masking 與(2)雙Pass覆寫深度 兩種作法。

Stencil masking

利用Stencil 遮罩將特定部位的像素裁剪出來,並指定疊加顯示於畫面上。

來源文獻[4]

在人體等想被穿透的物體shader上加

Stencil
{
    Ref 1 // ReferenceValue = 1
    Comp NotEqual // Only render pixels whose reference value differs from the value in the buffer.
}

能穿透的物件shader加上

Stencil
{
    Ref 1 // ReferenceValue = 1
    Comp Always // Comparison Function - Make the stencil test always pass.
    Pass Replace // Write the reference value into the buffer.
}

缺點是只有PC能執行,由於Occulus是安卓系統,礙於該平台不支援,導致只會渲染在一顆眼睛上。

雙Pass覆寫深度

原理:

第一個pass無視深度條件寫入背面,同時也會寫下深度,讓深度能在它之上的只有自己的正面,因此讓第二個pass正常渲染即可。

Cull Front
ZTest Always
Shader "Unlit/OrganOutline"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" { }
        _Color ("outline color", Color) = (1, 0, 0, 1)
        _OutlineWidth ("Outline width", Range(0.0, 1.0)) = .005
    }
    SubShader
    {
        Tags { "RenderType" = "Opaque" }
        LOD 100

        pass
        {
            Cull Front
            ZTest Always
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            float4 _Color;
            float _OutlineWidth = 0.5;

            struct v2f
            {
                float4 pos : SV_POSITION;
            };

            v2f vert(appdata_base v)
            {
                v2f o;
                float3 norm = normalize(v.normal);
                v.vertex.xyz += v.normal * _OutlineWidth;
                o.pos = UnityObjectToClipPos(v.vertex);

                return o;
            }
            fixed4 frag(v2f i) : COLOR
            {
                return _Color;
            }
            ENDCG

        }

        Pass
        {
            Cull Back
            //...... render your texture
        }
    }
}

用戶操作

為了提升VR體驗,所以如影片中的感測器會自動貼合人體表面,防止穿模。

public LayerMask skinMeshLayer;

    [SerializeField]
    private Transform sensorPoint;

    [SerializeField]
    private float rayDistance = 10;

    private float stickLength;

    private void Start() {
        stickLength = (sensorPoint.position - transform.position).magnitude;
    }

    void Update()
    {
        Vector3 alignDir = (sensorPoint.position - transform.position).normalized;
        Vector3 _rayStart = transform.position -  alignDir*rayDistance ; //往後退保留ray空間

        RaycastHit hit;

        if (Physics.Raycast(_rayStart, alignDir, out hit, rayDistance * 2, skinMeshLayer))
        {
            Vector3 _stickPoint = hit.point;
            Vector3 _movePoint = _stickPoint - alignDir * stickLength;
            transform.position = _movePoint;
        }
        else {
            transform.localPosition = Vector3.zero;
        }

    }

總結

製作步驟整理如下:

額外閱讀

[5] 核磁共振是3D立體空間,而超聲波是建立在聲波網格(acoustic grid),渲染比較困難。

[6] 快速剖面演算法。 (因為超音波是取截面)

[7] 更專業的演算法。


參考文獻

  1. Ultrasound simulation with shaders – Avangarde-Software
  2. Barnouin, C., Zara, F., & Jaillet, F. (2020, February). A real-time ultrasound rendering with model-based tissue deformation for needle insertion. In 15th International Conference on Computer Graphics Theory and Applications, GRAPP 2020. [pdf]
  3. How to draw circular sector in RunTime? – Unity Answers
  4. unity – How can I create a “see behind walls” effect? – Game Development Stack Exchange
  5. Chapter 40. Applying Real-Time Shading to 3D Ultrasound Visualization | NVIDIA Developer
  6. https://www.scitepress.org/papers/2017/60972/60972.pdf
  7. GPU Ultrasound Simulation and Volume Reconstruction (tum.de)

這篇文章 【徵文賞-沉浸式內容】佳作|基於電腦圖學之及時渲染超音波檢測VR遊戲環境 – 林慶佳 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
【徵文賞-延展實境】佳作|史上最累Debug!深蹲才能過關(Unity + OpenCV 跨軟體傳輸實作) – 林慶佳 https://creativecoding.in/2022/11/24/collection221110-xr-1/ Thu, 24 Nov 2022 03:31:26 +0000 https://creativecoding.in/?p=3141 透過互動程式創作徵文賞,我們期望讓更多人認識並加入 Creative Coding 這個新奇有趣的領域。此作品為延展實境組佳作,以 Unity 結合 OpenCV 跨軟體實作,偵測深蹲姿勢到達一定水準後,讓遊戲中的物件跳起來。

這篇文章 【徵文賞-延展實境】佳作|史上最累Debug!深蹲才能過關(Unity + OpenCV 跨軟體傳輸實作) – 林慶佳 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
透過互動程式創作徵文賞,我們期望讓更多人認識並加入 Creative Coding 這個新奇有趣的領域。此作品為延展實境組佳作,以 Unity 結合 OpenCV 跨軟體實作,偵測深蹲姿勢到達一定水準後,讓遊戲中的物件跳起來。

實作成果:

偵測臉並回傳位置,使Unity中的物件可以跟隨他。

使用C++指標傳輸

原文:Unity and OpenCV – Part three: Passing detection data to Unity – Thomas Mountainborn

偵測臉並回傳位置,使Unity中的物件可以跟隨他。

建立與Unity溝通的結構

C++

struct Circle
{
	//建構子
	Circle(int x, int y, int radius) : X(x), Y(y), Radius(radius) {}
	int X, Y, Radius;
};

編譯器在產生dll檔案時會打亂method名稱,為讓方法保持原名稱,則外顯”C”

Normally, the C++ compiler will mangle the method names when packaging them into a .dll. Therefore, we instruct it to use the classic “C” style of signatures, which leaves the method names just as you wrote them.

extern "C" int __declspec(dllexport) __stdcall  Init(int& outCameraWidth, int& outCameraHeight)
extern "C" void __declspec(dllexport) __stdcall Detect(Circle* outFaces, int maxOutFacesCount, int& outDetectedFacesCount)

Circle* outFaces 表an array of Circles。

int& outDetectedFacesCount 表該變數是傳址(ref)。

C#

與C++溝通的格式,變數欄位、宣告順序必需與c++相同

// Define the structure to be sequential and with the correct byte size (3 ints = 4 bytes * 3 = 12 bytes)
[StructLayout(LayoutKind.Sequential, Size = 12)]
public struct CvCircle
{
    public int X, Y, Radius;
}

unsafe:讓你在C#能使用指標。

fixed:使編譯器讓該變數記憶體位置不被garbage collector處理掉。

在fixed區塊中,openCV會直接將變數寫入CvCircle結構陣列中,而省去copy的成本。

void Update()
    {     
				//接收陣列大小
        int detectedFaceCount = 0;
        unsafe
        {
            //pass fixed pointer
            fixed (CvCircle* outFaces = _faces)
            {
                OpenCVInterop.Detect(outFaces, _maxFaceDetectCount, ref detectedFaceCount);
            }
        }
}

fixed中只接受:

The legal initializers for a fixed statement are:

  • The address operator & applied to a variable reference.
  • An array
  • A string
  • A fixed-size buffer.

分段解析

C++

// Declare structure to be used to pass data from C++ to Mono. (用來與Unity溝通的結構)
struct Circle
{
	//建構子
	Circle(int x, int y, int radius) : X(x), Y(y), Radius(radius) {}
	int X, Y, Radius;
};

CascadeClassifier 是Opencv中做人臉檢測的時候的一個級聯分類器。 並且既可以使用Haar,也可以使用LBP特徵。()

C#

// Define the structure to be sequential and with the correct byte size 
//(3 ints = 4 bytes * 3 = 12 bytes)
[StructLayout(LayoutKind.Sequential, Size = 12)]
public struct CvCircle
{
    public int X, Y, Radius;
}

StructLayout :C#中StructLayout的特性 - IT閱讀 (itread01.com)

char型資料,對齊值為1,對於short型為2,對於int,float,double型別,其對齊值為4,單位位元組。

初始化鏡頭大小

C++

extern "C" int __declspec(dllexport) __stdcall  Init(int& outCameraWidth, 
																										 int& outCameraHeight)
{
	// Load LBP face cascade.
	if (!_faceCascade.load("lbpcascade_frontalface.xml"))
		return -1;

	// 打開鏡頭
	_capture.open(0);
	if (!_capture.isOpened())
		return -2;
	
	//取得視訊大小
	outCameraWidth = _capture.get(CAP_PROP_FRAME_WIDTH);
	outCameraHeight = _capture.get(CAP_PROP_FRAME_HEIGHT);

	return 0;
}

C#

在Opencv資料夾下找到lbpcascade_frontalface.xml,並放到Unity專案root資料夾下。

				int camWidth = 0, camHeight = 0;
        int result = OpenCVInterop.Init(ref camWidth, ref camHeight);
        if (result < 0)
        {
            if (result == -1)
            {
                Debug.LogWarningFormat("[{0}] Failed to find cascades definition.", GetType());
            }
            else if (result == -2)
            {
                Debug.LogWarningFormat("[{0}] Failed to open camera stream.", GetType());
            }

            return;
        }

        CameraResolution = new Vector2(camWidth, camHeight);

鏡頭大小的變數使用傳址呼叫,讓c++開啟鏡頭後順便設定好大小。使C#和c++使用相同的變數。

傳遞參數scale

C++

extern "C" void __declspec(dllexport) __stdcall SetScale(int scale)
{
	_scale = scale;
}

C#

private const int DetectionDownScale = 1;
void Start()
    {
        ...
        OpenCVInterop.SetScale(DetectionDownScale);
        _ready = true;
    }

辨識人臉

C++

extern "C" void __declspec(dllexport) __stdcall Detect(Circle* outFaces, 
																											int maxOutFacesCount,
																											int& outDetectedFacesCount)
{
	Mat frame;
	_capture >> frame;
	if (frame.empty())
		return;

	std::vector<Rect> faces;
	// Convert the frame to grayscale for cascade detection.
	Mat grayscaleFrame;
	cvtColor(frame, grayscaleFrame, COLOR_BGR2GRAY);
	Mat resizedGray;
	// Scale down for better performance.
	resize(grayscaleFrame, resizedGray, Size(frame.cols / _scale, frame.rows / _scale));
	equalizeHist(resizedGray, resizedGray);

	// Detect faces.
	_faceCascade.detectMultiScale(resizedGray, faces);

	// Draw faces.
	for (size_t i = 0; i < faces.size(); i++)
	{
		Point center(_scale * (faces[i].x + faces[i].width / 2), _scale * (faces[i].y + faces[i].height / 2));
		ellipse(frame, center, Size(_scale * faces[i].width / 2, _scale * faces[i].height / 2), 0, 0, 360, Scalar(0, 0, 255), 4, 8, 0);

		// Send to application.
		outFaces[i] = Circle(faces[i].x, faces[i].y, faces[i].width / 2);
		//返回數量用的
		outDetectedFacesCount++;

		if (outDetectedFacesCount == maxOutFacesCount)
			break;
	}

	// Display debug output.
	imshow(_windowName, frame);
}

步驟:

灰階→縮小解析度→直方圖均衡化→偵測

【補充】

直方圖均衡化(equalizeHist):

將拉伸數值分佈範圍從0-255。假設影像過曝(如藍色曲線),則直方圖均衡化能將其值範圍拉伸0-255區間內,使黑白更分明。 https://youtu.be/jWShMEhMZI4

人臉偵測 (detectMultiScale):

https://blog.csdn.net/leaf_zizi/article/details/107637433

CascadeClassifier.detectMultiScale(輸入圖片, 輸出向量, scaleFactor=1.1 , minNeighbor=3);

輸入圖片: 只接受灰階

scaleFactor:每次圖像縮小的比例,

minNeighbor:每個候選矩形有多少個”鄰居”,我的理解是:一個滑動窗口中的圖元需要符合幾個條件才能判斷為真。

大概意思是Haar cascade的工作原理是一種”滑動視窗”的方法,通過在圖像中不斷的”滑動檢測視窗”來匹配人臉。

因為圖像的圖元有大有小,圖像中的人臉因為遠近不同也會有大有小,所以需要通過scaleFactor參數設置一個縮小的比例,對圖像進行逐步縮小來檢測,這個參數設置的越大,計算速度越快,但可能會錯過了某個大小的人臉。

其實可以根據圖像的圖元值來設置此參數,圖元大縮小的速度就可以快一點,通常在1~1.5之間。

那麼,經過多次的反覆運算,實際會檢測出很多很多個人臉,這一點可以通過把minNeighbors 設為0來驗證。

所以呢,minNeighbors參數的作用就來了,只有其”鄰居”大於等於這個值的結果才認為是正確結果。

返回Rect ,其包含<x,y,w,h>

使用UDP

基本的傳輸協定,因為不會檢查是否收到或重傳等資訊,因此封包小、速度較快,缺點是長度上限548 Bytes,不適合串流影片,在此只用來傳輸事件資訊。 好處是就算Unity那端的port沒有開啟,UPD因不會檢查是否收到,所以不會報錯。

成果:

偵測手掌開闔。 參考:https://www.raywenderlich.com/5475-introduction-to-using-opencv-with-unity

尋找輪廓的原理先略過,主要流程是轉灰階=>高思模糊=>Canny邊緣偵測=>向外擴張=>findContours(尋找輪廓)

Python 傳送資料

import numpy as np
import cv2
import socket

UDP_IP = "127.0.0.1"
UDP_PORT = 5065

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

while true:
    sock.sendto( ("data!").encode(), (UDP_IP, UDP_PORT) )
    print("data sent")
    
capture.release()
cv2.destroyAllWindows()

C# UPD 建立連線

// 1. Declare Variables
    Thread receiveThread;  //在背景持續接受UDP訊息
    UdpClient client;   // parse the pre-defined address for data
    int port;   //port number

// 2. Initialize variables
    void Start()
    {
        port = 5065;
        InitUDP();
    }

    // 3. InitUDP
    private void InitUDP()
    {
        print("UDP Initialized");
        receiveThread = new Thread(new ThreadStart(ReceiveData)); //開個新的帶有參數的thread,傳入方法當參數
        receiveThread.IsBackground = true;
        receiveThread.Start();
    }

C# 定義接受方法

// 4. Receive Data
    private void ReceiveData()
    {
        client = new UdpClient(port); //指定port
        while (true)
        {
            try
            {
                IPEndPoint anyIP = new IPEndPoint(IPAddress.Parse("0.0.0.0"), port); //任何ip
                byte[] data = client.Receive(ref anyIP); //資料

                string text = Encoding.UTF8.GetString(data); //binary => utf8 text
                print(">> " + text);
								//....
            }
            catch (Exception e)
            {
                print(e.ToString());
            }
        }
    }

使用TCP

需經過三項交握確認連線。
若server(這裡是unity)的port沒有開,會收到傳送失敗的exception。

Python 建立連線

import numpy as np
import cv2
import socket

TCP_IP = "127.0.0.1"
TCP_PORT = 5066

#sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # SOCK_DGRAM 長度限制 548bytes,但不需要預先connect
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # TCP

#TCP連線
address=(TCP_IP ,TCP_PORT )
sock.connect(address)

print('sock init')
sock.send('Hi'.encode('utf-8'));

sock.close() #才會把累積的資料傳送

C# 建立連線

public class ImageReceiver : MonoBehaviour
{
    //TCP Port 開啟
    Thread receiveThread;
    TcpClient client;
    TcpListener listener;
    int port;
    private void Start()
    {
        InitTcp();
    }
    void InitTcp()
    {
        port = 5066;
        print("TCP Initialized");
        IPEndPoint anyIP = new IPEndPoint(IPAddress.Parse("127.0.0.1"), port);
        listener = new TcpListener(anyIP);
        listener.Start();
				//開個新的帶有參數的thread,傳入方法當參數
        receiveThread = new Thread(new ThreadStart(ReceiveData));
        receiveThread.IsBackground = true;
        receiveThread.Start();
    }
    private void OnDestroy()
    {
        receiveThread.Abort();
    }
}

定義接收方法

private void ReceiveData()
    {
        print("received somthing...");
        try
        {
            while (true)
            {
                client = listener.AcceptTcpClient();
                NetworkStream stream = new NetworkStream(client.Client);
                StreamReader sr = new StreamReader(stream);
                print(sr.ReadToEnd());
            }
        }
        catch (Exception e)
        {
            print(e);
        }
    }

注意1,由於一開始需要經過三項交握,所以”TCP Initialized”之後會log一次”received something…”,該訊息為用來回應tcp連線的。

注意2,在python端sock.close()之前收到的訊息會一直存在sr,直到close之後才一次print出,所以傳輸每frame都會經過:建立連線=>打包資料=>傳送=>close() 的循環。

傳輸畫面

Python 端

注意資料只能傳byte,string等型態,所以這邊使用json格式。Python有另一個類次的pickel插件,但該格式只能python使用,不方便給unity。

while cap.isOpened():          
    ret, img = cap.read()
    img_data={'image':cv2.imencode('.jpg',img)[1].ravel().tolist()}
    data=json.dumps(img_data);
    
    #準備連線
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # TCP
    sock.connect(address)
    
    #傳送資料
    sock.sendall(bytes(data,encoding='utf-8'))
    #print('sock sent')
    
    cv2.imshow("Image",img)
    cv2.waitKey(10)
    sock.close()

C# 端

宣告texture:

public class ImageReceiver : MonoBehaviour
{
    //TCP Port 開啟
    Thread receiveThread;
    TcpClient client;
    TcpListener listener;
    int port;
    private void Start()
    {
        InitTcp();
    }
    void InitTcp()
    {
        port = 5066;
        print("TCP Initialized");
        IPEndPoint anyIP = new IPEndPoint(IPAddress.Parse("127.0.0.1"), port);
        listener = new TcpListener(anyIP);
        listener.Start();
				//開個新的帶有參數的thread,傳入方法當參數
        receiveThread = new Thread(new ThreadStart(ReceiveData));
        receiveThread.IsBackground = true;
        receiveThread.Start();
    }
    private void OnDestroy()
    {
        receiveThread.Abort();
    }
}

由於Unity不支援多線程,無法在接收方法中直接設定texture,所以在fixedUpdate中設定。

private void FixedUpdate()
    {
        tex.LoadImage(imageDatas);
        img.texture = tex;
    }

成功~

Python傳輸畫面至Unity

操控物件也同理,在json資料中夾帶著操作的訊號就好。 但由於是跟著畫面資料一起串流,會有滿嚴重的延遲。 若資料改用UDP、畫面使用TCP傳輸應該會比較好!

這篇文章 【徵文賞-延展實境】佳作|史上最累Debug!深蹲才能過關(Unity + OpenCV 跨軟體傳輸實作) – 林慶佳 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>