-
Notifications
You must be signed in to change notification settings - Fork 254
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[NUI.Scene3D] Add capture to Scene3D.SceneView #6271
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
/* | ||
* Copyright(c) 2024 Samsung Electronics Co., Ltd. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
* | ||
*/ | ||
|
||
using System; | ||
using System.ComponentModel; | ||
|
||
namespace Tizen.NUI.Scene3D | ||
{ | ||
/// <summary> | ||
/// Event arguments of SceneView capture finished event. | ||
/// </summary> | ||
[EditorBrowsable(EditorBrowsableState.Never)] | ||
public class CaptureFinishedEventArgs : EventArgs | ||
{ | ||
/// <summary> | ||
/// Integer ID of the capture request. | ||
/// </summary> | ||
[EditorBrowsable(EditorBrowsableState.Never)] | ||
public int CaptureId | ||
{ | ||
get; set; | ||
} | ||
|
||
/// <summary> | ||
/// ImageUrl of the captured result | ||
/// If the capture is failed, it is null. | ||
/// </summary> | ||
[EditorBrowsable(EditorBrowsableState.Never)] | ||
public ImageUrl CapturedImageUrl | ||
{ | ||
get; set; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -22,6 +22,9 @@ | |||||
using Tizen.NUI; | ||||||
using Tizen.NUI.Binding; | ||||||
using Tizen.NUI.BaseComponents; | ||||||
using System.Threading.Tasks; | ||||||
using System.Collections.Generic; | ||||||
using System.Diagnostics; | ||||||
|
||||||
namespace Tizen.NUI.Scene3D | ||||||
{ | ||||||
|
@@ -68,14 +71,24 @@ namespace Tizen.NUI.Scene3D | |||||
/// <since_tizen> 10 </since_tizen> | ||||||
public class SceneView : View | ||||||
{ | ||||||
private Dictionary<int, TaskCompletionSource<ImageUrl>> asyncCaptureIds = new(); | ||||||
private string skyboxUrl; | ||||||
|
||||||
// CameraTransitionFinishedEvent | ||||||
private EventHandler cameraTransitionFinishedEventHandler; | ||||||
private CameraTransitionFinishedEventCallbackType cameraTransitionFinishedEventCallback; | ||||||
|
||||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] | ||||||
private delegate void CameraTransitionFinishedEventCallbackType(IntPtr data); | ||||||
|
||||||
// CaptureFinishedEvent | ||||||
private EventHandler<CaptureFinishedEventArgs> captureFinishedEventHandler; | ||||||
private EventHandler<CaptureFinishedEventArgs> asyncCaptureFinishedEventHandler; | ||||||
private CaptureFinishedEventCallbackType captureFinishedEventCallback; | ||||||
|
||||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] | ||||||
private delegate void CaptureFinishedEventCallbackType(IntPtr data, int captureId, IntPtr capturedImageUrl); | ||||||
|
||||||
internal SceneView(global::System.IntPtr cPtr, bool cMemoryOwn) : this(cPtr, cMemoryOwn, cMemoryOwn) | ||||||
{ | ||||||
} | ||||||
|
@@ -104,6 +117,15 @@ protected override void Dispose(DisposeTypes type) | |||||
cameraTransitionFinishedEventCallback = null; | ||||||
} | ||||||
|
||||||
if (captureFinishedEventCallback != null) | ||||||
{ | ||||||
NUILog.Debug($"[Dispose] captureFinishedEventCallback"); | ||||||
|
||||||
Interop.SceneView.CaptureFinishedDisconnect(GetBaseHandleCPtrHandleRef, captureFinishedEventCallback.ToHandleRef(this)); | ||||||
NDalicPINVOKE.ThrowExceptionIfExistsDebug(); | ||||||
captureFinishedEventCallback = null; | ||||||
} | ||||||
|
||||||
LayoutCount = 0; | ||||||
|
||||||
base.Dispose(type); | ||||||
|
@@ -170,6 +192,64 @@ public event EventHandler CameraTransitionFinished | |||||
} | ||||||
} | ||||||
|
||||||
/// <summary> | ||||||
/// An event emitted when Capture is finished. | ||||||
/// If Capture is successed, CaptureFinishedEventArgs includes finished capture ID and ImageUrl of the captured image. | ||||||
/// If Capture is failed, the ImageUrl is null. | ||||||
/// </summary> | ||||||
// This will be public opened after ACR done. (Before ACR, need to be hidden as Inhouse API) | ||||||
[EditorBrowsable(EditorBrowsableState.Never)] | ||||||
public event EventHandler<CaptureFinishedEventArgs> CaptureFinished | ||||||
{ | ||||||
add | ||||||
{ | ||||||
if (captureFinishedEventHandler == null) | ||||||
{ | ||||||
captureFinishedEventCallback = OnCaptureFinished; | ||||||
Interop.SceneView.CaptureFinishedConnect(SwigCPtr, captureFinishedEventCallback.ToHandleRef(this)); | ||||||
NDalicPINVOKE.ThrowExceptionIfExists(); | ||||||
} | ||||||
captureFinishedEventHandler += value; | ||||||
} | ||||||
|
||||||
remove | ||||||
{ | ||||||
captureFinishedEventHandler -= value; | ||||||
if (captureFinishedEventHandler == null && captureFinishedEventCallback != null) | ||||||
{ | ||||||
Interop.SceneView.CaptureFinishedDisconnect(SwigCPtr, captureFinishedEventCallback.ToHandleRef(this)); | ||||||
NDalicPINVOKE.ThrowExceptionIfExists(); | ||||||
captureFinishedEventCallback = null; | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
/// <summary> | ||||||
/// An event emitted when CaptureAsync is finished. | ||||||
/// </summary> | ||||||
private event EventHandler<CaptureFinishedEventArgs> AsyncCaptureFinished | ||||||
{ | ||||||
add | ||||||
{ | ||||||
if (asyncCaptureFinishedEventHandler == null) | ||||||
{ | ||||||
CaptureFinished += dummy; | ||||||
} | ||||||
asyncCaptureFinishedEventHandler += value; | ||||||
} | ||||||
|
||||||
remove | ||||||
{ | ||||||
asyncCaptureFinishedEventHandler -= value; | ||||||
if (asyncCaptureFinishedEventHandler == null) | ||||||
{ | ||||||
CaptureFinished += dummy; | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
void dummy(object sender, CaptureFinishedEventArgs e) {} | ||||||
|
||||||
/// <summary> | ||||||
/// Set/Get the ImageBasedLight ScaleFactor. | ||||||
/// Scale factor controls light source intensity in [0.0f, 1.0f] | ||||||
|
@@ -573,6 +653,82 @@ public void ResetResolution() | |||||
if (NDalicPINVOKE.SWIGPendingException.Pending) throw NDalicPINVOKE.SWIGPendingException.Retrieve(); | ||||||
} | ||||||
|
||||||
/// <summary> | ||||||
/// Requests to capture this SceneView with the Camera. | ||||||
/// When the capture is finished, CaptureFinished Event is emited. | ||||||
/// <see cref="CaptureFinishedEventArgs"/> includes <see cref="CaptureFinishedEventArgs.CaptureId"/> and <see cref="CaptureFinishedEventArgs.CapturedImageUrl"/>. | ||||||
/// If the capture is successful, the <see cref="CaptureFinishedEventArgs.CapturedImageUrl"/> contains url of captured image. | ||||||
/// If the capture fails, the <see cref="CaptureFinishedEventArgs.CapturedImageUrl"/> is null. | ||||||
/// </summary> | ||||||
/// <param name="camera">Camera to be used for capture.</param> | ||||||
/// <param name="size">captured size.</param> | ||||||
/// <remarks> | ||||||
/// The input camera should not be used for any other purpose during Capture. | ||||||
/// (Simultaneous usage elsewhere may result in incorrect rendering.) | ||||||
/// The camera is required to be added in this SceneView. (Not need to be a selected camera) | ||||||
/// If the SceneView is disconnected from Scene, the left capture requests are canceled with fail. | ||||||
/// </remarks> | ||||||
/// <returns> capture id that id unique value to distinguish each requiest.</returns> | ||||||
// This will be public opened after ACR done. (Before ACR, need to be hidden as Inhouse API) | ||||||
[EditorBrowsable(EditorBrowsableState.Never)] | ||||||
public int Capture(Scene3D.Camera camera, Vector2 size) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dali 구조와 상관 없이 순수 API사용자 관점에서 Capture API의 위치는 camera객체가 갖고 있어야 하는게 아닐까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 특정 행위에 대한 결과물이 비동기적으로 전달되는 경우에는
Suggested change
이런 API가 제공되면 앱 개발자는 이렇게 단순하게 코드 작성이 가능해 집니다. var url = await sceneView.Caputre(camea, new Vector2(100, 100));
imageView.ResourceUrl = url.ToString(); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ImageUrl class 는 UI Thread 에서만 생성되어야하는 class 이기 때문에 Task 로 만들 수 없습니다. ImageUrl class 는 외부 요인에 의해 생성된 이미지요소(ex : Texture, NativeImage 등,)의 lifecycle을 관리하기위한 class 이기 때문입니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Scene3D 개발자 관점에서, 카메라를 가져오고 설정하는 API 는 모두 SceneView에 있는데 Capture 만 Camera API 로 빠져있는 것이 더 어색해 보입니다. 하나의 Camera 가 동시에 여러 SceneView 에 속해있을 수도 없고, 또한 Camera 자기 자신은 자기가 속해있는 SceneView 도 모르는 상황으로, 정말 Camera 관련 속성들만 제어/관리 하고 있습니다. 이런 상태에서 SceneView 에 의존성이 생기는 API 하나를 추가하는 것은 별로 좋은 구조로 보이지 않습니다. View.SetRenderEffect(BackgroundBlurEffect) 처럼 RenderEffect 에 대한 구현을 Vjew 가 아니라 RenderEffect 에서 설정하는 것처럼 느껴집니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
말씀하신 형태도 API관점에서 좋은 방법이라고 생각합니다만, 몇가지 문제가 있는데요. 예를들어, Camera가 화면을 촬영하기 위해서 필요한 몇가지 속성 중에 종횡비가 있습니다. 그 외에도 차치하신 DALi 구조적인 문제로서도 Camera에서 Capture를 제공하기 위해서는 다양한 고통이 필요한데요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
제대로된 사용법으로 사용하면 블럭이 안됩니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 가이드 주신대로 여러가지 시도를 해본 결과 OnTouch 는 정상적으로 끝나면서 main thread 에서 발생한 이벤트도 정상적으로 받는 것을 확인했습니다. private bool OnTouch(object s, View.TouchEventArgs e)
{
if (e.Touch.GetState(0) == PointStateType.Down)
{
Tizen.Log.Error("NUI",$"Before ASDF\n");
ASDF();
Tizen.Log.Error("NUI",$"End ASDF\n");
}
return true;
}
private async void ASDF()
{
Tizen.Log.Error("NUI",$"Before await\n");
var task = DummyAsync();
int ret = await task;
Tizen.Log.Error("NUI",$"End await {ret}\n");
} 결국은 async 라는 태그가 달린 별도의 함수를 앱에서 알아서 잘 구현해야만 하고, await 이후 ImageUrl 을 받은 사용자가 무엇을 어떻게 할지는 저희가 아직은 신경 안써도 된다고 이해했습니다. if (e.Success)
{
ret.SetResult(e.CapturedImageUrl);
} 소소하게 이 부분에 대해서, 우선 e.CapturedImageUrl 은 cMemOwned 가 false 인 상태로 만들어졌기 때문에, 이 상태 그대로 SetResult 에 넣으면 안되고, Registry 에 등록을 한번 해야지 정상적인 사용이 가능할 것으로 보입니다. 승호님께서 구현하실 때 참고바랍니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 그런데 다른 질문이 있는데요, 예를 들어 FileIO 에 걸려있는 아무개 Task 라고 하더라도 유저가 원한다면 main loop 에 block 을 걸어서 FileIO 가 끝날때 까지 강제로 기다리게 하는 방법들이 잘 고려가 되어있을 것으로 예상되는데요... 지금의 Capture 처럼 main loop 에 dependency 가 있는 Task 의 경우에는 앱에서 동기적으로 task complete 결과를 기다리는 코드를 사용한 경우 무조건 dead lock 에 걸리게 될 것인데 이러한 사항이 일반적인 것이어서 따로 주석을 달지 않아도 되는건지, 아니면 함수에서 주석으로 적어놓기만 하면 되는건지 궁금합니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저희가... event로 뭔가를 제공할때도.. |
||||||
{ | ||||||
int id = Interop.SceneView.Capture(SwigCPtr, camera.SwigCPtr, Vector2.getCPtr(size)); | ||||||
if (NDalicPINVOKE.SWIGPendingException.Pending) throw NDalicPINVOKE.SWIGPendingException.Retrieve(); | ||||||
return id; | ||||||
} | ||||||
|
||||||
/// <summary> | ||||||
/// Requests to capture this SceneView with the Camera asynchronously. | ||||||
/// </summary> | ||||||
/// <param name="camera">Camera to be used for capture.</param> | ||||||
/// <param name="size">captured size.</param> | ||||||
/// <remarks> | ||||||
/// The input camera should not be used for any other purpose during Capture. | ||||||
/// (Simultaneous usage elsewhere may result in incorrect rendering.) | ||||||
/// The camera is required to be added in this SceneView. (Not need to be a selected camera) | ||||||
/// If the SceneView is disconnected from Scene, the left capture requests are canceled with fail. | ||||||
/// </remarks> | ||||||
/// <returns> | ||||||
/// A task that represents the asynchronous operation. The task result contains the URL of the captured image. | ||||||
/// If the capture is successful, the task result is the ImageURL. | ||||||
/// If the capture fails, the task will complete with an <see cref="InvalidOperationException"/> | ||||||
/// </returns> | ||||||
// This will be public opened after ACR done. (Before ACR, need to be hidden as Inhouse API) | ||||||
[EditorBrowsable(EditorBrowsableState.Never)] | ||||||
public Task<ImageUrl> CaptureAsync(Scene3D.Camera camera, Vector2 size) | ||||||
{ | ||||||
void Handler(object _, CaptureFinishedEventArgs e) | ||||||
{ | ||||||
if (asyncCaptureIds.TryGetValue(e.CaptureId, out var tcs)) | ||||||
{ | ||||||
try | ||||||
{ | ||||||
if (e.CapturedImageUrl != null) | ||||||
{ | ||||||
tcs.SetResult(e.CapturedImageUrl); | ||||||
} | ||||||
else | ||||||
{ | ||||||
tcs.SetException(new InvalidOperationException("Fail to Capture")); | ||||||
} | ||||||
} | ||||||
finally | ||||||
{ | ||||||
AsyncCaptureFinished -= Handler; | ||||||
asyncCaptureIds.Remove(e.CaptureId); | ||||||
} | ||||||
} | ||||||
}; | ||||||
|
||||||
AsyncCaptureFinished += Handler; | ||||||
var captureId = Interop.SceneView.Capture(SwigCPtr, camera.SwigCPtr, Vector2.getCPtr(size)); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 모종의 이유로 (ex : SceneView 가 SceneOff 상태) 캡쳐가 실패한 경우 Capture API 가 끝나기 전에 CaptureFinished 콜백이 올라오도록 구현되어 있는 것이 가장 마지막 코드인데요, CaptureFinished 콜백을 Idler 에서 올려보내도록 수정 부탁드립니다. |
||||||
TaskCompletionSource<ImageUrl> ret = new TaskCompletionSource<ImageUrl>(); | ||||||
asyncCaptureIds.Add(captureId, ret); | ||||||
|
||||||
return ret.Task; | ||||||
} | ||||||
|
||||||
internal void SetUseFramebuffer(bool useFramebuffer) | ||||||
{ | ||||||
Interop.SceneView.UseFramebuffer(SwigCPtr, useFramebuffer); | ||||||
|
@@ -693,5 +849,25 @@ private void OnCameraTransitionFinished(IntPtr data) | |||||
{ | ||||||
cameraTransitionFinishedEventHandler?.Invoke(this, EventArgs.Empty); | ||||||
} | ||||||
|
||||||
// Callback for capture finished signal | ||||||
private void OnCaptureFinished(IntPtr data, int captureId, IntPtr capturedImageUrl) | ||||||
{ | ||||||
CaptureFinishedEventArgs e = new CaptureFinishedEventArgs(); | ||||||
ImageUrl imageUrl = new ImageUrl(NUI.Interop.ImageUrl.NewImageUrl(new ImageUrl(capturedImageUrl, false).SwigCPtr), true); | ||||||
NDalicPINVOKE.ThrowExceptionIfExists(); | ||||||
|
||||||
e.CaptureId = captureId; | ||||||
e.CapturedImageUrl = imageUrl.HasBody() ? imageUrl : null; | ||||||
|
||||||
if (asyncCaptureIds.ContainsKey(e.CaptureId)) | ||||||
{ | ||||||
asyncCaptureFinishedEventHandler?.Invoke(this, e); | ||||||
} | ||||||
else | ||||||
{ | ||||||
captureFinishedEventHandler?.Invoke(this, e); | ||||||
} | ||||||
} | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,5 +34,4 @@ public enum VisibilityChangeType | |
/// <since_tizen> 3 </since_tizen> | ||
PARENT | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SceneView CaptureFinished 이벤트가 오기 전에 SceneView 자기 자신이 Dispose() 될 수 있으니, 이 경우 CaptureAsync 를 기다리는 유저한테 실패정보를 알려주기 위해서
TaskCompletionSource<ImageUrl>
을 private 변수로 만들고, 이 Dispose 타이밍에 SetResult() 를 해줘야 할 것 같습니다.CaptureFinished 콜백 형태로만 제공한 경우에는 지금처럼 Dispose 된 경우에 별도로 앱한테 DIspose 여부를 알려줄 필요가 없었을텐데...Task<> 로 제공해버린 경우 SetResult 가 오기 전까지 await 를 걸어버린 함수가 계속 남아있을 것이기 때문에 (아마도?) 강제로 SetResult 를 해줘야만 한다..라고 이해를 하고있는데요..
이렇게 Dispose 가 되는 도중 (~= Disposed 가 아직 false 이지만 기본적인 리소스들이 한창 해제가 되고있는 와중에) 유저가 추가적으로 무언가 작업을 할 수 있도록 하는 여지를 만드는게 과연 좋은 일인지 모르겠습니다.
유저가 이 Dispose 로 인해서 Capture 가 취소된 경우를 별도로 처리할 수 있게 하기 위해서, 결국 Capture Result 의 종류를 3가지 이상 (성공 / 실패 / Dispose 로 인한 취소 등) 을 만들어야하고, 특히 Dispose 로 인한 취소가 결과물로 넘어간 경우에는 SceneView 에 접근하는 어떤 동작도 구현해서는 안된다는 제약사항을 주석으로만 남겨야하는데... 앱 개발자들이 이 동작을 잘 납득하고 그대로 따라줄지는 모르겠습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
특히 이 "Dispose 로 인한 취소" 같은 경우에는 CaptureFinished 콜백을 사용하는 경우에는 필요없는 정보이기 때문에, CaptureAsync 에서만 사용되게 될텐데... 앞으로 Task<> 형태로 return 하는 API 를 계속해서 추가할 때마다 '실제 return 정보 + Dispose 로 인한 취소 여부' 정보를 포함한 struct 를 새로 정의해서 Task 의 return 타입으로 만들어나가면 되는건지.. 의견을 구합니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 문제 겸사겸사 찝찝했던 것을 해결하는 겸사겸사 SceneOff 타이밍이나 SceneView 소멸 시점에 실패 콜백을 올려주도록 수정 할 계획입니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
확인해보니 SceneOff때는 이미 남은 캡쳐 요청들에 대해서 실패 콜백을 올려주고 있었습니다.