跪拜 Guibai
← Back to the summary

Building a 3D Smart Exhibition Dashboard with Codex, Without Writing a Line of Code

Without Writing a Single Line of Code: I Built a 3D Smart Exhibition Hall Dashboard with Codex

In my previous smart agriculture and smart highway projects, I had already walked through the whole chain of "research, drafting, asset extraction, modeling, and web implementation." This time, I switched the scene to a corporate exhibition hall.

Indoor scenes gave me one immediate impression: details become impossible to hide. A campus can first be judged by building mass and road relationships, but when the camera approaches inside the hall, wall edges, glass thickness, computer stands, display case seams, and floor reflections all come into view. Placing a screen on a wall is easy; making it look like a real device that can actually play content and accept operations requires designing the model and the frontend together.

image

This article follows the actual production pipeline: first turning industry research into requirements, then generating complete design drafts, splitting out resources and implementation boundaries, building models via Blender-MCP, and finally integrating into the web page, on-screen content, and camera navigation. Rework along the way is placed in the corresponding technical phases, explaining why changes were made and at which layer.

The project's business metrics use fixed sample data; the model is a visualization-level conceptual scene. Code in this article is organized and trimmed from the actual project, with some context and exception handling omitted; parameters serve to explain this implementation and should not be treated as universal configuration for all exhibition halls.

Video preview effect:

1. Research the business first, then let Codex write an executable Brief

I set two entry points for this project: enterprise operation overview and exhibition navigation.

When an operator opens the page, they need to know how many people came today, which exhibition zone is crowded, and which devices or environmental sensors need attention. A visitor entering navigation should be able to find zones, approach exhibits, read introductions, and operate screens. Both usage modes share one space, but attention and interaction depth differ.

image

Therefore, the research must first answer three specific questions.

  1. How is foot traffic counted? What do cumulative admissions, exits, and current in-hall count mean respectively, and how are the statistical periods divided?
  2. How is device status judged? What information supports heartbeat valid, content playing, playback error, and data expired states?
  3. How does content relate to space? When a zone is selected, where should the page and camera take the user?

AXIS's foot traffic documentation distinguishes passage counting from zone population estimation; BrightSign's diagnostics separately checks connection, network, display, and logs; ArtLens associates exhibit information, maps, and navigation. I used these materials to determine metrics and operation relationships, then made trade-offs for the specific corporate exhibition scenario. AXIS foot traffic docs, BrightSign diagnostics docs, ArtLens navigation intro.

After research, I had Codex organize the content into a Brief and a data contract. The Brief specifies users, screen size, reading order, zones, and interactions; the data contract agrees on fields, units, statistical scope, timestamps, and missing states.

image

This time it is designed for 1920*1080 with mouse operation, with six conceptual zones: entrance hall, corporate history, core technology, product experience, application cases, and future vision. In the fixed sample, cumulative admissions are 720, departures 600, current in-hall 120, and the six zones' counts also sum to 120. This gives a single data basis for later charts, graphics, and linkage.

Two easy-to-ignore details here: a normal device heartbeat alone does not prove content is playing normally; if an area lacks a historical curve, it should display "no records." Also, 49 business device records and 29 3D physical screen surfaces are different presentation objects; a complete device mapping requires a real ledger.

The output of this phase is the requirements, data, and interaction agreements that design and development can continue to use. If these agreements are not written clearly, every subsequent layer may interpret "foot traffic" and "current zone" differently.

2. Produce a complete design draft first, settle the whole-screen relationship

Only with the Brief does design generation begin.

Three candidate designs

I had Codex organize complete screens along three directions: A emphasizes the full hall and surrounding operational info; B uses a bright silver-white architectural axis with a continuous data band at the bottom; C emphasizes close-up product experience while retaining whole-hall orientation. The three drafts compare information organization and observation scale.

image

imageimage

In the generation task, I wrote the requirements as several concrete constraints: business fields and fixed readings, positions of each reading group, hall observation angles, material responsibilities for walls and glass, and the mapping of selected states. For example, the headcount bar chart is explicitly six zones with a shared 0-40 person scale, and space labels and details both point to the product experience zone. Post-generation checks compare each constraint, avoiding a pretty picture that has an extra bar, a missing unit, or contradictory numbers.

I chose B, the "silver-white exhibition axis." Light walls and silver-gray equipment form the main body, dark text ensures readability, blue carries business charts, and purple is used for selection and navigation hints. The center is reserved for the hall, the left side shows foot traffic, the right side shows zone details, and the bottom shows trends, distributions, and device records.

Freeze package of B design

Figure: The selected B "silver-white exhibition axis" design draft. It is a reference for later composition and material direction, not a live browser screenshot.

This step requires carefully looking at the whole screen, not just the central model. Whether the title and navigation crowd the space, whether the chart plot area is large enough, whether text is readable, and whether left/right panels block key exhibits must be judged together.

After selection, I froze it as the single design baseline. This freeze image is 1664936, mapped to 19201080 using a 15/13 proportional coordinate conversion, while saving the original generated image, crop records, and file summaries. I did not treat the enlarged image as a higher-resolution original.

The point is that later discussions about "the model is off" or "the card position is wrong" can return to the same reference. A design image provides visible outlines, colors, and lighting cues, but a single image cannot directly give wall-back structure, object depth, or real dimensions; those must be established separately during modeling.

3. Before splitting resources, assign each element an implementation owner

After the design image comes the easiest step to get wrong: cutting the entire panel, including text, numbers, and charts, into one image. It looks like the design at a glance, but it becomes restrictive everywhere when integrating data and interactions.

imageimageimage

I first had Codex build an element list, recording each element's original image coordinates, implementation owner, data source, and final destination.

Design element Implementation method What to preserve when splitting
Icons, borders, decorative surfaces PNG and UI components Original area, transparent edges, reuse relationships
Titles, values, units, tables React with real fonts Text, format, state, and data binding
Trend and distribution charts ECharts Data, scales, legends, and interactions
Walls, floors, glass, stands, equipment Blender and Three.js Scale, geometry, materials, and object identifiers
Spatial cards, pins, lines, selections Runtime DOM, SVG, and 3D effects World anchors, selection states, occlusion relationships
Content in exhibit screens Canvas, video textures, or CSS3D Screen position, orientation, size, and content type

During actual extraction, each native area must have a corresponding record. Below is a simplified example of an environment panel resource:

{  "id": "surface.environment",  "dimensions": [449, 499],  "source_rect_native": [1213, 77, 449, 499],  "method": "BACKGROUND_SEPARATION_DERIVED",  "owner": "SCAFFOLD"}

The coordinates here point to the frozen design draft; method indicates it went through background separation. It is not a resource that "keeps all original pixels just by cutting from the source image."

For a base plate with text, separate the text from the surface first. In this project, I performed same-image neighborhood pixel repair within designated areas and kept the repair mask; icons and soft edges preserve Alpha and exclusion areas. Repaired resources are explicitly marked as derived and cannot claim to have restored the original background hidden behind text.

Transparent edges deserve special attention. The color of a pixel in a screenshot is a blend of foreground, background, and coverage. Simply removing a light background often leaves white edges; estimating Alpha may accidentally remove white that legitimately exists inside an icon. I placed materials on dark, light, and checkerboard backgrounds to inspect, then pasted them back to the original position for comparison.

Finally, generate an asset registry and a recomposition preview: panels and icons use extracted resources, text and charts are redrawn, and the whole screen is reassembled by coordinates. This lets you check for omissions, duplicates, alignment, and edge issues. Using the original image as a base and overlaying a few slices makes it hard to spot true missing pieces.

The asset list also distinguishes reusable master assets from instances that appear in the scene: the same icon can appear multiple times, and multiple usages should not be treated as separate independent assets. Later, spatial cards and pins were converted to DOM styles for interaction, so the design assets must be updated to match the actual implementation.

image

After this step, the frontend gets a composable resource package, and modeling gets architectural outlines, material, and local construction references. A floor crop from the design draft only helps judge appearance; it cannot be used directly as a seamless stone texture to cover the scene.

4. Build the model via Blender-MCP, set up scale, structure, and camera first

In the 3D phase, I first built representative slices of the entrance hall, product experience zone, and adjacent floor, including entrances, display walls, stands, and plants, to expose proportion, material, and export issues, then gradually expanded to the full hall.

image

The call chain for this project is: Codex writes project Python scripts, which bridge to the installed Blender-MCP plugin to execute bpy operations in Blender, save an editable scene, and export web assets. The platform saves materials and execution records at each stage, allowing you to return to a previous round for modification.

Before modeling, I had Codex sort out three basic prerequisites.

First, unify the spatial scale. Buildings, display cases, screens, and camera positions must use the same unit. When one unit in modeling is understood as one meter, wall thickness, countertop height, and camera height become comparable. This project later adjusted the full space height to a conceptual 4.2 meters; it is used to establish proportion, and final calibration still requires site data.

imageimage

Second, establish object hierarchy. Under the hall root node, distinguish architecture and zones, then place display walls, cabinets, equipment, and plants. The detachable roof is managed separately for switching between full building and section overview. Each zone keeps a stable zoneId, and screens separately record position, rotation, and size.

image

Close-up of new computer model

Fixed portrait screen installation

image

Third, align the composition first. Look at the axis from entrance to back row, the volume of display walls, and the main equipment positions, then adjust the camera. Different camera distances, field-of-view, and target points make the same model look too flat, too tall, or crowded. If composition is not aligned, continuing to increase highlights rarely solves the difference.

When reusing existing components, object transforms also need handling. The project script first transforms the source object's vertices to world coordinates, calculates center and size, then places it into the new zone with a unified scale. If you keep the parent scale from the source asset and apply scaling again on a new parent, the device easily deforms or shifts. After moving and exporting, check final world coordinates, not just local values in the properties panel.

At this point I ask Codex to output several kinds of checks: overall bounding box, major object dimensions, object hierarchy, overview and close-up camera positions, and exported content range. Blender working files and runtime GLB are saved separately, and preview helper objects should not be mixed into the official scene.

This phase establishes a credible spatial skeleton. Text and business numbers on screens remain for runtime, avoiding baking future-changing content into the model.

5. From gray box to realistic: geometry, normals, and PBR together

The most common user feedback is "material is wrong" or "edges are unrealistic." Such problems may come from materials, or from geometry without thickness, incorrect normals, or over-smoothed silhouettes. I fixed the refinement order as: structure → edges → normals → materials → lighting.

image

1. Give walls and equipment real construction relationships.

Walls need thickness, corners, top and side surfaces; display cases need countertops, bodies, kickboards, and door gaps; monitors need shells, bezels, recessed display surfaces, and support structures. Computers must distinguish lid, hinge, keyboard, trackpad, and body thickness. From a distance it looks like a rectangle; up close it must explain how it is assembled.

Also, all sub-parts must follow the correct parent object. When a plant pot was later intersecting with a cabinet, we moved the entire group containing pot, soil, trunk, and leaves, avoiding the situation where the pot moves but the plant stays.

2. Use bevels to create edge highlights, then normals to stabilize large planes.

Completely sharp mathematical edges rarely form realistic highlight bands; too large a bevel makes a wall look like soft plastic. Bevel radius should be considered with object size, manufacturing method, and viewing distance.

Below is an excerpt from the wall refinement script. Before execution, the object has been rebuilt at local dimensions, scale normalized, and dims is the size vector:

bevel = o.modifiers.new("ManufacturedEdge", "BEVEL")
bevel.width = min(0.028, float(min(dims)) * 0.11)
bevel.segments = 5
bevel.affect = "EDGES"
bevel.harden_normals = True
bpy.context.view_layer.objects.active = o
bpy.ops.object.modifier_apply(modifier=bevel.name)
normal = o.modifiers.new("PlanarNormals", "WEIGHTED_NORMAL")
normal.keep_sharp = True
normal.weight = 65
bpy.ops.object.modifier_apply(modifier=normal.name)

The radius is constrained by the thinnest direction and has an upper limit; it is not a uniform 28mm round-corner applied to all objects. Walls, side seals, and top surfaces are also given separate materials, so large planes and edges differ under light.

Normals decide how a surface responds to light. Weighted normals help keep large planes flat while allowing bevel transitions to look natural; but normals cannot fix wrong silhouettes or replace real thickness. Different Blender versions may have different entry points for normal operations, so scripts must be checked against the actual version. Blender weighted normal docs: https://docs.blender.org/manual/en/3.0/modeling/modifiers/modify/weighted_normal.html

3. Design color, roughness, and micro-normals separately.

In PBR, Base Color describes base color, Metallic distinguishes metal vs. non-metal response, Roughness controls micro-surface scattering, Normal changes local shading direction, and Transmission with IOR participate in glass transmission and refraction. Each has its own role; one "brightness" parameter cannot serve all. Blender Principled BSDF: https://docs.blender.org/manual/en/4.0/render/shader_nodes/shader/principled.html

This time, stone refinement generated three 512*512 maps: color, roughness, and normal. Color keeps only low-amplitude cloud patterns, roughness adds subtle variation, and normal controls very weak surface undulation. UV is laid out in world scale, avoiding the same material showing completely different texture proportions on a large floor versus a small object.

A practical modeling fact: normal maps change lighting response, not the outer silhouette. Fine stone grain can go into normals; wall thickness, computer edges, and leaf curvature still require geometry.

Color maps and data maps must not mix color spaces. Color usually uses sRGB; roughness, normal, and AO are treated as non-color data. If a roughness map is color-transformed, the tuned material response changes. Three.js color management: https://threejs.org/manual/pages/color-management.html

4. Glass requires transmission, thickness, and environment together.

Lowering opacity is just transparent blending; it does not fully describe glass optics. In one round of refinement, this project used the following glass parameters:

bs = glass.node_tree.nodes["Principled BSDF"]
bs.inputs["Base Color"].default_value = (.985, .995, 1, 1)
bs.inputs["Roughness"].default_value = .018
bs.inputs["Metallic"].default_value = 0
bs.inputs["Transmission Weight"].default_value = .985
bs.inputs["IOR"].default_value = 1.47
# glTF Material Output node already created in context
settings.inputs["Thickness"].default_value = .02

The scene also configures volume absorption, and on the web side checks thickness, attenuationDistance, and attenuationColor. These values are visually tuned parameters; a real project should reset them based on glass type and thickness.

Glass needs a visible space behind and a reflective environment in front. Frames, seams, sides, and thickness together help people judge its presence. A single fully transparent thin plane, even with all parameters present, may lack richness.

5. For small devices and plants, prioritize silhouette and ground contact.

The thin/thick ratio of a monitor, the base and countertop relationship, are more noticeable than blindly increasing face count. Plants need trunk hierarchy, leaf orientation, size variation, and natural asymmetry. Close-ups must preserve leaf outlines and curvature; distant fine branches can be simplified.

Later we removed the intersection between a plant and an adjacent cabinet, and rechecked ground clearance, contact shadows, and export positions. This example shows that object placement and combination relationships are also part of realism.

6. Complete lighting, AO, and export pipeline so materials truly enter the web

A model that looks good in Blender often looks washed out or gray when imported to the web, often involving differences between two rendering environments.

I broke this step into three layers: environment reflection provides overall lighting cues, lights shape space, and contact AO strengthens local occlusion. Each is checked separately.

1. Provide a reflectable environment for metals and glass first.

image

In the web, RoomEnvironment generates the environment, and PMREM pre-filters it for materials of different roughness. Core configuration:

const room = new RoomEnvironment();
const pmrem = new THREE.PMREMGenerator(renderer);
const environment = pmrem.fromScene(room, 0.05);
scene.environment = environment.texture;
scene.environmentIntensity = 0.9;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.04;

The environment, render target, and generator must be released on unload. Before adjusting exposure, first confirm the texture and output color space are correct, avoiding stronger lights to compensate for incorrect color conversion.

Light strips are also split into two parts: visible emitting geometry and actual illumination. In real-time rendering, an emissive surface does not automatically provide full indirect lighting. This project uses rectangular area lights to shape ceiling illumination, plus local lights and selection effects.

image

2. Use contact AO to place cabinets, corners, and equipment in the space.

AO describes occlusion caused by nearby geometry. It helps render cabinet bottoms, seams, and wall corners, but does not include full direct lighting and cannot replace specular reflection.

image

This project sets up an independent ContactAO_UV on the export copy, keeps the original surface UV for stone, and uses a 2048*2048 atlas for unified baking. The node is temporarily wired as "AO -> Emission -> Output", then bake EMIT, avoiding mixing light and material colors into the occlusion map:

ao = nodes.new("ShaderNodeAmbientOcclusion")
ao.inputs["Distance"].default_value = 0.55
ao.samples = 24
emission = nodes.new("ShaderNodeEmission")
links.new(ao.outputs["AO"], emission.inputs["Color"])
links.new(emission.outputs[0], output.inputs["Surface"])
# non-overlapping UV, target atlas, and bake object created
scene.render.bake.margin = 5
bpy.ops.object.bake(type="EMIT", uv_layer="ContactAO_UV")

Material copies, target Image Texture activation, and object filtering code are omitted. The complete flow restores the original BSDF after baking, then connects the atlas to the Occlusion input of the glTF Material Output. The web reads the exported occlusion texture; the temporary Emission material must not remain in the final model. Blender glTF occlusion map export: https://docs.blender.org/manual/en/5.3/addons/scene_gltf2.html

Transparent glass, plants, and emissive parts are not all baked with the same AO recipe. After moving an object, its occlusion must be recalculated. The final UV channel may be cleaned up; reading should follow the exported texCoord, not assume all materials always use the second UV set.

3. Handle floor reflections separately, avoid turning stone into a mirror.

image

The web uses Reflector to get a planar reflection, then perturbs and softens the sampling based on stone roughness and micro-normals. The fragment shader also adjusts reflection intensity by the angle between the view and the floor. Simplified excerpt:

float facing = clamp(
  dot(normalize(cameraPosition - vWorld), vec3(0.0, 1.0, 0.0)),
  0.0, 1.0);
float fresnel = pow(1.0 - facing, 3.0);
float reflectance =
  mix(0.13, 0.37, fresnel) * (1.0 - rough * 0.22);

This is a real-time visual approximation, not a full physical light transport solution. It makes reflections more visible at glancing angles while preserving roughness variation. The actual code also does multi-point sampling on the reflection map to soften overly sharp reflections.

At the export stage, we export only the independent physical scene, saving geometry, normals, UVs, and necessary materials, excluding preview media and helper cameras. Procedurally generated material details are converted into portable textures, and glTF material extensions are checked to be included in the file.

A model manifest is also generated, recording runtime URL, content summary, zone anchors, and screen transforms. The editable Blend, runtime GLB, textures, and manifest must be a matching set of versions.

7. Integrate with Scaffold, connect model, charts, and selection state

After the model and assets are ready, web implementation begins.

image

The page continues to use the @lius1314/visual-dashboard-scaffold I used before, managing the whole screen through App, registered blocks, and JSON layout; React organizes content and state, ECharts draws peripheral business charts, and React Three Fiber and Three.js carry the scene.

The previously split images enter resource configuration; titles, numbers, and tables continue to use real fonts. Loading placeholder images appear only in loading or error states; after the model is ready, the interactive scene takes over the central area.

I first had Codex establish shared state instead of each card maintaining its own "current zone." The selection logic is short:

const selectZone = (id: string) => {
  setZone(id);
  setDevice(null);
};
const selectDevice = (id: string) => {
  const device = data.devices.find(item => item.deviceId === id);
  if (!device) return;
  setZone(device.zoneId);
  setDevice(id);
  setTab('devices');
};

Spatial labels, right-side details, device list, and camera all read the same state. When a user selects a zone from a chart, the scene finds the same zoneId; entering from the device list locates the zone first. The whole-hall count keeps the whole-hall basis, and zone details show the current selection.

Page mode, model readiness, animation pause, and return-to-overview also need centralized handling. The project unifies motion permission as "not paused, system does not require reduced motion, and page is visible," and each animation track reads this permission.

Scene loading is separated from business selection. Switching zones only updates state, markers, and camera; it does not re-request the same model. Only when truly swapping models or retrying does loading start based on the model URL; if an old request returns late, it should be discarded and released to avoid overwriting the current scene.

After this phase, the data panel and the 3D space become one application, not two things that happen to be on the same page.

8. Let exhibition screens carry content, not just a sticker image

After the exhibition web page ran, I raised one demand:

"I mean play directly inside the 3D hall. Not click-to-popup."

This demand determined the technical choice for on-screen content. We split content into three types.

1. Video: VideoTexture mapped to physical screen planes.

Video is decoded by the browser and passed as a texture to Three.js. The screen's position, rotation, and size come from the model manifest, and playback is triggered by user click:

image

const video = document.createElement('video');
video.src = movieUrl;
video.muted = true;
video.playsInline = true;
video.loop = true;
const texture = new THREE.VideoTexture(video);
texture.colorSpace = THREE.SRGBColorSpace;
const material = createDisplayMaterial(texture);
// set mesh position, quaternion, and size using screen record
// call video.play() on user click, handle returned Promise

Video follows camera perspective, fitting the screen with original aspect ratio. Pause video on exit and clear play intent; release texture, material, and event listeners on switch or unload to prevent background playback after exit.

2. Corporate history and data dashboards: Canvas drawing, then generate texture.

image

Corporate history is split into six chapters: founding and start, technology accumulation, product formation, industry application, collaborative ecosystem, and future outlook, with each panel corresponding to different content. Official years and company achievements require company materials; the prototype uses reviewable chapter text to establish structure.

The data dashboard draws operation, foot traffic, and device-environment pages from the same business snapshot. CanvasTexture only marks needsUpdate when content changes; the currently read dynamic dashboard updates at most about 12 times per second, and background posters remain static. This frequency constrains content drawing only, not limiting the entire scene to 12 FPS.

Reading content had another issue: reflections were obvious enough, but text got washed out. We separated text, charts, and the physical shell, giving content an independent reading material:

const readingMaterial = new THREE.MeshBasicMaterial({
  map: canvasTexture,
  color: '#ffffff',
  toneMapped: false,
  depthTest: true,
  depthWrite: true,
});

This keeps reading pixels stable while metal frames and architectural glass continue using physical materials; content still participates in depth testing and does not display through walls. Font size, line height, and weight are checked by projected size on screen, not just the Canvas's original resolution.

3. Operable computer: CSS3D to place real DOM.

In the distance, the computer uses a static desktop preview. When entering the computer, CSS3DRenderer places React DOM onto the screen plane, allowing windows, buttons, and input fields to receive mouse and keyboard operations.

image

The most important transform is mapping mouse screen coordinates to the computer desktop's local coordinates:

raycaster.setFromCamera(
  new THREE.Vector2(
    (clientX - rect.left) / rect.width * 2 - 1,
    1 - (clientY - rect.top) / rect.height * 2,
  ),
  camera,
);
const hit = raycaster.ray.intersectPlane(screenPlane, point);
if (!hit) return null;
hit.applyMatrix4(inverseScreenMatrix);
return { x: hit.x + desktopWidth / 2, y: desktopHeight / 2 - hit.y };

screenPlane is built from the screen's position and orientation; inverseScreenMatrix is the inverse of the CSS3D object's world matrix already scaled by screen size, so local coordinates correspond to desktop pixels. When entering operation state, input goes to the desktop and the scene camera stops grabbing; restore on exit.

CSS3D and WebGL do not share a depth buffer, so we separately check orientation and occlusion to decide desktop visibility. This implements a macOS-style web desktop, providing prototype operations, not running real macOS.

These content types share the physical screen's position information, each choosing an appropriate presentation method. Adding external dashboards or streaming in the future requires handling authentication, cross-origin, and update mechanisms.

9. Add spatial cards and camera navigation, so clicks have a clear destination

With content displayable, users also need to see it and see it clearly.

imageimage

I split interaction into two things: spatial cards accurately point to an object; after a click, the camera enters a suitable observation position.

1. Project card pins first, then layout, then draw connecting lines.

3D anchors are first projected through the camera to normalized coordinates, then converted to CSS pixels in the current viewport. Cards and SVG lines use the same coordinate system to avoid device pixel ratio offsets:

const p = worldAnchor.clone().project(camera);
const pinX = (p.x + 1) * viewportWidth / 2;
const pinY = (1 - p.y) * viewportHeight / 2;

After obtaining the anchor, arrange card position and avoidance, then connect from the card's actual edge to the anchor center. Card size should be stable, selection mainly changes color, and the toolbar participates in avoidance. Visible pins and click hotspots can be separate, keeping the view light while making it easy to hit.

Animation follows state changes: brief line drawing on selection, slight ring change, soft panel entry. The anchor itself stays stable, preventing cards, arrows, and real positions from constantly appearing disconnected.

2. Observation camera is calculated from screen size, then occlusions are checked.

Close-up reading cannot just use a fixed "move camera forward two meters." Screens have portrait and landscape aspect ratios, and viewports change. The project first estimates required visible range from screen height, width, and viewport aspect:

const normal = new THREE.Vector3(0, 0, 1)
  .applyQuaternion(screenQuaternion);
const span = Math.max(
  screenHeight / 0.78,
  screenWidth / (Math.max(0.5, aspect) * 0.88),
);
const distance = span / (
  2 * Math.tan(THREE.MathUtils.degToRad(38 / 2)));
const position = target.clone().addScaledVector(normal, distance);

This uses perspective projection: given a vertical field of view, visible height is determined by distance; width is converted via viewport aspect ratio. 0.78 and 0.88 are composition margins, and 38 degrees is the preferred vertical FOV for this project.

This result is not the final camera. We cast rays from candidate positions to the screen center and edge sample points, checking whether walls, cabinets, and plants block the reading area, then try lateral or vertical adjustments. In narrow positions requiring close-up and larger FOV, perspective distortion must also be watched.

Before entering reading, save the original camera; restore on exit; recalc on window change or model re-readiness. Navigation switches not just a target point but also camera position, target, FOV, and content state.

10. Control resource costs, then verify results at different levels

As features grow, performance must be managed with the production process. I check from four directions.

Current iteration model is about 348.7k triangles, 11.4 MB. We have done plant geometry optimization and lossless compression of AO grayscale textures; dedicated geometry encoding, GPU texture compression, LOD, and progressive loading are directions for the next step.

image

Acceptance must also be layered: asset splice-back checks resources and layout, geometry check checks dimensions and intersections, model export check checks nodes, materials, and screens, web operation checks selection, playback, input, and return, and performance test checks first screen, switching, frame time, and long-memory.

We have build, standalone content rendering, geometry, and limited camera ray-cast check records, but these cannot replace browser performance on target devices. Currently narrow camera positions and occlusions still have trade-offs, and overall visuals need continued comparison with the design. Real interfaces, site scales, and long-run performance must be verified under later deployment conditions.

Looking back at this production chain, I think several tasks deserve to be continued in the next project.

  1. Research must leave executable requirements. Every metric can explain its source, scope, and missing state; every interaction has a clear purpose.
  2. Design must be splittable into resources with clear responsibilities. Images, text, charts, models, and spatial markers are carried by the appropriate technology.
  3. Realism starts from geometry and scale. Fix structure, edges, thickness, and contact relationships before adjusting materials and lighting.
  4. Consider web usage during modeling. Object identifiers, screen transforms, detachable roof, and observation cameras must serve subsequent interactions.
  5. Every round of changes must be traceable. Design references, model source files, exported assets, and web manifests correspond to each other to locate differences.

In this practice, Codex connected material organization, design generation, asset processing, Blender scripts, and frontend code into one workflow. My job was to keep making the goal concrete: what edge this wall needs, how this device should be supported, what should happen when this screen is clicked.

As these questions get implemented one by one, the exhibition hall transforms from an anticipated design image into a space that can be observed, entered, read, and operated. That is what I value most about continuing to use Codex for 3D visualization.

I have organized the materials and source code for the 3D smart exhibition hall. If you need them, follow my account and send me a private message with "smart exhibition hall" to get them.

image