VRED Core 웹 API를 사용하는 방법을 알아봅니다.
동영상 캡션: 안녕하세요. VRED Core의 마지막 튜토리얼 동영상에 오신 것을 환영합니다. 저는 크리스토퍼라고 하며, 오늘은 지난 동영상에서 설명한 스트리밍 앱의 예제를 확장하고, VRED의 웹 API를 사용하여 더 많은 기능을 구현해 보겠습니다.
Python API v2에서 오토데스크는 Python 명령을 VRED로 전송하는 데 사용할 수 있는 Python 끝점을 바로 승계하는 Python 웹 API를 도입했습니다. 이전 동영상에서는 Python 끝점을 사용하여 VRED로 Python 명령을 전송하고 비디오 스트림을 구현하기 위해 VRED Core를 위한 자체 HTML 인터페이스를 작성해 보았습니다.
예를 들어 임의의 Python 명령을 Python 끝점으로 전송해 이 작업의 결과를 응답으로 받을 수 있습니다. 이러한 끝점의 단점은 객체 또는 렌더링된 인터페이스를 사용하는 대신 Python 명령의 문자열 표현을 전송해야 하며, JavaScript 앱에서 사용하기 위해 반환 값을 제대로 전달해야 한다는 점입니다.
새로운 웹 API를 통해 VRED는 VRED의 Python 인터페이스에 대한 직접 연결과 같은 역할을 하는 완전히 통합된 JavaScript 모듈을 제공합니다. 웹 API는 Python API v2에서 사용할 수 있는 모든 기능에 대해 객체 지향 액세스 지점을 제공합니다. 즉, 적절한 JavaScript 유형에 액세스할 수 있으며 약속이나 오류 처리 같은 일반적인 JavaScript 기능을 사용할 수 있습니다.
예를 들어 이러한 JavaScript 호출로 카메라를 새로 만들어 사용할 수 있습니다. 그런 다음 해당 카메라 객체를 사용하여 이름을 설정하거나 활성 상태를 설정하거나 다른 방식으로 프로세스를 설정할 수 있습니다. 카메라 객체에는 물론 Python 클래스 vrdCamera에서도 사용할 수 있는 모든 함수가 포함되어 있습니다.
자체 앱에서 새 웹 API를 사용하는 방법에 대해 살펴보겠습니다. 이 튜토리얼에서는 이전 동영상에서 작성한 사용자화 HTML 앱에 주석 관리자를 통합해 보겠습니다. 이전 튜토리얼의 코드를 직접 확장하게 되므로 이전 동영상을 시청하지 않은 경우 먼저 시청하는 것이 좋습니다.
좋습니다. 시작하겠습니다.
지난 튜토리얼에서는 모든 CSS 및 JavaScript 코드를 HTML 파일에 배치했습니다. 정돈을 위해 스타일시트와 스크립트를 별도의 파일로 이동했습니다. 이렇게 하면 HTML이 훨씬 깔끔해집니다.
먼저 장면에 주석을 추가하는 방법을 추가하려고 합니다. 이 튜토리얼에서는 이러한 주석의 위치를 설정하지 않고 그냥 주석을 추가한 후 이름과 설명을 지정하겠습니다. 이를 위해 이름과 설명에 대한 두 개의 입력과 주석을 생성하기 위한 제출 버튼이 포함된 양식을 HTML 코드에 추가합니다. 이를 div 컨테이너에 래핑하고 CSS를 사용하여 일부 스타일을 지정하고 비디오 스트림 위에 배치할 것입니다. 이번에는 왼쪽 상단에 배치합니다.
다음으로, 주석 관리자를 위한 JavaScript 코드를 포함하는 webapiscript라는 새 JavaScript 파일을 작성합니다. 이 가져오기 문을 사용하여 VRED Core용 JavaScript API를 JavaScript 모듈에 추가할 수 있습니다. 보시다시피 예제의 경우 포트 8888을 통해 localhost를 사용하고 있습니다. VRED Core 설정이 다르게 지정된 경우에는 이러한 값을 조정해야 합니다.
먼저 name 및 description 매개변수를 사용하여 새 함수 addAnnotation을 추가합니다. 이 함수에서, 가져온 VRED의 JavaScript API를 직접 호출합니다. 보시다시피 API는 Python API와 매우 유사합니다.
여기서는 vrAnnotation 서버를 사용하여 name 매개변수와 함께 createAnnotation 메서드를 호출합니다. 그러면 then 함수를 사용하여 비동기적으로 처리하는 promise 객체가 반환됩니다. Javascript 패턴에 익숙하지 않은 분을 위해 설명하자면, 이것을 async/await 패턴이라고 하며 프로그래밍 시에 동시성을 처리하는 최신 접근 방식입니다. createAnnotation은 기본적으로 반환하기 전에 시간이 필요하므로 기다렸다가 다음 작업을 수행합니다. 그러면 스크립트가 계속 작동하면서 VRED를 기다릴 때 조회하지 않습니다.
이제 and then 함수 내에서 findAnnotation 함수를 안전하게 호출하여 새 주석 노드에 대한 참조를 가져올 수 있습니다. 동일한 async/await 패턴에 따라 새 주석의 텍스트를 description 매개변수로 설정합니다. 주석을 작성하는 동안 오류가 발생하면 이 오류를 포착하여 사용자에게 알릴 수 있습니다. 이 경우에는 Python 터미널에 오류 메시지를 출력합니다.
주석 추가 기능을 사용할 수 있지만 작성한 HTML 양식에도 연결해야 합니다. 이를 위해 양식의 unsubmit 콜백을 연결합니다. 여기서 이름과 설명 입력을 추출하고 addAnnotation 함수를 호출할 수 있습니다. 새로운 기능을 테스트하면 모든 것이 제대로 작동하고 장면에 새 주석을 추가할 수 있음을 알 수 있습니다. 좋습니다.
또한 주석을 삭제하는 기능과 함께 장면에 주석 목록을 추가해 보겠습니다. 이제 HTML에서 ID를 사용하여 정렬되지 않은 목록 요소를 추가하기만 하면 됩니다. 이 목록에 주석을 동적으로 추가하고 제거할 수 있습니다.
JavaScript 스크립트에서 모든 주석을 로드하는 함수로 시작합니다. 모든 주석 목록을 가져오려면 vrAnnotationService를 사용합니다. async/await 패턴으로 VRED의 모든 주석 목록을 기다렸다가 하나씩 반복합니다.
각 주석에 대해 이름과 포함된 텍스트를 쿼리합니다. 이 작업도 async/await 패턴을 사용하여 수행합니다. 그렇기 때문에 loop 함수를 async로 표시해야 합니다. 이름 및 주석 텍스트를 사용하여 곧 구현할 addAnnotationItem이라는 두 번째 함수를 호출할 수 있습니다.
하지만 먼저 loadAnnotation 함수가 호출될 때마다 목록을 지우려고 합니다. 이를 위해 모든 목록에서 모든 항목을 지우는 deleteAnnotationItems 함수를 추가하고 이를 load 함수 시작 시에 호출합니다. 그런 다음 name 및 description 매개변수를 사용하여 addAnnotationItem이라는 새 함수를 작성합니다.
여기에서는 주석 이름 및 설명을 보관할 텍스트 노드로 채우는 새 HTML 목록 요소를 생성합니다. 이름이 정의되지 않은 경우 이 함수에서 반환합니다.
또한 VRED에서 주석을 삭제할 버튼을 추가하려고 합니다. 이를 위해 주석에 링크를 추가할 수 있습니다. 링크를 보려면 어떤 항목을 링크 속성으로 설정해야 합니다. 하지만 이 기능은 없습니다. 대신 onClick 함수에 연결하여 클릭 시 AnnotationService를 사용해 이 주석을 삭제합니다.
여기에서도 async/await 패턴을 사용했습니다. 마지막으로, 주석 요소를 목록에 추가합니다. 지금은 스트리밍 앱을 로드할 때 주석 목록이 한 번 로드되지만 업데이트되지는 않습니다. 앱이 실제로 동적으로 되도록 하려면 주석을 추가하거나 삭제할 때 알리는 vrAnnotationService의 신호를 사용하면 됩니다. 이 방법은 Python 인터페이스에서 신호를 사용하는 것과 매우 유사합니다.
모두 끝났습니다. 앱을 테스트할 때 새 주석을 추가하면 주석 목록에서 바로 확인할 수 있습니다. 또한 장면에서 주석을 제거할 수 있는 삭제 버튼이 있습니다.
모든 주석이 동일한 위치에 추가되었지만 다른 튜토리얼을 위한 작업일 수 있습니다. VRED의 웹 API 작업 및 이를 사용하여 장면을 제어하는 동적인 대화식 HTML 응용프로그램을 구축하는 방법에 대해 알아봤습니다. 이것으로 VRED Core 튜토리얼 시리즈를 마칩니다. VRED Core를 시작하는 데 도움이 되었기를 바라며, 이 제품을 확장하고 사용할 수 있게 되셨기를 바랍니다.
시청해 주셔서 감사합니다. 다음에 뵙겠습니다.
튜토리얼 6: VRED Core 웹 API를 사용하는 방법 동영상과 함께 제공되는 예제 Python 스크립트입니다.
이러한 파일을 압축하여 다운로드하려면 여기를 클릭하십시오.
<!DOCTYPE html>
<html>
<head>
<title>Web API Example: Create Camera</title>
Web API Module
<script type="module">
import { api } from 'http://localhost:8888/api.js';
window.onload = function() {
// Use vrCameraService to create a camera in the scene
// Wait for the result and "then"...
api.vrCameraService.createCamera("New Custom Camera").then(async (vrdCamera) => {
// ... set a new name for the camera...
await vrdCamera.setName("New Custom Camera - Changed Name");
// ... and set the camera active
await vrdCamera.activate();
// Test
api.vrCameraService.getCameraNames().then((names) => {
window.alert(names.join('\n'));
})
});
}
</script>
</head>
</html>
<body>
</body>
<!DOCTYPE html>
<html>
<head>
<title>My Custom App</title>
<meta charset="utf-8"/>
<link rel="stylesheet" href="styles.css">
<script src="script.js"></script>
<script type="module" src="web-api-script.js"></script>
</head>
<body>
<div id="page">
<div id="add-annotations-container">
<h2>Annotations</h2>
<form id="annotationForm">
<input name="name" type="text" placeholder="Name" required />
<input name="description" type="text" placeholder="Description" required />
<button type="submit">Add</button>
</form>
<ul id="annotations"></ul>
</div>
Variant Set Container
<div id="variant-set-container">
<h2>Variant Sets</h2>
</div>
Video Stream Container
<iframe id="stream-container" frameborder="0" overflow="hidden" scroll="no"></iframe>
Python Terminal Container
<div class="python-interface">
<h2>Python</h2>
<div class="python-send-wrapper">
<input id="python-value" type="text" placeholder="Enter your python code..."></input>
<button onclick="executeTerminalCode(document.getElementById('python-value').value)">Send Python</button>
</div>
<textarea id="python-response"></textarea>
</div>
</div>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>My Custom App</title>
<meta charset="utf-8"/>
<link rel="stylesheet" href="styles.css">
<script src="script.js"></script>
</head>
<body>
<div id="page">
Variant Set Container
<div id="variant-set-container"></div>
Video Stream Container
<iframe id="stream-container" frameborder="0" overflow="hidden" scroll="no"></iframe>
Python Terminal Container
<div class="python-interface">
<div class="python-send-wrapper">
<input id="python-value" type="text" placeholder="Enter your python code..."></input>
<button onclick="executeTerminalCode(document.getElementById('python-value').value)">Send Python</button>
</div>
<textarea id="python-response"></textarea>
</div>
</div>
</body>
</html>
var host = "localhost";
var port = "8888";
function initializeStream() {
var w = window.innerWidth;
var h = window.innerHeight;
document.getElementById("stream-container").src = 'http://' + host + ':' + port + '/apps/VREDStream/index.html?width='+w+'&height='+h+'';
}
function updateVariantSets() {
sendAndReceivePython("getVariantSets()",
(response) => {
var variantSets = eval(response);
if(variantSets) {
// get a reference to our variant set container
var variantSetContainer = document.getElementById('variant-set-container');
// add element for each variant set
variantSets.forEach(variantSet => {
var variantSetNode = document.createElement('div');
variantSetNode.innerHTML = variantSet;
variantSetNode.onclick = function() {
sendAndReceivePython("selectVariantSet('"+variantSet+"')");
}
variantSetContainer.appendChild(variantSetNode);
});
}
}
);
}
function executeTerminalCode(python) {
console.log(python);
sendAndReceivePython(python,
(response) => {
var text = document.getElementById('python-response').value;
text += '\n' + response;
document.getElementById('python-response').value = text;
},
(error) => console.error("Error sending pyhton command.", python)
);
}
function sendAndReceivePython(command, successCallback, errorCallback) {
var http = new XMLHttpRequest();
var url = 'http://' + host + ':' + port + '/pythoneval2?value=' + encodeURI(command);
http.open('GET', url, true);
http.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
if(this.responseText && successCallback) {
successCallback(this.responseText)
}
}
};
http.onerror = function() {
if(errorCallback) {
errorCallback();
}
}
http.send();
}
window.onload = function() {
initializeStream();
updateVariantSets();
}
window.onresize = function() {
initializeStream();
}
html, body {
padding: 0;
margin: 0;
width: 100vw;
height: 100vh;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif
}
h2 {
color: white;
}
#page {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
#variant-set-container {
padding: 1em 2em;
position: absolute;
left: 1em;
bottom: 1em;
background-color: #2d2d2d;
display: flex;
flex-direction: column;
height: 20em;
overflow-y: scroll;
color: white;
}
#variant-set-container > div {
padding: 0.1em 0.2em;
background-color: #4b4b4b;
margin-bottom: 0.1em;
cursor: pointer;
}
#stream-container {
width: 100%;
height: 100%;
overflow: hidden;
}
.python-interface {
position: absolute;
padding: 1em 2em;
right: 1em;
bottom: 1em;
background-color: #2d2d2d;
display: flex;
flex-direction: column;
}
.python-send-wrapper {
margin-bottom: 0.5em;
}
.python-send-wrapper > input {
width: 25em;
}
textarea#python-response {
height: 10em;
resize: none;
}
#add-annotations-container {
position: absolute;
padding: 1em 2em;
left: 1em;
top: 1em;
background-color: #2d2d2d;
display: flex;
flex-direction: column;
}
#annotations {
color: white;
}
#annotations li {
list-style: none;
display: flex;
}
#annotations li strong {
margin-right: 0.5em;
}
#annotations li a {
color: white;
margin-left: 0.5em;
float: right;
}
import { api } from "http://localhost:8888/api.js";
// -- PART 1 --
// Add an annotation with a name and a description
function addAnnotation(name, description) {
api.vrAnnotationService
.createAnnotation(name)
.then(() => {
api.vrAnnotationService.findAnnotation(name).then((a) => {
a.setText(description);
addEventMessage("Annotations added successfully");
});
})
.catch(() =>
document.getElementById("python-response")("Add annotation failed")
);
}
// Annotation form
annotationForm.onsubmit = (ev) => {
ev.preventDefault();
const name = ev.target.elements.name.value;
const description = ev.target.elements.description.value;
addAnnotation(name, description);
};
// -- END: PART 1 --
function addEventMessage(message) {
var textarea = document.getElementById("python-response");
var text = textarea.value;
textarea.value = message + "\n" + text;
}
// Adds a new list item to the unordered list #annotations.
// function addAnnotationItem(name, description, deleteCallback) {
function addAnnotationItem(name, description) {
let item = undefined;
if (name) {
item = document.createElement("li");
const nameItem = document.createElement("strong");
nameItem.appendChild(document.createTextNode(name));
item.appendChild(nameItem);
} else {
return;
}
if (description) {
const descriptionItem = document.createElement("span");
descriptionItem.appendChild(document.createTextNode(description));
item.appendChild(descriptionItem);
}
const link = document.createElement("a");
link.href = `?delete=${name}`;
link.appendChild(document.createTextNode("( X )"));
link.onclick = (e) => {
deleteAnnotation(name);
e.preventDefault();
};
item.appendChild(link);
document.getElementById("annotations").appendChild(item);
}
function deleteAnnotation(name) {
api.vrAnnotationService
.findAnnotation(name)
.then((a) => api.vrAnnotationService.deleteAnnotation(a));
}
// Delete all annotations in the list
function deleteAnnotationItems() {
const list = document.getElementById("annotations");
list.textContent = "";
}
// -- PART 2.2 --
// Loads annotations and adds annotation names to the unordered list #annotations.
function loadAnnotations() {
deleteAnnotationItems();
api.vrAnnotationService
.getAnnotations()
.then((annotations) => {
if (annotations.length > 0) {
annotations.forEach(async (a) => {
const name = await a.getName();
const description = await a.getText();
addAnnotationItem(name, description);
});
} else {
addAnnotationItem("No annotations found.");
}
})
.then(() => addEventMessage("Annotations loaded successfully"))
.catch(() => addEventMessage("Loading annotations failed"));
}
// -- END: PART 2.2 --
// -- PART 2.1 --
// Add event listeners
api.vrAnnotationService.annotationsAdded.connect(() => {
loadAnnotations();
});
api.vrAnnotationService.annotationsDeleted.connect(() => {
loadAnnotations();
});
// Load annotations
loadAnnotations();
// -- END: PART 2.1 --