The complete source of the demo shown on the front page. Roughly 700 lines of DominScript that opens an OpenGL window, decodes a video stream with FFmpeg, and renders a spinning textured cube — with every geometric and rendering decision visible in this file.
The OpenGL plugin underneath is a thin glue layer over real GL entry points (gl.Begin, gl.Vertex3f, gl.Frustum, ...). There is no rendering engine hidden behind a high-level façade; what you read here is what runs.
/*
* opengl_ffmpeg_script_driven_demo.dom — same scene as
* opengl_ffmpeg_callback_demo, but every rendering decision lives in
* the script.
*
* What it does:
* Identical scene to opengl_ffmpeg_callback_demo: 1280x720 window,
* spinning textured cube, four side faces show video frames, top
* and bottom faces show the project logo, mouse wheel zoom and
* left-drag rotate work the same way.
*
* The difference is *where* the rendering logic lives. The original
* demo calls into a thick gl.* facade (DrawCube, DrawFace,
* DrawBackground, BeginFrame, ApplyRotation, ...) and the plugin
* hides the OpenGL state machine behind those names. Here, the
* plugin is reduced to a thin glue layer over real GL entry points
* (gl.Begin, gl.Vertex3f, gl.Color3f, gl.Frustum, gl.Rotatef, ...)
* and every drawing decision -- geometry, edges, gradients, matrix
* setup, rotation state -- is written out in this file.
*
* If you want to learn how the cube is built, you read this file.
* There is nowhere else to hide it.
*
* State sharing between the main loop and the callbacks (rev31zzng):
* The interactive state (RenderState) lives in the CALLBACK world as a
* `callback global blob $State` -- the world where the mouse/resize
* callback (OnOpenGlEvent) and the frame callback (OnVideoFrame)
* run, so they read and write it directly. The main world never
* names it: once per frame main calls into the callback world with
* worker.Call("Tick", ref $Snapshot) -- the callback world advances
* the time-based rotation and writes the whole state into the lent
* blob, and main renders from that snapshot. At startup the same
* crossing runs the other way: worker.Call("Configure", ref $Init)
* seeds the callback-world state with the texture slots and the
* initial values. Two worlds, one call per frame, no shared
* pointer, no lock -- the language's own model (reference 5.2b and
* Examples/global_two_worlds_demo.dom).
* (Until rev31zzng this demo used the script_state plugin's raw
* buffer mapped by pointer from both sides.)
*
* Usage:
* ./domin Examples/opengl_ffmpeg_script_driven_demo.dom [flags]
*
* --help Show this help message and exit.
*
* Required runtime:
* A display server reachable from the process:
* - Linux: X11 or Wayland with a valid XDG_RUNTIME_DIR.
* - Windows: a desktop session; SDL2.dll in runtime/ (see
* runtime/README.txt).
* Built plugins: opengl, ffmpeg, sdl2_image, path, timer, worker.
*
* Graceful skip:
* Same two-layered SKIP behavior as the callback demo: env pre-check
* before gl.Open, plus gl.Open's return value as a backstop.
*
* Expected output:
* - Display available: opens a window, shows the spinning cube,
* returns 0 on close. (No stdout output in normal operation.)
* - No display: prints "opengl-ffmpeg-script-driven: SKIP -
* display not available" and exits 0.
*/
plugin "builtin:os";
plugin "builtin:struct";
plugin "builtin:print";
plugin "../plugins/worker/WorkerPlugin";
plugin "../plugins/opengl/GlPlugin";
plugin "../plugins/ffmpeg/FfmpegPlugin";
plugin "../plugins/sdl2_image/Sdl2ImagePlugin"; /* rev31zzuw: the logo through the SDL2_image adapter */
plugin "../plugins/path/PathPlugin";
plugin "../plugins/timer/TimerPlugin";
string ProjectAssetPath(string $RelativePath)
{
string $ScriptDir[512];
string $ProjectDir[512];
$ScriptDir = os.ScriptDir();
$ProjectDir = path.Join($ScriptDir, "..");
return(path.Join($ProjectDir, $RelativePath));
}
#define WINDOW_WIDTH 1280
#define WINDOW_HEIGHT 720
#define VIDEO_WIDTH 640
#define VIDEO_HEIGHT 360
#define LOGO_WIDTH 620
#define LOGO_HEIGHT 220
/* GL constants, copied verbatim from <GL/gl.h>. The plugin's gl.*
wrappers take these by value, just like glBegin / glEnable / etc.
in C. Picking them out by name here keeps the script self-explanatory
instead of sprinkling magic hex numbers through gl.Begin / gl.Enable
call sites. */
#define GL_COLOR_BUFFER_BIT 16384 /* 0x4000 */
#define GL_DEPTH_BUFFER_BIT 256 /* 0x0100 */
#define GL_TEXTURE_2D 3553 /* 0x0DE1 */
#define GL_QUADS 7 /* 0x0007 */
#define GL_LINES 1 /* 0x0001 */
#define GL_DEPTH_TEST 2929 /* 0x0B71 */
#define GL_BLEND 3042 /* 0x0BE2 */
#define GL_SRC_ALPHA 770 /* 0x0302 */
#define GL_ONE_MINUS_SRC_ALPHA 771 /* 0x0303 */
#define GL_PROJECTION 5889 /* 0x1701 */
#define GL_MODELVIEW 5888 /* 0x1700 */
#define GL_ALPHA_TEST 3008 /* 0x0BC0 */
#define GL_GREATER 516 /* 0x0204 */
/* Event packet layout -- same as in opengl_ffmpeg_callback_demo.dom. */
#define GL_EVENT_NONE 0
#define GL_EVENT_QUIT 1
#define GL_EVENT_RESIZE 2
#define GL_EVENT_MOUSE_DOWN 3
#define GL_EVENT_MOUSE_UP 4
#define GL_EVENT_MOUSE_MOVE 5
#define GL_EVENT_MOUSE_WHEEL 6
#define GL_MOUSE_LEFT 1
#define FACE_FRONT 0
#define FACE_BACK 1
#define FACE_RIGHT 2
#define FACE_LEFT 3
#define FACE_TOP 4
#define FACE_BOTTOM 5
#define AUTO_SPEED_X 0.23
#define AUTO_SPEED_Y 0.37
#define AUTO_SPEED_Z 0.11
#define CUBE_PANEL_HALF 0.78
/* ----- Shared state struct ------------------------------------------ */
struct RenderState packet
{
i32 WindowWidth;
i32 WindowHeight;
i32 VideoSlot;
i32 LogoSlot;
double CameraDistance;
double ViewScale;
double AngleX;
double AngleY;
double AngleZ;
double SpeedX;
double SpeedY;
double SpeedZ;
double LastAdvanceMs;
double CubeGap;
double EdgeR;
double EdgeG;
double EdgeB;
double EdgeWidth;
};
/* The interactive state, owned by the callback world (dispatcher thread).
Only callback functions touch it; main sees it through Tick's snapshot. */
callback global blob $State[sizeof(RenderState)];
/* main -> callback world, once: seed the state from main's blob. */
callback i32 Configure(ref blob $Init)
{
/* rev31zzvb: one whole-blob copy (reference 13.7) instead of 18 field
assignments -- the two blobs share the RenderState layout. */
$State = $Init;
return 0;
}
/* main -> callback world, once per frame: advance the time-based rotation
and hand back the whole state in the lent blob (the snapshot main
renders from). Same arithmetic as before, same place in the loop. */
callback i32 Tick(ref blob $Snapshot)
{
$State assign RenderState;
/* Time-based rotation (rev31zzeh): the Speed values are degrees
per 40 ms (one 25 fps frame), so the visual speed is identical
at any render rate. The delta is clamped so window drags or
stalls cannot make the cube jump. */
double $NowMs = (double)timer.MonotonicMs();
double $DtMs = 40.0;
if ($State.LastAdvanceMs > 0.0)
{
$DtMs = $NowMs - $State.LastAdvanceMs;
$DtMs = ($DtMs < 0.0) ? 0.0 : ($DtMs > 200.0) ? 200.0 : $DtMs; /* clamp, as ClampCameraDistance */
}
$State.LastAdvanceMs = $NowMs;
$State.AngleX += ($State.SpeedX * ($DtMs / 40.0));
$State.AngleY += ($State.SpeedY * ($DtMs / 40.0));
$State.AngleZ += ($State.SpeedZ * ($DtMs / 40.0));
$Snapshot = $State; /* the whole state, one copy */
return 0;
}
struct GlEvent packet
{
u8 magic0;
u8 magic1;
u8 magic2;
u8 magic3;
u8 version;
u8 type;
u8 button;
u8 pad0;
i16 x;
i16 y;
i16 dx;
i16 dy;
i16 wheel;
u16 width;
u16 height;
};
void GetCubeFaceGeometry(i32 $Face, double $CubeGap,
ref double $Ax, ref double $Ay , ref double $Az,
ref double $Bx, ref double $By , ref double $Bz,
ref double $Cx, ref double $Cy , ref double $Cz,
ref double $Dx, ref double $Dy , ref double $Dz)
{
double $P = CUBE_PANEL_HALF;
double $D = 1.05 + ($CubeGap * 0.40);
if ($Face == FACE_FRONT)
{
$Ax = -$P; $Ay = -$P; $Az = $D;
$Bx = $P; $By = -$P; $Bz = $D;
$Cx = $P; $Cy = $P; $Cz = $D;
$Dx = -$P; $Dy = $P; $Dz = $D;
}
else if ($Face == FACE_BACK)
{
$Ax = $P; $Ay = -$P; $Az = -$D;
$Bx = -$P; $By = -$P; $Bz = -$D;
$Cx = -$P; $Cy = $P; $Cz = -$D;
$Dx = $P; $Dy = $P; $Dz = -$D;
}
else if ($Face == FACE_RIGHT)
{
$Ax = $D; $Ay = -$P; $Az = $P;
$Bx = $D; $By = -$P; $Bz = -$P;
$Cx = $D; $Cy = $P; $Cz = -$P;
$Dx = $D; $Dy = $P; $Dz = $P;
}
else if ($Face == FACE_LEFT)
{
$Ax = -$D; $Ay = -$P; $Az = -$P;
$Bx = -$D; $By = -$P; $Bz = $P;
$Cx = -$D; $Cy = $P; $Cz = $P;
$Dx = -$D; $Dy = $P; $Dz = -$P;
}
else if ($Face == FACE_TOP)
{
$Ax = -$P; $Ay = $D; $Az = $P;
$Bx = $P; $By = $D; $Bz = $P;
$Cx = $P; $Cy = $D; $Cz = -$P;
$Dx = -$P; $Dy = $D; $Dz = -$P;
}
else
{
$Ax = -$P; $Ay = -$D; $Az = -$P;
$Bx = $P; $By = -$D; $Bz = -$P;
$Cx = $P; $Cy = -$D; $Cz = $P;
$Dx = -$P; $Dy = -$D; $Dz = $P;
}
}
void FitTextureQuad(double $Ax, double $Ay, double $Az,
double $Bx, double $By, double $Bz,
double $Cx, double $Cy, double $Cz,
double $Dx, double $Dy, double $Dz,
i32 $TexW,
i32 $TexH,
ref double $Q0x, ref double $Q0y, ref double $Q0z,
ref double $Q1x, ref double $Q1y, ref double $Q1z,
ref double $Q2x, ref double $Q2y, ref double $Q2z,
ref double $Q3x, ref double $Q3y, ref double $Q3z)
{
double $Aspect;
double $ScaleX = 1.0;
double $ScaleY = 1.0;
double $StartX;
double $StartY;
double $EndX;
double $EndY;
double $L0x; double $L0y; double $L0z;
double $R0x; double $R0y; double $R0z;
double $L1x; double $L1y; double $L1z;
double $R1x; double $R1y; double $R1z;
if (($TexW <= 0) || ($TexH <= 0))
{
$Q0x = $Ax; $Q0y = $Ay; $Q0z = $Az;
$Q1x = $Bx; $Q1y = $By; $Q1z = $Bz;
$Q2x = $Cx; $Q2y = $Cy; $Q2z = $Cz;
$Q3x = $Dx; $Q3y = $Dy; $Q3z = $Dz;
return;
}
$Aspect = (double)$TexW / (double)$TexH;
if ($Aspect > 1.0)
{
$ScaleY = 1.0 / $Aspect;
}
else
{
$ScaleX = $Aspect;
}
$StartX = (1.0 - $ScaleX) * 0.5;
$EndX = 1.0 - $StartX;
$StartY = (1.0 - $ScaleY) * 0.5;
$EndY = 1.0 - $StartY;
$L0x = $Ax + (($Dx - $Ax) * $StartY); $L0y = $Ay + (($Dy - $Ay) * $StartY); $L0z = $Az + (($Dz - $Az) * $StartY);
$R0x = $Bx + (($Cx - $Bx) * $StartY); $R0y = $By + (($Cy - $By) * $StartY); $R0z = $Bz + (($Cz - $Bz) * $StartY);
$L1x = $Ax + (($Dx - $Ax) * $EndY); $L1y = $Ay + (($Dy - $Ay) * $EndY); $L1z = $Az + (($Dz - $Az) * $EndY);
$R1x = $Bx + (($Cx - $Bx) * $EndY); $R1y = $By + (($Cy - $By) * $EndY); $R1z = $Bz + (($Cz - $Bz) * $EndY);
$Q0x = $L0x + (($R0x - $L0x) * $StartX); $Q0y = $L0y + (($R0y - $L0y) * $StartX); $Q0z = $L0z + (($R0z - $L0z) * $StartX);
$Q1x = $L0x + (($R0x - $L0x) * $EndX); $Q1y = $L0y + (($R0y - $L0y) * $EndX); $Q1z = $L0z + (($R0z - $L0z) * $EndX);
$Q2x = $L1x + (($R1x - $L1x) * $EndX); $Q2y = $L1y + (($R1y - $L1y) * $EndX); $Q2z = $L1z + (($R1z - $L1z) * $EndX);
$Q3x = $L1x + (($R1x - $L1x) * $StartX); $Q3y = $L1y + (($R1y - $L1y) * $StartX); $Q3z = $L1z + (($R1z - $L1z) * $StartX);
}
void DrawTexturedQuad(i32 $TextureSlot,
double $Ax, double $Ay, double $Az,
double $Bx, double $By, double $Bz,
double $Cx, double $Cy, double $Cz,
double $Dx, double $Dy, double $Dz)
{
gl.Enable(GL_TEXTURE_2D);
gl.Enable(GL_BLEND);
gl.BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
/* Alpha test: transparent pixels (alpha <= 0.003) get DISCARDED
before the depth-buffer write, so a logo's transparent corners
don't block the opposite face of the cube from showing through. */
gl.Enable(GL_ALPHA_TEST);
gl.AlphaFunc(GL_GREATER, 0.003);
gl.BindTexture(GL_TEXTURE_2D, $TextureSlot);
gl.Color3f(1.0, 1.0, 1.0);
gl.Begin(GL_QUADS);
gl.TexCoord2f(0.0, 1.0); gl.Vertex3f($Ax, $Ay, $Az);
gl.TexCoord2f(1.0, 1.0); gl.Vertex3f($Bx, $By, $Bz);
gl.TexCoord2f(1.0, 0.0); gl.Vertex3f($Cx, $Cy, $Cz);
gl.TexCoord2f(0.0, 0.0); gl.Vertex3f($Dx, $Dy, $Dz);
gl.End();
gl.Disable(GL_ALPHA_TEST);
}
void DrawQuadEdges(double $EdgeR, double $EdgeG, double $EdgeB,
double $Ax, double $Ay, double $Az,
double $Bx, double $By, double $Bz,
double $Cx, double $Cy, double $Cz,
double $Dx, double $Dy, double $Dz)
{
gl.Disable(GL_TEXTURE_2D);
gl.Color3f($EdgeR, $EdgeG, $EdgeB);
gl.Begin(GL_LINES);
gl.Vertex3f($Ax, $Ay, $Az); gl.Vertex3f($Bx, $By, $Bz);
gl.Vertex3f($Bx, $By, $Bz); gl.Vertex3f($Cx, $Cy, $Cz);
gl.Vertex3f($Cx, $Cy, $Cz); gl.Vertex3f($Dx, $Dy, $Dz);
gl.Vertex3f($Dx, $Dy, $Dz); gl.Vertex3f($Ax, $Ay, $Az);
gl.End();
}
void DrawCubeFace(i32 $Face, i32 $TextureSlot, i32 $TexW, i32 $TexH,
double $CubeGap,
double $EdgeR, double $EdgeG, double $EdgeB)
{
double $Ax = 0.0; double $Ay = 0.0; double $Az = 0.0;
double $Bx = 0.0; double $By = 0.0; double $Bz = 0.0;
double $Cx = 0.0; double $Cy = 0.0; double $Cz = 0.0;
double $Dx = 0.0; double $Dy = 0.0; double $Dz = 0.0;
double $Q0x = 0.0; double $Q0y = 0.0; double $Q0z = 0.0;
double $Q1x = 0.0; double $Q1y = 0.0; double $Q1z = 0.0;
double $Q2x = 0.0; double $Q2y = 0.0; double $Q2z = 0.0;
double $Q3x = 0.0; double $Q3y = 0.0; double $Q3z = 0.0;
GetCubeFaceGeometry($Face, $CubeGap,
&$Ax, &$Ay, &$Az,
&$Bx, &$By, &$Bz,
&$Cx, &$Cy, &$Cz,
&$Dx, &$Dy, &$Dz);
FitTextureQuad($Ax, $Ay, $Az,
$Bx, $By, $Bz,
$Cx, $Cy, $Cz,
$Dx, $Dy, $Dz,
$TexW, $TexH,
&$Q0x, &$Q0y, &$Q0z,
&$Q1x, &$Q1y, &$Q1z,
&$Q2x, &$Q2y, &$Q2z,
&$Q3x, &$Q3y, &$Q3z);
DrawTexturedQuad($TextureSlot,
$Q0x, $Q0y, $Q0z,
$Q1x, $Q1y, $Q1z,
$Q2x, $Q2y, $Q2z,
$Q3x, $Q3y, $Q3z);
DrawQuadEdges($EdgeR, $EdgeG, $EdgeB,
$Ax, $Ay, $Az,
$Bx, $By, $Bz,
$Cx, $Cy, $Cz,
$Dx, $Dy, $Dz);
}
void DrawBackgroundGradient(double $TopR, double $TopG, double $TopB,
double $BottomR, double $BottomG, double $BottomB)
{
double $FarZ = -18.0;
double $SkyTop = 7.0;
double $SkyBottom = -2.0;
double $SkyHalfW = 18.0;
gl.Disable(GL_TEXTURE_2D);
gl.Begin(GL_QUADS);
gl.Color3f($TopR, $TopG, $TopB);
gl.Vertex3f(-$SkyHalfW, $SkyTop, $FarZ);
gl.Vertex3f( $SkyHalfW, $SkyTop, $FarZ);
gl.Color3f($BottomR, $BottomG, $BottomB);
gl.Vertex3f( $SkyHalfW, $SkyBottom, $FarZ);
gl.Vertex3f(-$SkyHalfW, $SkyBottom, $FarZ);
gl.End();
}
void DrawGroundGradient(double $NearR, double $NearG, double $NearB,
double $FarR, double $FarG, double $FarB)
{
double $GroundY = -2.3;
double $GridNear = -3.0;
double $GridFar = -18.0;
gl.Disable(GL_TEXTURE_2D);
gl.Begin(GL_QUADS);
gl.Color3f($NearR, $NearG, $NearB);
gl.Vertex3f(-12.0, $GroundY, $GridNear);
gl.Vertex3f( 12.0, $GroundY, $GridNear);
gl.Color3f ($FarR, $FarG, $FarB);
gl.Vertex3f( 20.0, $GroundY, $GridFar);
gl.Vertex3f(-20.0, $GroundY, $GridFar);
gl.End();
}
void SetupPerspective(i32 $WindowWidth, i32 $WindowHeight,
double $ViewScale, double $CameraDistance)
{
double $Aspect;
double $Top;
double $Right;
i32 $H;
/* rev31zzhg: the guard reads best as the C conditional idiom. */
$H = ($WindowHeight <= 0) ? 1 : $WindowHeight;
$Aspect = (double)$WindowWidth / (double)$H;
$Top = $ViewScale;
$Right = $Top * $Aspect;
gl.MatrixMode(GL_PROJECTION);
gl.LoadIdentity();
gl.Frustum(-$Right, $Right, -$Top, $Top, 1.0, 20.0);
gl.MatrixMode(GL_MODELVIEW);
gl.LoadIdentity();
gl.Translatef(0.0, 0.0, -$CameraDistance);
}
void RenderScriptFrame(ref blob $S)
{
$S assign RenderState;
if (gl.MakeCurrent() == 0)
{
return;
}
/* The rotation was advanced by Tick (callback world) just before
this call; $S is that snapshot. */
gl.Viewport (0, 0, $S.WindowWidth, $S.WindowHeight);
gl.ClearColor(0.015, 0.020, 0.035, 1.0);
gl.Clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
gl.Enable(GL_DEPTH_TEST);
gl.Enable(GL_BLEND);
gl.BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
SetupPerspective($S.WindowWidth, $S.WindowHeight,
$S.ViewScale, $S.CameraDistance);
DrawBackgroundGradient(0.01, 0.02, 0.08, 0.18, 0.07, 0.18);
DrawGroundGradient (0.16, 0.16, 0.16, 0.08, 0.08, 0.08);
gl.Rotatef($S.AngleX, 1.0, 0.0, 0.0);
gl.Rotatef($S.AngleY, 0.0, 1.0, 0.0);
gl.Rotatef($S.AngleZ, 0.0, 0.0, 1.0);
gl.LineWidth($S.EdgeWidth);
DrawCubeFace(FACE_FRONT , $S.VideoSlot, VIDEO_WIDTH, VIDEO_HEIGHT,$S.CubeGap, $S.EdgeR, $S.EdgeG, $S.EdgeB);
DrawCubeFace(FACE_BACK , $S.VideoSlot, VIDEO_WIDTH, VIDEO_HEIGHT,$S.CubeGap, $S.EdgeR, $S.EdgeG, $S.EdgeB);
DrawCubeFace(FACE_RIGHT , $S.VideoSlot, VIDEO_WIDTH, VIDEO_HEIGHT,$S.CubeGap, $S.EdgeR, $S.EdgeG, $S.EdgeB);
DrawCubeFace(FACE_LEFT , $S.VideoSlot, VIDEO_WIDTH, VIDEO_HEIGHT,$S.CubeGap, $S.EdgeR, $S.EdgeG, $S.EdgeB);
DrawCubeFace(FACE_TOP , $S.LogoSlot , LOGO_WIDTH , LOGO_HEIGHT ,$S.CubeGap, $S.EdgeR, $S.EdgeG, $S.EdgeB);
DrawCubeFace(FACE_BOTTOM, $S.LogoSlot , LOGO_WIDTH , LOGO_HEIGHT ,$S.CubeGap, $S.EdgeR, $S.EdgeG, $S.EdgeB);
gl.SwapBuffers();
gl.UnbindCurrent();
}
callback i32 OnVideoFrame(ref blob $FramePacket)
{
$State assign RenderState;
gl.UpdateTexture($State.VideoSlot, $FramePacket);
return 0;
}
double ClampCameraDistance(double $Distance)
{
/* rev31zzhg: the classic C clamp idiom — the right-associative
'?:' chain (rev31zzgy) reads top-to-bottom like the old ifs. */
return ($Distance < 1.5) ? 1.5
: ($Distance > 20.0) ? 20.0
: $Distance;
}
callback i32 OnOpenGlEvent(ref blob $EventPacket)
{
$State assign RenderState;
$EventPacket assign GlEvent;
if ($EventPacket.type == GL_EVENT_RESIZE)
{
$State.WindowWidth = (i32)$EventPacket.width;
$State.WindowHeight = (i32)$EventPacket.height;
}
else if ($EventPacket.type == GL_EVENT_MOUSE_WHEEL)
{
if ($EventPacket.wheel > 0)
{
$State.CameraDistance -= (0.25 * (double)$EventPacket.wheel);
}
else if ($EventPacket.wheel < 0)
{
$State.CameraDistance += (0.25 * (double)(-$EventPacket.wheel));
}
$State.CameraDistance = ClampCameraDistance($State.CameraDistance);
}
else if ($EventPacket.type == GL_EVENT_MOUSE_DOWN)
{
if ($EventPacket.button == GL_MOUSE_LEFT)
{
$State.SpeedX = 0.0;
$State.SpeedY = 0.0;
$State.SpeedZ = 0.0;
}
}
else if ($EventPacket.type == GL_EVENT_MOUSE_UP)
{
if ($EventPacket.button == GL_MOUSE_LEFT)
{
$State.SpeedX = AUTO_SPEED_X;
$State.SpeedY = AUTO_SPEED_Y;
$State.SpeedZ = AUTO_SPEED_Z;
}
}
else if ($EventPacket.type == GL_EVENT_MOUSE_MOVE)
{
if ($EventPacket.button == GL_MOUSE_LEFT)
{
$State.AngleY += ((double)$EventPacket.dx * 0.50);
$State.AngleX += ((double)$EventPacket.dy * 0.50);
}
}
return 0;
}
i32 main()
{
i32 $Video = 0;
i32 $Opened = 0;
i32 $Delivered = 0;
blob $LogoPacket[0];
string $EnvDisplay [256];
string $EnvXdg [256];
string $EnvSession [256];
if ((os.HasFlag("--help") != 0) || (os.HasFlag("-help") != 0))
{
printf("Usage: %s [options]\n", os.ScriptPath());
print.Line("");
print.Line("Options:");
print.Line(" --help Show this help message and exit.");
print.Line("");
print.Line("Same scene as opengl_ffmpeg_callback_demo, but every drawing");
print.Line("decision lives in this script (gl.Begin/Vertex3f/Frustum/...).");
print.Line("Compare side-by-side to see what the thick gl.* facade was");
print.Line("hiding.");
print.Line("");
print.Line("Mouse wheel zooms the camera; left-drag rotates the cube.");
print.Line("Close the window to exit.");
print.Line("");
print.Line("If no display is available, the demo will SKIP gracefully");
print.Line("(prints a single SKIP line and exits 0).");
return(0);
}
$EnvDisplay = os.GetEnv("DISPLAY");
$EnvXdg = os.GetEnv("XDG_RUNTIME_DIR");
$EnvSession = os.GetEnv("SESSIONNAME");
if (($EnvDisplay.Length == 0) && ($EnvXdg.Length == 0) && ($EnvSession.Length == 0))
{
print.Line("opengl-ffmpeg-script-driven: SKIP - display not available (need X11/Wayland on Linux, or a desktop session on Windows).");
return(0);
}
$Opened = gl.Open(WINDOW_WIDTH, WINDOW_HEIGHT, "Domin OpenGL FFmpeg script-driven demo");
if ($Opened == 0)
{
print.Line("opengl-ffmpeg-script-driven: SKIP - gl.Open returned 0 (SDL2 init or window creation failed).");
return(0);
}
/* main's own copy of the state: seeded here, then refreshed by Tick
every frame (the snapshot the render code reads). */
blob $S[sizeof(RenderState)];
{
$S assign RenderState;
$S.WindowWidth = WINDOW_WIDTH;
$S.WindowHeight = WINDOW_HEIGHT;
$S.CameraDistance = 4.2;
$S.ViewScale = 0.45;
$S.AngleX = 18.0;
$S.AngleY = -24.0;
$S.AngleZ = 0.0;
$S.SpeedX = AUTO_SPEED_X;
$S.SpeedY = AUTO_SPEED_Y;
$S.SpeedZ = AUTO_SPEED_Z;
$S.LastAdvanceMs = 0.0;
$S.CubeGap = 0.45;
$S.EdgeR = 0.95;
$S.EdgeG = 0.95;
$S.EdgeB = 0.80;
$S.EdgeWidth = 3.0;
$S.VideoSlot = gl.CreateTextureSlot();
$S.LogoSlot = gl.CreateTextureSlot();
}
/* seed the callback-world state (one synchronous crossing) */
if (worker.Call("Configure", ref $S, 1000) < 0)
{
print.Line("opengl-ffmpeg-script-driven: Configure call failed.");
gl.Close();
return 1;
}
/* the logo PNG as an RGBA pixel packet from the sdl2_image adapter -- the same
packet image.Load gives; gl.UpdateTexture eats either (the blob is the interface) */
$LogoPacket = sdl2image.Load(ProjectAssetPath("Assets/Images/domin_logo.png"));
gl.UpdateTexture($S.LogoSlot, $LogoPacket);
ffmpeg.SetOutputSize(VIDEO_WIDTH, VIDEO_HEIGHT);
ffmpeg.SetRealtime(1);
ffmpeg.SetVideoOnly(1);
if (ffmpeg.FileExists(ProjectAssetPath("Assets/Videos/demo.mp4")) != 0)
{
$Video = ffmpeg.Open(ProjectAssetPath("Assets/Videos/demo.mp4"));
ffmpeg.StartAudio($Video);
}
else
{
ffmpeg.SetRealtime(0);
$Video = ffmpeg.Open("synthetic:640x360");
}
while (gl.IsOpen() != 0)
{
gl.PumpEvents("OnOpenGlEvent");
if (gl.IsOpen() == 0)
{
break;
}
/* rev31zzej: the render loop is clocked by vsync (SwapBuffers,
~60 fps), not by the video. PollNext is non-blocking: 1 = a
new frame arrived (the callback updated the texture), 0 = no
new frame yet (we still render — the cube keeps moving on
its time-based rotation), -1 = end of stream -> loop the
video. The video paints at its own rate underneath. */
$Delivered = ffmpeg.PollNext($Video, "OnVideoFrame");
if ($Delivered == -1)
{
ffmpeg.Rewind($Video);
}
if (gl.IsOpen() != 0)
{
/* one crossing per frame: advance in the callback world,
snapshot into $S, render from $S */
if (worker.Call("Tick", ref $S, 1000) >= 0)
{
RenderScriptFrame(ref $S);
}
}
}
ffmpeg.Close($Video);
gl.Close();
return 0;
}