/
tiktaalik
/
ThatOpenEngineDocs
Обзор
Документация
Войти
/
tiktaalik
/
ThatOpenEngineDocs
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
docs/Tutorials/Components/Front/PostproductionRenderer.mdx
642 строки
24 KB
Antonio González Viegas
chore: update build
02 дек 2025, 21:14
02 дек 2025, 21:14
9643a7d
Код
Авторство
О чём код?
<div style={{position: "relative"}}><iframe src="https://thatopen.github.io/engine_components/examples/PostproductionRenderer"></iframe><button class="full-screen-btn" onClick={() => window.open("https://thatopen.github.io/engine_components/examples/PostproductionRenderer")} >Go Full Screen</button></div> :::info Source Copying and pasting? We've got you covered! You can find the full source code of this tutorial [here](https://github.com/ThatOpen/engine_components/blob/main/packages/front/src/core/PostproductionRenderer/example.ts). ::: ### 🎥 Great graphics --- Postproduction effects enrich your 3D scenes. There are several post-production effects, such as adding shadows, rendering outlines, adding ambient occlusion and applying bloom, that can enhance and make your scene look cool. In this tutorial, you'll learn how to do it. :::tip Postproduction? The simple Three.js renderer isn't bad, but it's pretty basic. Postproduction are a collection of effects you can add to your scene to make it look much better. Of course, this means consuming more resources, but luckily for us, the power of devices is proportional to the size of its screen, so we should be able to enjoy this beauty in most scene even from our smartphones! ::: In this tutorial, we will import: - `three` to create some 3D items. - `@thatopen/components` to set up the barebone of our app. - `@thatopen/ui` to add some simple and cool UI menus. - `@thatopen/components-front` to use some frontend-oriented components. - `Stats.js` (optional) to measure the performance of our app. - `GTAOPass` (optional) to control the ambient occlusion parameters. ```js import * as THREE from "three"; import * as OBC from "@thatopen/components"; import Stats from "stats.js"; import * as BUI from "@thatopen/ui"; // You have to import * as OBF from "@thatopen/components-front" import * as OBF from "../.."; ``` ### 🌎 Setting up a simple scene --- We will start by creating a simple scene with a camera and a renderer. If you don't know how to set up a scene, you can check the Worlds tutorial. Notice how we use the PostproductionRenderer in this case. We will also instantiate fragments right away to load BIM models. :::tip Fragments? If you are not familiar with fragments, check out the IfcLoader tutorial! ::: ```js const components = new OBC.Components(); const worlds = components.get(OBC.Worlds); const world = worlds.create< OBC.SimpleScene, OBC.OrthoPerspectiveCamera, OBF.PostproductionRenderer >(); world.scene = new OBC.SimpleScene(components); world.scene.setup(); world.scene.three.background = null; const container = document.getElementById("container")!; world.renderer = new OBF.PostproductionRenderer(components, container); world.camera = new OBC.OrthoPerspectiveCamera(components); await world.camera.controls.setLookAt(68, 23, -8.5, 21.5, -5.5, 23); components.init(); const grids = components.get(OBC.Grids); const grid = grids.create(world); grid.config.color.set(0x666666); ``` We'll make the background of the scene transparent so that it looks good in our docs page, but you don't have to do that in your app! ```js world.scene.three.background = null; ``` We'll also set up the update logic for the renderer in manual mode (if enabled). ```js const updateIfManualMode = () => { if (world.renderer!.mode === OBC.RendererMode.MANUAL) { world.renderer!.needsUpdate = true; } }; world.camera.controls.addEventListener("update", updateIfManualMode); world.renderer.onResize.add(updateIfManualMode); ``` ### 🛠️ Setting Up Fragments Now, let's configure the FragmentsManager. This will allow us to load models effortlessly and start manipulating them with ease: ```js const githubUrl = "https://thatopen.github.io/engine_fragment/resources/worker.mjs"; const fetchedUrl = await fetch(githubUrl); const workerBlob = await fetchedUrl.blob(); const workerFile = new File([workerBlob], "worker.mjs", { type: "text/javascript", }); const workerUrl = URL.createObjectURL(workerFile); const fragments = components.get(OBC.FragmentsManager); fragments.init(workerUrl); world.camera.controls.addEventListener("rest", () => fragments.core.update(true), ); world.onCameraChanged.add((camera) => { for (const [, model] of fragments.list) { model.useCamera(camera.three); } fragments.core.update(true); }); fragments.list.onItemSet.add(({ value: model }) => { model.useCamera(world.camera.three); world.scene.three.add(model.object); fragments.core.update(true); }); // Remove z fighting fragments.core.models.materials.list.onItemSet.add(({ value: material }) => { if (!("isLodMaterial" in material && material.isLodMaterial)) { material.polygonOffset = true; material.polygonOffsetUnits = 1; material.polygonOffsetFactor = Math.random(); } }); ``` ### 📂 Loading Fragments Models With the core setup complete, it's time to load a Fragments model into our scene. Fragments are optimized for fast loading and rendering, making them ideal for large-scale 3D models. :::info Where can I find Fragment files? You can use the sample Fragment files available in our repository for testing. If you have an IFC model you'd like to convert to Fragments, check out the IfcImporter tutorial for detailed instructions. ::: ```js const fragPaths = ["https://thatopen.github.io/engine_components/resources/frags/school_arq.frag"]; await Promise.all( fragPaths.map(async (path) => { const modelId = path.split("/").pop()?.split(".").shift(); if (!modelId) return null; const file = await fetch(path); const buffer = await file.arrayBuffer(); return fragments.core.load(buffer, { modelId }); }), ); ``` ### 🎬 Turning on the Postproduction --- Now we will activate the postproduction effect and enable the visibility for postproduction layer. ```js world.renderer.postproduction.enabled = true; world.dynamicAnchor = false; // We will exclude LOD objects from all the passes except the base pass: fragments.core.models.materials.list.onItemSet.add(({ value: material }) => { const isLod = "isLodMaterial" in material && material.isLodMaterial; if (isLod) { world.renderer!.postproduction.basePass.isolatedMaterials.push(material); } }); // Outline const model = fragments.list.values().next().value!; const outliner = components.get(OBF.Outliner); outliner.world = world; const walls = await model.getItemsOfCategories([/IFCWALL/]); const wallsIds = walls.IFCWALL; const [wall1, wall2] = wallsIds; outliner.addItems({ [model.modelId]: new Set([wall1, wall2]) }); ``` ### 📹 Switching the camera --- If you are using an orthoperspective camera, you need to update the postproduction renderer camera: ```js // To update the camera // window.addEventListener("dblclick", () => { // world.camera.projection.toggle(); // model.useCamera(world.camera.three); // world.renderer.postproduction.updateCamera(); // }); ``` ### ⏱️ Measuring the performance (optional) --- We'll use the [Stats.js](https://github.com/mrdoob/stats.js) to measure the performance of our app. We will add it to the top left corner of the viewport. This way, we'll make sure that the memory consumption and the FPS of our app are under control. ```js const stats = new Stats(); stats.showPanel(2); document.body.append(stats.dom); stats.dom.style.left = "0px"; stats.dom.style.zIndex = "unset"; world.renderer.onBeforeUpdate.add(() => stats.begin()); world.renderer.onAfterUpdate.add(() => stats.end()); ``` ### 🧩 Adding some UI --- We will use the `@thatopen/ui` library to add some simple and cool UI elements to our app. First, we need to call the `init` method of the `BUI.Manager` class to initialize the library: ```js BUI.Manager.init(); ``` Now we will add some UI to control some of the most common postproduction parameters. For more information about the UI library, you can check the specific documentation for it! ```js const { aoPass, outlinePass, edgesPass, defaultAoParameters } = world.renderer.postproduction; const pdParameters = { lumaPhi: 10, depthPhi: 2, normalPhi: 3, radius: 4, radiusExponent: 1, rings: 2, samples: 16, }; aoPass.updateGtaoMaterial(defaultAoParameters); aoPass.updatePdMaterial(pdParameters); const cube = new THREE.Mesh( new THREE.BoxGeometry(1, 1, 1), new THREE.MeshLambertMaterial({ color: 0x00ff00 }), ); cube.position.set(10, 0, 0); world.scene.three.add(cube); world.renderer.postproduction.excludedObjectsPass.addExcludedMaterial( cube.material, ); const panel = BUI.Component.create<BUI.PanelSection>(() => { return BUI.html` <bim-panel active label="Postproduction Tutorial" class="options-menu"> <bim-panel-section label="General"> <bim-checkbox checked label="Postproduction enabled" @change="${({ target }: { target: BUI.Checkbox }) => { world.renderer!.postproduction.enabled = target.value; updateIfManualMode(); }}"> </bim-checkbox> <bim-checkbox checked label="Outlines enabled" ?checked=${world.renderer!.postproduction.outlinesEnabled} @change="${({ target }: { target: BUI.Checkbox }) => { world.renderer!.postproduction.outlinesEnabled = target.value; updateIfManualMode(); }}"> </bim-checkbox> <bim-checkbox checked label="Excluded objects enabled" ?checked=${world.renderer!.postproduction.excludedObjectsEnabled} @change="${({ target }: { target: BUI.Checkbox }) => { world.renderer!.postproduction.excludedObjectsEnabled = target.value; updateIfManualMode(); }}"> </bim-checkbox> <bim-checkbox checked label="SMAA enabled" ?checked=${world.renderer!.postproduction.smaaEnabled} @change="${({ target }: { target: BUI.Checkbox }) => { world.renderer!.postproduction.smaaEnabled = target.value; updateIfManualMode(); }}"> </bim-checkbox> <bim-dropdown required label="Postproduction style" @change="${({ target }: { target: BUI.Dropdown }) => { const result = target.value[0] as OBF.PostproductionAspect; world.renderer!.postproduction.style = result; updateIfManualMode(); }}"> <bim-option checked label="Basic" value="${OBF.PostproductionAspect.COLOR}"></bim-option> <bim-option label="Pen" value="${OBF.PostproductionAspect.PEN}"></bim-option> <bim-option label="Shadowed Pen" value="${OBF.PostproductionAspect.PEN_SHADOWS}"></bim-option> <bim-option label="Color Pen" value="${OBF.PostproductionAspect.COLOR_PEN}"></bim-option> <bim-option label="Color Shadows" value="${OBF.PostproductionAspect.COLOR_SHADOWS}"></bim-option> <bim-option label="Color Pen Shadows" value="${OBF.PostproductionAspect.COLOR_PEN_SHADOWS}"></bim-option> </bim-dropdown> </bim-panel-section> <bim-panel-section label="Edges"> <bim-number-input slider step="0.1" label="Width" value="${world.renderer!.postproduction.edgesPass.width}" min="1" max="3" @change="${({ target }: { target: BUI.NumberInput }) => { world.renderer!.postproduction.edgesPass.width = target.value; updateIfManualMode(); }}"> </bim-number-input> <bim-color-input label="Edges color" color="#${edgesPass.color.getHexString()}" @input="${({ target }: { target: BUI.ColorInput }) => { edgesPass.color.set(target.value.color); updateIfManualMode(); }}"> </bim-color-input> <bim-dropdown label="Edges Mode" @change="${({ target }: { target: BUI.Dropdown }) => { edgesPass.mode = target.value[0] as OBF.EdgeDetectionPassMode; updateIfManualMode(); }}"> <bim-option checked label="Default" value="${OBF.EdgeDetectionPassMode.DEFAULT}"></bim-option> <bim-option label="Global" value="${OBF.EdgeDetectionPassMode.GLOBAL}"></bim-option> </bim-dropdown> </bim-panel-section> <bim-panel-section label="Outline"> <bim-number-input slider step="0.1" label="Outline thickness" value="${outlinePass.thickness}" min="1" max="10" @change="${({ target }: { target: BUI.NumberInput }) => { outlinePass.thickness = target.value; updateIfManualMode(); }}"> </bim-number-input> <bim-number-input slider step="0.01" label="Fill opacity" value="${outlinePass.fillOpacity}" min="0" max="1" @change="${({ target }: { target: BUI.NumberInput }) => { outlinePass.fillOpacity = target.value; updateIfManualMode(); }}"> </bim-number-input> <bim-color-input label="Line color" color="#${outlinePass.outlineColor.getHexString()}" @input="${({ target }: { target: BUI.ColorInput }) => { outlinePass.outlineColor.set(target.value.color); updateIfManualMode(); }}"> </bim-color-input> <bim-color-input label="Fill color" color="#${outlinePass.fillColor.getHexString()}" @input="${({ target }: { target: BUI.ColorInput }) => { outlinePass.fillColor.set(target.value.color); updateIfManualMode(); }}"> </bim-color-input> </bim-panel-section> <bim-panel-section label="Gloss"> <bim-checkbox label="Enabled" ?checked=${world.renderer!.postproduction.glossEnabled} @change="${({ target }: { target: BUI.Checkbox }) => { world.renderer!.postproduction.glossEnabled = target.value; updateIfManualMode(); }}"> </bim-checkbox> <bim-number-input slider step="0.01" label="Min gloss" value="${world.renderer!.postproduction.glossPass.minGloss}" min="-1" max="1" @change="${({ target }: { target: BUI.NumberInput }) => { world.renderer!.postproduction.glossPass.minGloss = target.value; updateIfManualMode(); }}"> </bim-number-input> <bim-number-input slider step="0.01" label="Max gloss" value="${world.renderer!.postproduction.glossPass.maxGloss}" min="-1" max="1" @change="${({ target }: { target: BUI.NumberInput }) => { world.renderer!.postproduction.glossPass.maxGloss = target.value; updateIfManualMode(); }}"> </bim-number-input> <bim-number-input slider step="0.01" label="Gloss exponent" value="${world.renderer!.postproduction.glossPass.glossExponent}" min="0.1" max="20" @change="${({ target }: { target: BUI.NumberInput }) => { world.renderer!.postproduction.glossPass.glossExponent = target.value; updateIfManualMode(); }}"> </bim-number-input> <bim-number-input slider step="0.01" label="Fresnel exponent" value="${world.renderer!.postproduction.glossPass.fresnelExponent}" min="0.1" max="50" @change="${({ target }: { target: BUI.NumberInput }) => { world.renderer!.postproduction.glossPass.fresnelExponent = target.value; updateIfManualMode(); }}"> </bim-number-input> <bim-number-input slider step="0.01" label="Gloss factor" value="${world.renderer!.postproduction.glossPass.glossFactor}" min="0" max="1" @change="${({ target }: { target: BUI.NumberInput }) => { world.renderer!.postproduction.glossPass.glossFactor = target.value; updateIfManualMode(); }}"> </bim-number-input> <bim-number-input slider step="0.01" label="Fresnel factor" value="${world.renderer!.postproduction.glossPass.fresnelFactor}" min="0" max="10" @change="${({ target }: { target: BUI.NumberInput }) => { world.renderer!.postproduction.glossPass.fresnelFactor = target.value; updateIfManualMode(); }}"> </bim-number-input> </bim-panel-section> <bim-panel-section label="Manual mode"> <bim-checkbox label="Enabled" ?checked=${world.renderer!.mode === OBC.RendererMode.MANUAL} @change="${({ target }: { target: BUI.Checkbox }) => { world.renderer!.mode = target.value ? OBC.RendererMode.MANUAL : OBC.RendererMode.AUTO; }}"> </bim-checkbox> <bim-number-input label="Delay" value="${world.renderer!.manualModeDelay}" min="10" max="1000" @change="${({ target }: { target: BUI.NumberInput }) => { world.renderer!.manualModeDelay = target.value; updateIfManualMode(); }}"> </bim-number-input> <bim-checkbox label="Turn off on manual mode" ?checked=${world.renderer!.turnOffOnManualMode} @change="${({ target }: { target: BUI.Checkbox }) => { world.renderer!.turnOffOnManualMode = target.value; updateIfManualMode(); }}"> </bim-checkbox> <bim-dropdown label="Default style" @change="${({ target }: { target: BUI.Dropdown }) => { const result = target.value[0] as OBF.PostproductionAspect; world.renderer!.manualDefaultStyle = result; updateIfManualMode(); }}"> <bim-option label="Basic" value="${OBF.PostproductionAspect.COLOR}"></bim-option> <bim-option label="Pen" value="${OBF.PostproductionAspect.PEN}"></bim-option> <bim-option label="Pen Shadows" value="${OBF.PostproductionAspect.PEN_SHADOWS}"></bim-option> <bim-option label="Color Pen" value="${OBF.PostproductionAspect.COLOR_PEN}"></bim-option> <bim-option label="Color Shadows" value="${OBF.PostproductionAspect.COLOR_SHADOWS}"></bim-option> <bim-option label="Color Pen Shadows" value="${OBF.PostproductionAspect.COLOR_PEN_SHADOWS}"></bim-option> </bim-dropdown> </bim-panel-section> <bim-panel-section label="Ambient Occlusion"> <bim-checkbox checked label="Screen Space Radius" ?checked=${defaultAoParameters.screenSpaceRadius} @change="${({ target }: { target: BUI.Checkbox }) => { defaultAoParameters.screenSpaceRadius = target.value; aoPass.updateGtaoMaterial(defaultAoParameters); updateIfManualMode(); }}"> </bim-checkbox> <bim-number-input slider step="0.01" label="Blend intensity" value="${aoPass.blendIntensity}" min="0" max="1" @change="${({ target }: { target: BUI.NumberInput }) => { aoPass.blendIntensity = target.value; updateIfManualMode(); }}"> </bim-number-input> <bim-number-input slider step="0.01" label="Radius" value="${defaultAoParameters.radius}" min="0.01" max="1" @change="${({ target }: { target: BUI.NumberInput }) => { defaultAoParameters.radius = target.value; aoPass.updateGtaoMaterial(defaultAoParameters); updateIfManualMode(); }}"> </bim-number-input> <bim-number-input slider step="0.01" label="Distance exponent" value="${defaultAoParameters.distanceExponent}" min="1" max="10" @change="${({ target }: { target: BUI.NumberInput }) => { defaultAoParameters.distanceExponent = target.value; aoPass.updateGtaoMaterial(defaultAoParameters); updateIfManualMode(); }}"> </bim-number-input> <bim-number-input slider step="0.01" label="Thickness" value="${defaultAoParameters.thickness}" min="0.01" max="10" @change="${({ target }: { target: BUI.NumberInput }) => { defaultAoParameters.thickness = target.value; aoPass.updateGtaoMaterial(defaultAoParameters); updateIfManualMode(); }}"> </bim-number-input> <bim-number-input slider step="0.01" label="Distance falloff" value="${defaultAoParameters.distanceFallOff}" min="0" max="1" @change="${({ target }: { target: BUI.NumberInput }) => { defaultAoParameters.distanceFallOff = target.value; aoPass.updateGtaoMaterial(defaultAoParameters); updateIfManualMode(); }}"> </bim-number-input> <bim-number-input slider step="0.01" label="Scale" value="${defaultAoParameters.scale}" min="0.01" max="10" @change="${({ target }: { target: BUI.NumberInput }) => { defaultAoParameters.scale = target.value; aoPass.updateGtaoMaterial(defaultAoParameters); updateIfManualMode(); }}"> </bim-number-input> <bim-number-input slider step="1" label="Samples" value="${defaultAoParameters.samples}" min="2" max="32" @change="${({ target }: { target: BUI.NumberInput }) => { defaultAoParameters.samples = target.value; aoPass.updateGtaoMaterial(defaultAoParameters); updateIfManualMode(); }}"> </bim-number-input> <bim-number-input slider step="0.1" label="PD Luma Phi" value="${pdParameters.lumaPhi}" min="0" max="20" @change="${({ target }: { target: BUI.NumberInput }) => { pdParameters.lumaPhi = target.value; aoPass.updatePdMaterial(pdParameters); updateIfManualMode(); }}"> </bim-number-input> <bim-number-input slider step="0.1" label="PD Depth Phi" value="${pdParameters.depthPhi}" min="0.01" max="20" @change="${({ target }: { target: BUI.NumberInput }) => { pdParameters.depthPhi = target.value; aoPass.updatePdMaterial(pdParameters); updateIfManualMode(); }}"> </bim-number-input> <bim-number-input slider step="0.1" label="PD Normal Phi" value="${pdParameters.normalPhi}" min="0.01" max="20" @change="${({ target }: { target: BUI.NumberInput }) => { pdParameters.normalPhi = target.value; aoPass.updatePdMaterial(pdParameters); updateIfManualMode(); }}"> </bim-number-input> <bim-number-input slider step="1" label="PD Radius" value="${pdParameters.radius}" min="0" max="32" @change="${({ target }: { target: BUI.NumberInput }) => { pdParameters.radius = target.value; aoPass.updatePdMaterial(pdParameters); updateIfManualMode(); }}"> </bim-number-input> <bim-number-input slider step="0.1" label="PD Radius Exponent" value="${pdParameters.radiusExponent}" min="0.1" max="4" @change="${({ target }: { target: BUI.NumberInput }) => { pdParameters.radiusExponent = target.value; aoPass.updatePdMaterial(pdParameters); updateIfManualMode(); }}"> </bim-number-input> <bim-number-input slider step="0.125" label="PD Rings" value="${pdParameters.rings}" min="1" max="16" @change="${({ target }: { target: BUI.NumberInput }) => { pdParameters.rings = target.value; aoPass.updatePdMaterial(pdParameters); updateIfManualMode(); }}"> </bim-number-input> <bim-number-input slider step="1" label="PD Samples" value="${pdParameters.samples}" min="2" max="32" @change="${({ target }: { target: BUI.NumberInput }) => { pdParameters.samples = target.value; aoPass.updatePdMaterial(pdParameters); updateIfManualMode(); }}"> </bim-number-input> </bim-panel-section> </bim-panel> `; }); document.body.append(panel); ``` And we will make some logic that adds a button to the screen when the user is visiting our app from their phone, allowing to show or hide the menu. Otherwise, the menu would make the app unusable. ```js const button = BUI.Component.create<BUI.PanelSection>(() => { return BUI.html` <bim-button class="phone-menu-toggler" icon="solar:settings-bold" @click="${() => { if (panel.classList.contains("options-menu-visible")) { panel.classList.remove("options-menu-visible"); } else { panel.classList.add("options-menu-visible"); } }}"> </bim-button> `; }); document.body.append(button); // ``` // ### 🎉 Wrap up // --- // That's it! You have created an app that looks great thanks to postproduction and exposes a menu to allow the user control it in real time. //