Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
build
Dockerfile
output.png
19 changes: 19 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Build inside Docker

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Build Docker image
run: docker build .
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
build
.vscode
*.png
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ target_link_libraries(${PROJECT_NAME} PRIVATE
Qt6::Quick
Qt6::Widgets
)

install(TARGETS ${PROJECT_NAME} DESTINATION bin)
21 changes: 21 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
FROM ubuntu:latest

RUN apt update && \
apt install -y \
xvfb mesa-utils \
cmake \
build-essential \
qt6-declarative-dev \
qml6-module-qtquick \
qml6-module-qtquick-controls \
qml6-module-qtqml-workerscript \
qml6-module-qtquick-templates

WORKDIR /app
COPY . .
RUN cmake -B build -S .
RUN cmake --build build --parallel --target install

COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
CMD ["/entrypoint.sh"]
31 changes: 27 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,36 @@
# ![status](https://github.com/krjakbrjak/offscreen_rendering/actions/workflows/build.yml/badge.svg)

# Offscreen Rendering

Rendering into a custom framebuffer, also known as off-screen rendering or rendering to a texture, plays a crucial role in numerous scenarios within computer graphics and game development. This repo is just an example that demonstrates how this can be effortlessly achieved using the Qt framework (version 6.6 is used).
Rendering into a custom framebuffer, also known as off-screen rendering or rendering to a texture, plays a crucial role in numerous scenarios within computer graphics and game development. This repo is just an example that demonstrates how this can be achieved using the Qt framework (version 6).

![main.qml](screenshot.png)
![Screenshot of Qt offscreen rendering example](screenshot.png)

## Build and run

```bash
cmake -B build -S .
cmake --build build --parallel
./build/offscreen_sample
```
```

## Dependencies

This project uses Qt (Qt Quick, Qt QML, Qt OpenGL). The required Ubuntu packages for Qt and other dependencies are listed in the Dockerfile included in this repository.

## Usage

When running inside Docker, the output image will be written to a mounted folder. Specify the output path using the `--output` or `-o` CLI argument:

```sh
./build/offscreen_sample --output /output/image.png
```

Make sure to mount a host directory to `/output` in your Docker container to access the generated image.

## Example Docker run (headless mode)

```sh
docker run --rm -u $(id -u):$(id -g) -v $(pwd)/output:/output $(docker build . -q)
```

This will save the rendered image to the `output` folder on your host machine.
13 changes: 13 additions & 0 deletions entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env bash

pkill Xvfb || true

Xvfb :99 -screen 0 1024x768x24 2>/dev/null &
XVFB_PID=$!
export DISPLAY=:99
export QT_OPENGL=desktop

# Trap all common signals to ensure Xvfb is killed
trap "kill $XVFB_PID" EXIT SIGHUP SIGINT SIGTERM SIGQUIT

QT_QPA_PLATFORM=xcb offscreen_sample -o /output/image.png
35 changes: 24 additions & 11 deletions main.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <QApplication>
#include <QLabel>
#include <QCommandLineOption>
#include <QCommandLineParser>
#include <QOffscreenSurface>
#include <QOpenGLContext>
#include <QOpenGLFramebufferObject>
Expand All @@ -14,11 +15,29 @@
#include <QSurfaceFormat>

int main(int argc, char **argv) {
QApplication app{argc, argv};

QGuiApplication app(argc, argv);

QCommandLineParser parser;
parser.setApplicationDescription("Offscreen Qt Quick Renderer");
parser.addHelpOption();
QCommandLineOption outputOption({"o", "output"}, "Path to output image",
"output");
parser.addOption(outputOption);
parser.process(app);

QString outputPath = parser.value(outputOption);
if (outputPath.isEmpty()) {
qCritical() << "No output path specified. Use --output <path> or -o <path>";
return 1;
}
QSurfaceFormat format;
format.setMajorVersion(4);
format.setMinorVersion(6);
if (QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGL) {
format.setRenderableType(QSurfaceFormat::OpenGL);
} else {
format.setRenderableType(QSurfaceFormat::OpenGLES);
}

QOpenGLContext gl_ctx;
gl_ctx.setFormat(format);
Expand Down Expand Up @@ -61,12 +80,6 @@ int main(int argc, char **argv) {
auto tg = QQuickRenderTarget::fromOpenGLTexture(fb.texture(), fb.size());
window.setRenderTarget(tg);

// Label that displays the content of the custom framebuffer.
QLabel label;
label.setFixedWidth(fb.size().width());
label.setFixedHeight(fb.size().height());
label.show();

QObject::connect(
&control, &QQuickRenderControl::sceneChanged, &control,
[&] {
Expand All @@ -76,8 +89,8 @@ int main(int argc, char **argv) {
control.render();
control.endFrame();

// To simplify, just display the image.
label.setPixmap(QPixmap::fromImage(fb.toImage()));
// To simplify, just save the image.
fb.toImage().save(outputPath);
},
Qt::QueuedConnection);

Expand Down