Skip to content

Commit

Permalink
Merge pull request #80 from zbanks/light-output-node
Browse files Browse the repository at this point in the history
Light output node
  • Loading branch information
zbanks authored Nov 1, 2018
2 parents 2b947cf + 03704ca commit ee54e2e
Show file tree
Hide file tree
Showing 16 changed files with 1,470 additions and 39 deletions.
4 changes: 3 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ list(APPEND libradiance_SOURCES
src/GraphicalDisplay.cpp
src/ImageNode.cpp
src/Library.cpp
src/LightOutputNode.cpp
src/Model.cpp
src/OpenGLUtils.cpp
src/OpenGLWorker.cpp
Expand Down Expand Up @@ -119,7 +120,8 @@ add_executable(radiance WIN32 src/main.cpp
src/GlslDocument.cpp
src/GlslHighlighter.cpp
src/QQuickVideoNodePreview.cpp
src/QQuickPreviewAdapter.cpp)
src/QQuickPreviewAdapter.cpp
src/QQuickLightOutputPreview.cpp)

set_target_properties(radiance PROPERTIES OUTPUT_NAME ${EXE_NAME})
set_target_properties(libradiance PROPERTIES OUTPUT_NAME ${LIB_NAME})
Expand Down
177 changes: 177 additions & 0 deletions light_output.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# Outputting to lights from Radiance
Radiance provides a simple way to output to light displays.
If you implement this protocol on your device or middleware,
then all you have to do is instantiate a LightOutputNode and point it to your device.
This document describes the communication protocol between your device and Radiance.

## Example device
The file [test_server.py](support/test_server.py) implements a simple device.
You should use it as a reference for implementing your own devices.

To try it out, run it in the background. Then, open Radiance and create a new `LightOutputNode` pointing at `localhost`.
Add a full-screen pattern such as `purple` before the `LightOutputNode` to fill the display with color.
You should see a circle of lights that correspond to the edges of the frame.

If you restart the server and want to reconnect, simply select the `LightOutputNode` and hit `R`.

## Nuts and bolts
* Your device should bind a TCP port. Radiance will connect.
* Radiance defaults to port 9001 if no port is specified.
* All messages are asynchronous and may be sent at any time.
* Unknown messages should be ignored.
* All values are little-endian unless otherwise noted.
* If an unrecoverable error occurs, the connection should be terminated.
* No state is stored between connections, so you will have to re-issue setup messages after a disconnect.

## Message format
<table><tr>
<td>Length (4 bytes)</td>
<td>Command (1 byte)</td>
<td>Data (length - 1 bytes)</td>
</tr></table>

## Messages

**Important note:** Only commands 0-5 are implemented right now. Commands 6-9 will be implemented in the future.

<table>
<tr>
<th>byte</th>
<th>Command</th>
<th>Sender</th>
<th>Data</th>
<th>Description</th>
<th>Default if not sent</th>
</tr>
<tr>
<td>0</td>
<td>Description</td>
<td>Device</td>
<td>JSON</td>
<td>Device returns a high level description of itself in JSON format. See below</td>
<td>See below</td>
</tr>
<tr>
<td>1</td>
<td>Get frame</td>
<td>Device</td>
<td>uint32</td>
<td>Ask Radiance for a frame. Data is a uint32 that sets the frame period in milliseconds. Request 0 ms for a single frame or to stop a previous request.</td>
<td>No frames will be sent</td>
</tr>
<tr>
<td>2</td>
<td>Frame</td>
<td>Radiance</td>
<td>RGBA data (32 bits total)</td>
<td>Radiance returns a pixel color in RGBA format for every location requested.</td>
<td>N/A</td>
</tr>
<tr>
<td>3</td>
<td>Lookup coordinates 2D</td>
<td>Device</td>
<td>Array of {float u, float v}</td>
<td>A list of pixel coordinates in uv space to lookup and return in "frame" messages. Must be sent before "get frame"</td>
<td>No pixels will be returned in a frame unless a "lookup coordinates 2D" or "lookup coordinates 3D" command is sent.</td>
</tr>
<tr>
<td>4</td>
<td>Physical coordinates 2D</td>
<td>Device</td>
<td>Array of {float u, float v}</td>
<td>A list of physical coordinates (in UV space.) Used purely for visualization in radiance.</td>
<td>Radiance will visualize pixel locations using the lookup coordinates.</td>
</tr>
<tr>
<td>5</td>
<td>Geometry 2D</td>
<td>Device</td>
<td>PNG image data</td>
<td>A PNG image that will be used as a background for visualization in radiance.</td>
<td>Radiance will visualize pixel locations against a transparent background.</td>
</tr>
<tr>
<td>6</td>
<td>Lookup coordinates 3D</td>
<td>Device</td>
<td>Array of {float t, float u, float v}</td>
<td>A list of pixel coordinates in tuv space to lookup and return in “frame” messages. Must be sent before “get frame”</td>
<td>No pixels will be returned in a frame unless a “lookup coordinates 2D” or “lookup coordinates 3D” command is sent.</td>
</tr>
<tr>
<td>7</td>
<td>Physical coordinates 3D</td>
<td>Device</td>
<td>Array of {float x, float y, float z}</td>
<td>A list of physical coordinates (in the STL’s space.) Used purely for visualization in radiance. If not sent, radiance will visualize pixel locations using the 3D lookup coordinates.</td>
<td>Radiance will visualize pixel locations using the lookup coordinates.</td>
</tr>
<tr>
<td>8</td>
<td>Geometry 3D</td>
<td>Device</td>
<td>STL data</td>
<td>A STL file that will be used as a background for visualization in radiance.</td>
<td>Radiance will visualize pixel locations against a transparent background.</td>
</tr>
<tr>
<td>9</td>
<td>tuv map</td>
<td>Device</td>
<td>GLSL shader program</td>
<td>A file containing one or more shader programs that convert tuv coordinates to uv coordinates for sampling.</td>
<td>Radiance will use a set of presets.</td>
</tr>
</table>

### Description keys

<table>
<tr>
<th>Key name</th>
<th>Value</th>
<th>Description</th>
<th>Default if not set</th>
<th>Notes</th>
</tr>
<tr>
<td>name</td>
<td>string</td>
<td>What to call this device</td>
<td>unnamed</td>
<td></td>
</tr>
<tr>
<td>size</td>
<td>[width, height] or single number for width & height</td>
<td>How big the canvas should be on which the points are sampled</td>
<td>300x300</td>
<td>all UV values are [0, 1] despite aspect ratio</td>
</tr>
</table>

### Typical conversation

A minimal example:
* **Radiance**: connects to your device on port 9001
* **Device**: sends `lookup coordinates 2D` command with 5 pixel coordinates in UV space
* **Device**: sends `get frame` command with `0` ms period
* **Radiance**: sends `frame` command with 5 pixel colors
* **Device**: sends `get frame` command with `0` ms period
* **Radiance**: sends `frame` command with 5 pixel colors
* etc.

A more fully featured example:
* **Radiance**: connects to your device on port 9001
* **Device**: sends `lookup coordinates 2D` command with pixel coordinates in UV space indicating how they should be sampled
* **Device**: sends `physical coordinates 2D` command with pixel coordinates in UV space indicating how they should be visualized in Radiance
* **Device**: sends `geometry 2D` command with a PNG image to serve as a background in Radiance
* **Device**: sends `get frame` command with `10` ms period
* **Radiance**: sends `frame` command with pixel colors
* 10 ms later, **Radiance**: sends `frame` command with pixel colors
* 10 ms later, **Radiance**: sends `frame` command with pixel colors
* etc.

Note that you may update the lookup coordinates, the physical coordinates, and the background image at any time.
This is useful for outputting to interactive displays.
47 changes: 47 additions & 0 deletions resources/qml/BusyBrokenIndicator.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import QtQuick 2.7
import QtQuick.Controls 1.4
import radiance 1.0
import "."

Item {
property VideoNode videoNode;

BusyIndicator {
id: busy
anchors.fill: parent
anchors.margins: 10
opacity: videoNode && videoNode.nodeState == VideoNode.Loading ? 1 : 0
Behavior on opacity {
NumberAnimation {
easing {
type: Easing.InOutQuad
amplitude: 1.0
period: 0.5
}
duration: 500
}
}
}
Rectangle {
anchors.fill: parent
color: RadianceStyle.tileLineColor
opacity: videoNode && videoNode.nodeState == VideoNode.Broken ? 0.9 : 0
Behavior on opacity {
NumberAnimation {
easing {
type: Easing.InOutQuad
amplitude: 1.0
period: 0.5
}
duration: 500
}
}
Text {
id: bang
text: "!"
font.pixelSize: parent.height * 0.9
color: RadianceStyle.accent
anchors.centerIn: parent
}
}
}
38 changes: 2 additions & 36 deletions resources/qml/CheckerboardPreview.qml
Original file line number Diff line number Diff line change
Expand Up @@ -24,43 +24,9 @@ Item {
anchors.fill: parent
previewAdapter: Globals.previewAdapter
}
BusyIndicator {
id: busy
BusyBrokenIndicator {
anchors.fill: parent
anchors.margins: 10
opacity: vnr.videoNode && vnr.videoNode.nodeState == VideoNode.Loading ? 1 : 0
Behavior on opacity {
NumberAnimation {
easing {
type: Easing.InOutQuad
amplitude: 1.0
period: 0.5
}
duration: 500
}
}
}
Rectangle {
anchors.fill: parent
color: RadianceStyle.tileLineColor
opacity: vnr.videoNode && vnr.videoNode.nodeState == VideoNode.Broken ? 0.9 : 0
Behavior on opacity {
NumberAnimation {
easing {
type: Easing.InOutQuad
amplitude: 1.0
period: 0.5
}
duration: 500
}
}
Text {
id: bang
text: "!"
font.pixelSize: parent.height * 0.9
color: RadianceStyle.accent
anchors.centerIn: parent
}
videoNode: vnr.videoNode
}
}
}
Expand Down
1 change: 1 addition & 0 deletions resources/qml/Graph.qml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Item {
"ScreenOutputNode": "ScreenOutputNodeTile",
"FFmpegOutputNode": "FFmpegOutputNodeTile",
"PlaceholderNode": "PlaceholderNodeTile",
"LightOutputNode": "LightOutputNodeTile",
"": "VideoNodeTile"
}
x: (parent.width - width) / 2
Expand Down
39 changes: 39 additions & 0 deletions resources/qml/Instantiators/LightOutputInstantiator.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import QtQuick 2.3
import QtQuick.Controls 1.2
import QtQuick.Dialogs 1.2
import QtQuick.Layouts 1.3

Dialog {
visible: true
title: "Connect to light output device"
standardButtons: StandardButton.Ok | StandardButton.Cancel

onAccepted: {
var vn = registry.deserialize(context, JSON.stringify({
type: "LightOutputNode",
url: textbox.text,
}));
if (vn) {
graph.insertVideoNode(vn);
} else {
console.log("Could not instantiate LightOutputNode");
}
}

ColumnLayout {
anchors.fill: parent

Label {
text: "Enter device URL:"
}
TextField {
id: textbox
Layout.fillWidth: true
text: "localhost"

Component.onCompleted: {
textbox.forceActiveFocus();
}
}
}
}
50 changes: 50 additions & 0 deletions resources/qml/LightOutputNodeTile.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import QtQuick 2.7
import QtQuick.Layouts 1.2
import QtQuick.Controls 2.2
import radiance 1.0
import "."

VideoNodeTile {
id: tile;

normalHeight: 260;
normalWidth: 200;

ColumnLayout {
anchors.fill: parent;
anchors.leftMargin: 10
anchors.rightMargin: 10
anchors.bottomMargin: 5
anchors.topMargin: 5

RadianceTileTitle {
Layout.fillWidth: true
text: tile.videoNode.name ? tile.videoNode.name : "Light Output"
}

Item {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.margins: 5
layer.enabled: true

LightOutputPreview {
id: vnr
videoNode: tile.videoNode
anchors.fill: parent
}
BusyBrokenIndicator {
anchors.fill: parent
videoNode: vnr.videoNode
}
}
}
Keys.onPressed: {
if (event.modifiers == Qt.NoModifier) {
if (event.key == Qt.Key_R) {
videoNode.reload();
reloaded();
}
}
}
}
4 changes: 2 additions & 2 deletions src/EffectNode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ void EffectNode::attachSignals() {

QJsonObject EffectNode::serialize() {
QJsonObject o = VideoNode::serialize();
o.insert("file", d()->m_file);
o.insert("intensity", d()->m_intensity);
o.insert("file", file());
o.insert("intensity", intensity());
return o;
}

Expand Down
Loading

0 comments on commit ee54e2e

Please sign in to comment.