一、实现步骤
1. 前提条件:安装 NVIDIA 驱动(无桌面版)
首先要确保 Linux 系统正确安装了 NVIDIA 显卡驱动(不是桌面版的 nouveau 开源驱动),且驱动支持 Egl/GLX 离屏渲染。
c
# 1. 卸载原有开源驱动(若有)
sudo apt purge nvidia* nouveau-firmware
sudo apt autoremove
# 2. 添加NVIDIA官方源(以Ubuntu 22.04为例)
sudo apt install -y software-properties-common
sudo add-apt-repository ppa:graphics-drivers/ppa
sudo apt update
# 3. 安装对应版本的NVIDIA驱动(推荐470+以上版本)
# 先查看适配的驱动版本:ubuntu-drivers devices
sudo apt install -y nvidia-driver-550 nvidia-utils-550
# 4. 验证驱动是否安装成功(无桌面也能执行)
nvidia-smi # 能输出显卡信息即表示驱动正常
# 5.安装 OpenGL/EGL 开发库
sudo apt install libegl1-mesa-dev libgles2-mesa-dev libopengl-dev
如果使用vulkan + unity
c
# 安装基础 OpenGL 库(即使无桌面)
sudo apt install libgl1 libegl1 libvulkan1
# 赋予执行权限
chmod +x YourGame.x86_64
# 后台运行
sudo nohup ./YourGame.x86_64 -batchmode -screen-width 1920 -screen-height 960 > unity.log 2>&1 &
2. Unity 程序启动参数
无需 xvfb 虚拟驱动,直接通过 NVIDIA 的 EGL/GLX 离屏渲染上下文启动 Unity 程序
c
#无桌面Linux + NVIDIA显卡启动Unity程序(启用GPU渲染,无物理窗口)
./YourGame.x86_64 -batchmode -no-window -force-vulkan -gpu-device-id 0
./YourGame.x86_64 -batchmode -screen-width 1920 -screen-height 960`
参数说明:
-batchmode:批处理模式,禁用交互;
-no-window:禁用物理窗口(核心!只保留 GPU 离屏渲染);
-force-vulkan:强制使用 Vulkan API(NVIDIA 对 Vulkan 离屏渲染支持更好,也可改用-force-glcore);
-gpu-device-id 0:指定使用第 0 块 NVIDIA 显卡(多显卡场景必备)。
即使没有 X11/Wayland,只要 NVIDIA 驱动支持 EGL,Unity 也能创建 OpenGL 上下文进行离屏渲染。
3. Unity 代码实现 RenderTexture 渲染(和普通环境一致)
c#
using UnityEngine;
using System.IO;
public class NVIDIAOffscreenRender : MonoBehaviour
{
public Camera renderCamera;
public int width = 1920;
public int height = 1080;
private RenderTexture rt;
void Start()
{
// 1. 初始化RenderTexture(底层是NVIDIA显卡的FBO)
rt = new RenderTexture(width, height, 24, RenderTextureFormat.ARGB32);
rt.enableRandomWrite = true; // 启用随机读写(NVIDIA显卡优化)
rt.Create(); // 显式创建FBO(无桌面环境建议显式调用)
// 2. 将相机渲染目标指向RenderTexture
renderCamera.targetTexture = rt;
// 3. 执行渲染并提取图像数据
CaptureFrame();
}
void CaptureFrame()
{
// 强制相机渲染一帧到FBO(RenderTexture)
renderCamera.Render();
// 4. 读取FBO中的像素数据到CPU
RenderTexture.active = rt;
Texture2D tex = new Texture2D(width, height, TextureFormat.ARGB32, false);
tex.ReadPixels(new Rect(0, 0, width, height), 0, 0);
tex.Apply();
// 5. 输出图像(保存为文件/网络传输/其他处理)
byte[] pngData = tex.EncodeToPNG();
string savePath = Path.Combine(Application.persistentDataPath, "nvidia_capture.png");
File.WriteAllBytes(savePath, pngData);
Debug.Log($"图像已保存到:{savePath}");
// 6. 清理资源
RenderTexture.active = null;
Destroy(tex);
rt.Release(); // 释放FBO资源
}
}
//////////////////////////////////////////////////////////////
//第二个例子
using UnityEngine;
using UnityEngine.Rendering;
using System.Threading.Tasks;
public class HeadlessRenderer : MonoBehaviour
{
public int width = 1920;
public int height = 960;
private RenderTexture renderTex;
private Camera renderCam;
async void Start()
{
// 创建离屏渲染目标
renderTex = new RenderTexture(width, height, 24, RenderTextureFormat.ARGB32);
renderTex.Create();
// 创建专用相机(或复用主相机)
GameObject camObj = new GameObject("RenderCamera");
renderCam = camObj.AddComponent<Camera>();
renderCam.targetTexture = renderTex;
renderCam.clearFlags = CameraClearFlags.Color;
renderCam.backgroundColor = Color.blue;
// 示例:每秒渲染一帧并保存
while (true)
{
RenderPanorama();
byte[] jpg = await CaptureAsJPG(renderTex);
System.IO.File.WriteAllBytes("/tmp/frame_" + Time.frameCount + ".jpg", jpg);
await Task.Delay(1000); // 1 FPS
}
}
void RenderPanorama()
{
// 在这里执行你的拼接逻辑:
// - 设置 Projector
// - 绘制鱼眼矫正后的网格
// - 渲染球面全景等
renderCam.Render();
}
async Task<byte[]> CaptureAsJPG(RenderTexture rt)
{
var request = AsyncGPUReadback.Request(rt);
while (!request.done)
await Task.Yield();
if (request.hasError)
{
Debug.LogError("GPU readback failed");
return null;
}
// 转为 Texture2D 并编码
Texture2D tex = new Texture2D(rt.width, rt.height, TextureFormat.RGB24, false);
tex.LoadRawTextureData(request.GetData<byte>());
tex.Apply();
byte[] jpg = tex.EncodeToJPG(90);
Object.Destroy(tex);
return jpg;
}
void OnDestroy()
{
if (renderTex != null) renderTex.Release();
}
}
权限
sudo chmod +x YourGame.x86_64 && sudo ./YourGame.x86_64
unity下别忘了挂载脚本
二、egl验证
使用egl去验证无头模式
c
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES3/gl32.h>
#include <iostream>
#include <vector>
#include <opencv2/opencv.hpp>
// EGL 函数指针(动态加载)
PFNEGLQUERYDEVICESEXTPROC eglQueryDevicesEXT = nullptr;
PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXT = nullptr;
bool initEGL(EGLDisplay& display, EGLContext& context) {
// 获取函数指针
eglQueryDevicesEXT = (PFNEGLQUERYDEVICESEXTPROC) eglGetProcAddress("eglQueryDevicesEXT");
eglGetPlatformDisplayEXT = (PFNEGLGETPLATFORMDISPLAYEXTPROC) eglGetProcAddress("eglGetPlatformDisplayEXT");
if (!eglQueryDevicesExtensions || !eglGetPlatformDisplayEXT) {
std::cerr << "EGL EXT not supported\n";
return false;
}
// 枚举 GPU 设备
EGLDeviceEXT devices[4];
EGLint numDevices;
if (!eglQueryDevicesEXT(4, devices, &numDevices) || numDevices == 0) {
std::cerr << "No EGL devices found\n";
return false;
}
// 使用第一个 NVIDIA GPU
display = eglGetPlatformDisplayEXT(EGL_PLATFORM_DEVICE_EXT, devices[0], nullptr);
if (display == EGL_NO_DISPLAY) {
std::cerr << "Failed to get EGL display\n";
return false;
}
if (!eglInitialize(display, nullptr, nullptr)) {
std::cerr << "eglInitialize failed\n";
return false;
}
// 选择配置
EGLint configAttribs[] = {
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
EGL_NONE
};
EGLConfig config;
EGLint numConfigs;
eglChooseConfig(display, configAttribs, &config, 1, &numConfigs);
// 创建 context(无 surface)
EGLint contextAttribs[] = {
EGL_CONTEXT_CLIENT_VERSION, 3,
EGL_NONE
};
context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs);
if (context == EGL_NO_CONTEXT) {
std::cerr << "Failed to create EGL context\n";
return false;
}
if (!eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, context)) {
std::cerr << "eglMakeCurrent failed\n";
return false;
}
return true;
}
GLuint createFBO(int width, int height, GLuint& colorTex, GLuint& depthRb) {
// 创建颜色纹理
glGenTextures(1, &colorTex);
glBindTexture(GL_TEXTURE_2D, colorTex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 创建深度渲染缓冲
glGenRenderbuffers(1, &depthRb);
glBindRenderbuffer(GL_RENDERBUFFER, depthRb);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, width, height);
// 创建 FBO
GLuint fbo;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTex, 0);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRb);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
std::cerr << "FBO incomplete\n";
return 0;
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
return fbo;
}
void renderToTexture(int width, int height) {
// 创建 FBO
GLuint colorTex, depthRb, fbo = createFBO(width, height, colorTex, depthRb);
// 绑定 FBO 并渲染
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glViewport(0, 0, width, height);
glClearColor(0.2f, 0.3f, 0.8f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// TODO: 在这里执行你的拼接渲染逻辑(如 Projector、球面映射等)
// 例如:绘制两个 warp 后的纹理到球面
// 读回像素
std::vector<unsigned char> pixels(width * height * 4);
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
// 转为 OpenCV Mat(BGR) 效率不高,注意只是测试,不要使用这类代码
//实际解码的时候就要直接倒过来,也不要RGBA
cv::Mat img(height, width, CV_8UC4, pixels.data());
cv::Mat bgr;
cvtColor(img, bgr, cv::COLOR_RGBA2BGR);
cv::flip(bgr, bgr, 0); // OpenGL 是 bottom-up
// 保存或送入 FFmpeg
cv::imwrite("rendered_panorama.jpg", bgr);
std::cout << "Saved rendered image.\n";
// 清理
glDeleteFramebuffers(1, &fbo);
glDeleteTextures(1, &colorTex);
glDeleteRenderbuffers(1, &depthRb);
}
int main() {
EGLDisplay display;
EGLContext context;
if (!initEGL(display, context)) {
return -1;
}
std::cout << "EGL initialized. Rendering...\n";
renderToTexture(1920, 960); // 输出 2:1 全景图
eglDestroyContext(display, context);
eglTerminate(display);
return 0;
}
g++ -o headless_render headless_render.cpp \
-lEGL -lGLESv2 -lopencv_core -lopencv_imgcodecs \
-I/usr/include/libdrm
三、备用Xvfb
c
> 安装 Xvfb sudo apt install xvfb
> 启动虚拟显示 Xvfb :99 -screen 0 1920x960x24 &
> 在该显示下运行 Unity DISPLAY=:99 ./YourGame.x86_64 -batchmode