Testing LLM 3D Scene Generation: First Impressions Are Easy, the Real Test Is the Backend
A single-file WebGL2 3D scene with zero dependencies reveals how large language models handle visual composition, engineering discipline, and state-driven design. This article breaks down a practical evaluation method, what separates strong results from weak ones, and why moving from an offline demo to a data-driven system shifts the real burden to backend architecture.
Why a Single-File 3D Scene Reveals an LLM’s Frontend Ability
The most common way to evaluate an LLM’s frontend code generation is to ask it to write a page or a component. But the signal from such tasks is limited. Most models can produce something that runs, and differences in quality are often masked by frameworks and default styling. To genuinely separate models, you need a task with much higher information density: a self-contained, offline HTML file that renders a complete micro-world using raw WebGL2.
The value of this task lies in its strict constraints:
- Zero dependencies: No external images, model files, or third-party libraries. All geometry must be generated procedurally in code, and matrix operations must be implemented by hand.
- Single file: Shaders, scene graph, animation loop, and state management all compressed into one file, making code organization immediately visible.
- Strong theme: The scene has a clear narrative subject and spatial logic, rather than a pile of disconnected geometric shapes.
Under these constraints, a model’s capabilities are naturally differentiated. Visual taste, mathematical rigor, engineering organization, and state design all show up directly in the output. More importantly, this type of task maps to two distinct levels of evaluation: the immediate “first impression” and the “data and engineering capability” behind it. The former determines whether a demo succeeds. The latter determines whether a system can move toward production.
Test Design: A Ring Lock, a Terraced Sea City, and Tidal Linkage
The scene consists of three entity types:
- A monumental ring lock: the primary visual subject, conveying mass and ceremony.
- A terraced sea city: layered platforms rising around the lock, providing depth and hierarchy.
- Dynamic tides: the single driving variable of the scene, rising and falling periodically.
The prompt follows two principles.
Principle one: composition, architecture, and materials first. The prompt explicitly requires establishing a solid composition, credible building forms, and correct material relationships before anything else. This prevents models from spending their “compute budget” on particles, bloom, or post-processing. No amount of effects can hide a failed composition.
Principle two: explicitly forbid “animating a blue plane up and down.” This is the crux of the task. Ship draft and rocking, shoreline waterlines and foam, plaza flooding and drying must all respond to tidal changes. In engineering terms: the tide level is global state, and every subsystem that interacts with water must subscribe to it.
The essence of this prompt style is moving acceptance criteria into the requirements. Instead of finding problems after the fact, define what “correct” means at the start. This is a transferable practice for writing technical requirements in everyday development.
The First-Impression Battle: How the Default Camera Organizes the Scene
In testing, two submissions both met the hard requirements of raw WebGL2 and zero dependencies, yet the quality gap was obvious within the first second. The gap came not from resolution or anti-aliasing level, but from how the default camera organized the scene’s hierarchy.
This breaks down into four decisions:
- Subject proportion: the ring lock should occupy the visual center of gravity rather than being diluted by surrounding elements.
- Horizon position: a slightly lowered horizon strengthens the terraced depth of the sea city.
- Foreground reference: a nearby boat or dock section provides a sense of scale for the lock’s mass.
- Layered rhythm: foreground detail, midground subject, and background silhouette should have clear contrast in brightness and detail density.
Example code for camera positioning:
// A default camera with a story: slightly elevated, medium-long focal lengthconst camera = {position: [42, 26, 58], // above the city level to read the terracestarget: [0, 6, 0], // aimed at the ring lock centerfov: 50 * Math.PI / 180, // medium-long lens to emphasize the subjectnear: 0.1, far: 800};
A practical takeaway: the default camera should be a “camera with a story.” Before the viewer touches any interaction, the spatial logic of the world should already be legible. Two implementations that are both rendering-correct can differ by a single field-of-view or camera-height choice, producing the gap between “a work” and “an exercise.”
Materials follow the same logic. With zero dependencies, there are no ready-made textures or assets. Credibility comes from three things: a unified fog color that presses all objects into the same atmosphere, a restrained palette of no more than three dominant colors for water, stone, and metal, and wetness cues near the shoreline. Achieve these three, and the image holds up even with fully procedural colors.
The Correct Way to Do Tides: One State Drive Multiple Subsystems
What does a weak submission do? It moves the water mesh’s y-coordinate along a sine wave over time. Done. This is exactly what the prompt forbids. A strong submission models the tide as global state and has all related subsystems subscribe to it:
const TIDE = { min: 0.0, max: 14.0, period: 90 }; // water level 0-14m, 90s periodfunction tideLevel(t) {// half-sine to simulate rise and fall, normalized to [0,1]return 0.5 - 0.5 * Math.cos((t % TIDE.period) / TIDE.period * Math.PI * 2);}function updateScene(t) {const k = tideLevel(t);const waterY = TIDE.min + k * (TIDE.max - TIDE.min);water.position.y = waterY; // water surface rises and fallswaterShader.uniforms.uWaterY.value = waterY; // shoreline foam followsplaza.setFlooded(waterY > plaza.sillHeight); // flooding as a state checkboats.forEach(b => b.setDraft(waterY)); // ship draft linkedgate.setOpenness(k); // gate opening linked}
Three design points stand out:
- Normalization: tide level is expressed as [0,1]; each subsystem maps it to its own unit range (draft depth, flooding threshold, gate opening), decoupling state from presentation.
- State over animation: flooding is a boolean condition (water level exceeds the plaza sill elevation), not a hard-coded timeline animation. High tide and low tide both behave correctly.
- Derived quantities: foam width and hull rocking amplitude are derived from water depth, requiring no independent timers and greatly narrowing the surface for bugs.
At the shader level, shoreline wetness and foam do not need textures. The difference between world-space height and the current water level is enough:
uniform float uWaterY;varying vec3 vWorldPos;void main() {float depth = uWaterY - vWorldPos.y; // current water depth at this pointfloat wet = smoothstep(0.0, 0.8, depth); // wetness gradientfloat foam = smoothstep(0.8, 0.05, depth) * (1.0 - wet);vec3 color = mix(mix(dryColor, wetColor, wet), foamColor, foam);gl_FragColor = vec4(color, 1.0);}
This pattern of “one state driving the whole scene” is exactly the foundation for moving from a demo to a data-driven system.
The Zero-Dependency Baseline: What Raw WebGL2 Actually Tests
Requiring raw WebGL2 instead of an established rendering library is fundamentally a test of graphics fundamentals:
- Matrices and transforms: view, projection, and normal matrices must be implemented manually. One row-major versus column-major mistake breaks the entire screen.
- Shader management: compilation, linking, and uniform uploads require a proper resource lifecycle.
- Geometry generation: the lock’s curved surfaces and the city’s terraces require procedural vertex generation and normal computation.
- Render order: draw order between transparent water and opaque objects, and depth-testing strategy, directly determine whether the scene breaks visually.
None of these can be bypassed by calling a library. In fact, they provide the clearest window into generated code quality: whether naming is systematic, initialization is centralized, and resources are releasable all become visible within a few hundred lines.
Beyond First Impressions: The Real Test Is the Backend
In a demo, the tide is driven by local time and everything looks elegant. But in a real system, water level is not a function of time. It is data. When the scene needs to consume real hydrological data, the structure of the problem changes:
- Where data comes from: water level may come from sensor measurements, astronomical tide harmonic calculations, or coupled meteorological models. Sources differ in frequency and quality.
- Where computation lives: tide prediction and flood risk assessment are server-side responsibilities. Model parameters should not be scattered across the frontend.
- How data reaches the frontend: water level updates require low latency, reconnection after network interruption, and the ability to replay history.
A typical general architecture looks like this:
[sensors / hydrological models] -> [ingestion and computation services] -> [time-series storage]|[message queue fan-out]|[real-time push channel] -> frontend scene state
When deployed to the cloud, these responsibilities map to mature product categories. Static assets and offline scene packages can live in object storage with CDN distribution. Water-level time-series data fits time-series and log storage. Real-time push can use a message queue for fan-out. Compute services can run on a container platform with event-driven scaling. End-to-end observability falls to logging, monitoring, and alerting. The frontend change is more fundamental: replace the “performance script” with “state subscription.” The scene no longer computes a tide function on its own. It responds to state frames pushed by the server, with interpolation and degradation strategies for data latency, gaps, and jumps.
This is the deeper water beyond the first impression: the visual layer solves “making people want to look.” The data layer solves “making people able to trust.”
A Reusable Three-Layer Evaluation Checklist
- L1 Visual layer (first impression): does the first frame have clear visual hierarchy? Are tone and fog consistent? Are first-frame time and frame rate stable?
- L2 Engineering layer (generated code quality): are code modules clear? Is state managed centrally? Do shaders and buffers have lifecycle management? Are core parameters tunable?
- L3 Integration layer (production feasibility): is core state decoupled from rendering and replaceable by external data? Are there error and degradation paths? Is the frontend-backend contract explicit?
The three layers progress in depth and become progressively rarer. Most generated results stop at an acceptable L1. A stable L2 output is already uncommon. L3 means the generated code understands that it is merely one client of a larger system.
Conclusion
A single-file 3D scene is an effective testing ground: with minimal packaging, it evaluates aesthetics, mathematics, and engineering simultaneously. The findings are also instructive. Today’s large models are increasingly good at shaping first impressions. Default choices for composition, materials, and atmosphere are improving. But when a scene moves from an offline performance to an online system, the backend work cannot be skipped: state must become data, computation responsibilities must be assigned to the server, and the real-time pipeline must be reliable. The frontend makes the world stand up visually. The backend makes the world trustworthy. Maturity on both ends ultimately determines whether this class of 3D application succeeds.
