教程 6:如何使用 VRED Core Web API

了解如何使用 VRED Core Web API。

下载示例脚本

跳转到脚本示例

视频字幕:大家好,欢迎学习最后一个面向 VRED Core 的教程视频。我是主持人 Christopher,今天我们将扩展上一个视频中的流式传输应用程序示例,并使用 VRED 的 Web API 实现更多功能。

借助 Python API v2,Autodesk 还引入了 Python Web API,该 API 是 Python 端点的直接换代产品,可用于将 Python 命令发送到 VRED。我们在上一个视频中使用了 Python 端点,在那种情况下您可以构建自己的面向 VRED Core 的 HTML 界面,以便将 Python 命令发送到 VRED,并实现我们的视频流。

例如,我们可以将任意 Python 命令发送到 Python 端点,并接收此操作的结果作为响应。这些端点的缺点是,您必须发送 Python 命令的字符串表示形式,而不是使用对象或渲染的界面,并且必须正确传递返回值,才能在 JavaScript 应用程序中使用它们。

借助新的 Web API,VRED 提供了完全集成的 JavaScript 模块,其作用相当于直接连接到 VRED 的 Python 界面。Web API 提供了一个面向对象的访问点,可以访问 Python API v2 中提供的所有功能。这意味着,您可以访问适当的 JavaScript 类型,并可以使用典型的 JavaScript 功能,例如 Promise 或错误处理。

举例来说,可以使用这样的 JavaScript 调用创建一个新的摄影机,并等待您刚刚创建的摄影机。然后,可以使用该摄影机对象来设置其名称、设置其活动状态以便以其他方式进行处理。当然,摄影机对象包含 Python 类 vrdCamera 中可用的所有函数。

我们来了解一下如何在我们自己的应用程序中使用新的 Web API。在本教程中,我们将在上一个视频的自定义 HTML 应用程序基础上集成一个标注管理器。如果您没有看过上一个视频,建议您先看一下,因为它会直接扩展本教程的这段代码。

好了。我们开始吧。

在上一个教程中,我们将所有 CSS 和 JavaScript 代码都放在 HTML 文件中。为了保持整洁,我将样式表和脚本移到单独的文件中。这样,我们的 HTML 就更简洁了。

首先,我想添加一种向场景中添加标注的方法。对于本教程,我们不想设置这些标注的位置,只是添加它们并为其指定名称和描述即可。为此,我们可以向 HTML 代码添加一个表单,其中包含名称和描述的 2 个输入,以及一个用于创建标注的提交按钮。我们可以将其封装在一个 div 容器中,使用 CSS 为其指定一些样式,然后将其放在我们的视频流上。这次,在左上角。

接下来,我们创建一个名为 webapiscript 的新 JavaScript 文件,该文件将包含用于标注管理器的 JavaScript 代码。可以使用此 import 语句将 VRED Core 的 JavaScript API 添加到 JavaScript 模块。如您所见,在我的示例中,我使用的是 localhost 以及端口 8888。如果 VRED Core 的设置不同,则必须调整这些值。

首先,我将添加一个新函数 addAnnotation,其参数为 namedescription。在该函数中,我直接调用我们导入的 VRED JavaScript API。如您所见,我们使用此 API 的方式与 Python API 非常相似。

在这里,我使用的是 vrAnnotation 服务器,并调用带有 name 参数的方法 createAnnotation。这将返回我们使用 then 函数异步处理的 Promise 对象。如果您不熟悉 JavaScript 模式,我来介绍一下,它被称为 async/await 模式,是一种处理编程并发性的最新方法。createAnnotation 一般需要一些时间才能返回,因此我们等待它完成,然后执行下一个操作。这样,脚本就可以继续工作,在等待 VRED 时不会锁定。

and then 函数内,我们现在可以安全地调用 findAnnotation 函数以获取对新标注节点的引用。我们遵循相同的 async/await 模式,将新标注的文本设置为 description 参数。如果创建标注时出错,我们可以捕获此错误并通知用户。在此示例中,我只是将错误消息输出到 Python 终端。

添加标注现在应该可以正常工作,但我们仍需将其连接到我们创建的 HTML 表单。要执行此操作,我们将挂钩表单的 onsubmit 回调。在这里,我们可以提取名称和描述输入,并调用 addAnnotation 函数。当测试新功能时,可以看到一切正常,我们可以向场景中添加新标注。还不错!

我还想在场景中添加标注列表,并且能够删除标注。现在,在 HTML 中,我们只需添加一个带有 ID 的无序列表元素。这足以动态地在此列表中添加和移除标注。

在 JavaScript 脚本中,我们从一个将加载所有标注的函数开始。使用 vrAnnotationService 获取所有标注的列表。使用 async/await 模式时,等待 VRED 中所有标注的列表,然后逐个进行迭代。

对于每个标注,我们查询了一个名称和所包含的文本。这也可以通过 async/await 模式完成。这就是我们必须将 loop 函数标记为 async 的原因。使用名称和标注文本,我们可以调用稍后将实现的名为 addAnnotationItem 的第二个函数。

但首先,我们要在每次调用 loadAnnotation 函数时清除列表。为此,我们添加一个 deleteAnnotationItems 函数,用于清除所有列表中的所有项,并在 load 函数的开头调用它。然后,我们创建一个名为 addAnnotationItem 的新函数,参数为 namedescription

在这里,我们创建一个新的 HTML 列表元素,用一个文本节点填充该元素,该节点将保存标注的名称和描述。如果名称未定义,则从该函数返回。

我们还想添加一个按钮,用于删除 VRED 中的标注。为此,我们可以添加一个指向标注的链接。我们需要将某些内容设置为链接属性,才能看到链接。但是,这不起作用。相反,我们挂钩到 onClick 函数,并使用 AnnotationService 删除此标注(如果已单击)。

我们在此处使用的也是 async/await 模式。最后,我们将标注元素附加到列表。现在,当我们加载流式传输应用程序时,标注列表将加载一次,但永远不会更新。为了使我们的应用程序真正具有动态性,可以使用来自 vrAnnotationService 的信号,这些信号会告诉我们何时添加或删除了标注。这与在 Python 界面中使用信号非常相似。

就是这样。当测试该应用程序时,我们可以添加新标注,并立即在标注列表中看到它们。还有一个“删除”按钮,用于从场景中移除标注。

我承认,所有标注都添加到同一位置,但这可能是另一个教程的任务。现在,您应该全面了解了有关使用 VRED 的 Web API 的信息,以及如何使用它构建动态交互式 HTML 应用程序来控制场景。本视频也结束了 VRED Core 教程系列。我真的希望我能帮助您快速入门 VRED Core,并让您了解扩展和使用本产品的各种可能性。

感谢您参加本课程,下次见。


Python 代码示例

下面是教程 6:如何使用 VRED Core Web API 视频随附的示例 Python 脚本。

提示:

要下载这些压缩文件,请单击此处

camera-example.html

<!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>

myapp-final.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>
        <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>

myapp.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>

script.js

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();
}

styles.css

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;
}

web-api-script.js

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 --