| sidebar_position | 2 | |||||
|---|---|---|---|---|---|---|
| title | Compounded Drawing | |||||
| sidebar_label | Compounded Drawing | |||||
| description | Learn how to use compounds for efficient rendering of complex geometric patterns and assemblies | |||||
| tags |
|
import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import BitByBitRenderCanvas from '@site/src/components/BitByBitRenderCanvas';
Building on the fundamental concepts from the compound shapes introduction, this tutorial demonstrates how to apply compound techniques to complex, real-world scenarios. We'll explore a sophisticated example that creates a parametric hexagonal pattern on a curved surface, showing how compounds dramatically improve rendering performance while enabling organized management of complex geometries.
When working with parametric designs that generate hundreds or thousands of individual geometric elements, performance becomes critical. Consider a scenario where you need to create a hexagonal frame pattern across a curved surface - each hexagon consists of multiple faces, wires, and edges that would normally require individual rendering calls.
Without compounds, such patterns would overwhelm the graphics pipeline with separate draw calls for each element. The solution lies in strategically grouping related elements into compounds before rendering, transforming what could be thousands of individual operations into just a few efficient compound draws.
The example below demonstrates this principle through a complex workflow that creates a parametric hexagonal pattern on a lofted surface. This real-world scenario showcases several advanced compound techniques:
Surface Creation: First, we generate a complex curved surface by lofting between three interpolated wire curves with different point configurations.
Pattern Generation: The surface is subdivided into hexagonal patterns using two different scaling approaches - one regular and one scaled - creating complementary geometric sets.
Strategic Grouping: Rather than rendering each hexagonal frame individually, we use pattern filtering to group frames into logical sets, then create separate compounds for each group.
Efficient Rendering: Finally, we render only two compound entities instead of potentially hundreds of individual faces, with different materials applied to each compound group.
bottomPointsmiddlePointstopPointsbottomWiremiddleWiretopWirewiresListloftedSurfacemainFacenrHexagonsUnrHexagonsVscalePatternhexWires1hexWires2reversedWiresiframesfirstFramesCompoundsecondFramesCompoundmaterial1material2bottomPoints[[-30, 0, -20], [-10, 0, 0], [0, 0, 10], [10, 0, 0],[30, 0, -20]]middlePoints[[-20, 20, -10], [-5, 20, 15], [0, 20, 20], [5, 20, 15],[20, 20, -10]]topPoints[[-15, 30, 0], [-5, 30, 5], [0, 30, 10], [5, 30, 5],[15, 30, 0]]bottomWirebottomPointsFALSE0.1middleWiremiddlePointsFALSE0.1topWiretopPointsFALSE0.1wiresListbottomWiremiddleWiretopWireloftedSurfacewiresListFALSEmainFaceloftedSurface0nrHexagonsU28nrHexagonsV10scalePattern0.7hexWires1mainFacenrHexagonsUnrHexagonsVFALSE00FALSEFALSEFALSEFALSEhexWires2mainFacenrHexagonsUnrHexagonsVFALSEscalePatternscalePattern00FALSEFALSEFALSEFALSEreversedWiresi1hexWires21INSERTLASTreversedWiresGETFROM_STARThexWires2iframesi1hexWires11INSERTLASTframesGETFROM_STARThexWires1iGETFROM_STARTreversedWiresiFALSEfirstFramesCompoundframes[true, true, false]secondFramesCompoundframes[false,false,true]material1First#9155ff#0000000.150.91FALSE3material2Second#000000#0000000.90.151FALSE3firstFramesCompound0.01material1TRUE#00000010secondFramesCompound0.01material2TRUE#00000010-100-100-1003#ffffff#ffffff1024TRUE0TRUE0.20.00010.00210000'default'10000.10.7TRUE442244015011791000100010001000340040010100.450.50.5FALSE#ffffff#ffffff#050506#627a9d'to top'0100","version":"0.20.7","type":"blockly"}} title="Compounding hex frames into single entity will render faster" /> {\n // Define point sets for three different wire levels\n const bottomPoints: Point3[] = [\n [-30, 0, -20],\n [-10, 0, 0],\n [0, 0, 10],\n [10, 0, 0],\n [30, 0, -20]\n ];\n\n const middlePoints: Point3[] = [\n [-20, 20, -10],\n [-5, 20, 15],\n [0, 20, 20],\n [5, 20, 15],\n [20, 20, -10]\n ];\n\n const topPoints: Point3[] = [\n [-15, 30, 0],\n [-5, 30, 5],\n [0, 30, 10],\n [5, 30, 5],\n [15, 30, 0]\n ];\n\n // Create wires by interpolating points\n const bottomWireOptions = new InterpolationDto();\n bottomWireOptions.points = bottomPoints;\n bottomWireOptions.periodic = false;\n bottomWireOptions.tolerance = 0.1;\n const bottomWire = await wire.interpolatePoints(bottomWireOptions);\n\n const middleWireOptions = new InterpolationDto();\n middleWireOptions.points = middlePoints;\n middleWireOptions.periodic = false;\n middleWireOptions.tolerance = 0.1;\n const middleWire = await wire.interpolatePoints(middleWireOptions);\n\n const topWireOptions = new InterpolationDto();\n topWireOptions.points = topPoints;\n topWireOptions.periodic = false;\n topWireOptions.tolerance = 0.1;\n const topWire = await wire.interpolatePoints(topWireOptions);\n\n // Create list of wires for lofting\n const wiresList = [bottomWire, middleWire, topWire];\n\n // Loft the wires to create a surface\n const loftOptions = new LoftDto();\n loftOptions.shapes = wiresList;\n loftOptions.makeSolid = false;\n const loftedSurface = await operations.loft(loftOptions);\n\n // Get the face from the lofted surface\n const getFaceOptions = new ShapeIndexDto();\n getFaceOptions.shape = loftedSurface;\n getFaceOptions.index = 0;\n const mainFace = await face.getFace(getFaceOptions);\n\n // Subdivision parameters\n const nrHexagonsU = 28;\n const nrHexagonsV = 10;\n const scalePattern = [0.7];\n\n // Create first hexagon subdivision (regular pattern)\n const hexSubdivision1Options = new FaceSubdivideToHexagonWiresDto();\n hexSubdivision1Options.shape = mainFace;\n hexSubdivision1Options.nrHexagonsU = nrHexagonsU;\n hexSubdivision1Options.nrHexagonsV = nrHexagonsV;\n const hexWires1 = await face.subdivideToHexagonWires(hexSubdivision1Options);\n\n // Create second hexagon subdivision (scaled pattern)\n const hexSubdivision2Options = new FaceSubdivideToHexagonWiresDto();\n hexSubdivision2Options.shape = mainFace;\n hexSubdivision2Options.nrHexagonsU = nrHexagonsU;\n hexSubdivision2Options.nrHexagonsV = nrHexagonsV;\n hexSubdivision2Options.scalePatternU = scalePattern;\n hexSubdivision2Options.scalePatternV = scalePattern;\n const hexWires2 = await face.subdivideToHexagonWires(hexSubdivision2Options);\n\n // Reverse the wires from the second subdivision using for loop\n const reversedWiresPromises: Promise[] = [];\n for (const hexWire of hexWires2) {\n const reversedWire = wire.reversedWire({ shape: hexWire });\n reversedWiresPromises.push(reversedWire);\n }\n\n const reversedWires = await Promise.all(reversedWiresPromises);\n\n // Combine both wire sets - equivalent to flip lists operation in Rete\n const frameWiresGrouped = hexWires1.map((h, i) => [h, reversedWires[i]]);\n\n // Create frames\n const framePromises = frameWiresGrouped.map(f => {\n const faceFromWires2Options = new FaceFromWiresDto();\n faceFromWires2Options.shapes = f;\n faceFromWires2Options.planar = false;\n return face.createFaceFromWires(faceFromWires2Options);\n });\n\n const frames = await Promise.all(framePromises);\n\n const firstFrames = await bitbybit.lists.getByPattern({\n list: frames,\n pattern: [true, true, false]\n });\n\n const secondFrames = await bitbybit.lists.getByPattern({\n list: frames,\n pattern: [false, false, true]\n })\n\n // Create first compound from the first pattern of hexagon faces\n const compoundOptions = new CompoundShapesDto();\n compoundOptions.shapes = firstFrames;\n const firstCompound = await compound.makeCompound(compoundOptions);\n\n // Create second compound from the first pattern of hexagon faces\n compoundOptions.shapes = secondFrames;\n const secondCompound = await compound.makeCompound(compoundOptions);\n\n // Create materials for rendering\n const firstMaterial = new PBRMetallicRoughnessDto();\n firstMaterial.name = \"Blue Material\";\n firstMaterial.baseColor = \"#9155ff\";\n firstMaterial.metallic = 0.1;\n firstMaterial.roughness = 0.9;\n firstMaterial.backFaceCulling = false;\n firstMaterial.zOffset = 3;\n const blueMatResult = material.pbrMetallicRoughness.create(firstMaterial);\n\n // Create drawing options for the first frames\n const firstDrawOptions = new DrawOcctShapeOptions();\n firstDrawOptions.drawEdges = true;\n firstDrawOptions.edgeColour = \"#000000\";\n firstDrawOptions.edgeWidth = 10;\n firstDrawOptions.faceMaterial = blueMatResult;\n\n bitbybit.draw.drawAnyAsync({\n entity: firstCompound,\n options: firstDrawOptions\n });\n\n // Create materials for rendering\n const secondMaterial = new PBRMetallicRoughnessDto();\n secondMaterial.name = \"Black Material\";\n secondMaterial.baseColor = \"#000000\";\n secondMaterial.metallic = 0.9;\n secondMaterial.roughness = 0.23;\n secondMaterial.backFaceCulling = false;\n secondMaterial.zOffset = 3;\n const secondMatResult = material.pbrMetallicRoughness.create(secondMaterial);\n\n // Create drawing options for the first frames\n const secondDrawOptions = new DrawOcctShapeOptions();\n secondDrawOptions.drawEdges = true;\n secondDrawOptions.edgeColour = \"#000000\";\n secondDrawOptions.edgeWidth = 10;\n secondDrawOptions.faceMaterial = secondMatResult;\n\n bitbybit.draw.drawAnyAsync({\n entity: secondCompound,\n options: secondDrawOptions\n });\n\n // Set up scene lighting and camera\n const skyboxOptions = new SkyboxDto();\n skyboxOptions.skybox = skyboxEnum.city;\n skyboxOptions.hideSkybox = true;\n scene.enableSkybox(skyboxOptions);\n\n const dirLightOptions = new DirectionalLightDto();\n dirLightOptions.intensity = 3;\n scene.drawDirectionalLight(dirLightOptions);\n\n const gradientBackgroundOptions = new SceneTwoColorLinearGradientDto();\n gradientBackgroundOptions.colorFrom = \"#050506\";\n gradientBackgroundOptions.colorTo = \"#627a9d\";\n gradientBackgroundOptions.direction = gradientDirectionEnum.toTop;\n scene.twoColorLinearGradient(gradientBackgroundOptions);\n\n const cameraConfigurationOptions = new CameraConfigurationDto();\n cameraConfigurationOptions.position = [44, 30, 44];\n cameraConfigurationOptions.lookAt = [0, 15, 0];\n scene.adjustActiveArcRotateCamera(cameraConfigurationOptions);\n\n const gridOptions = new SceneDrawGridMeshDto();\n bitbybit.draw.drawGridMesh(gridOptions);\n}\n\n// Execute the function\nstart();","version":"0.20.7","type":"typescript"}} title="Compounding hex frames into single entity will render faster" />Let's examine how this complex example demonstrates advanced compound usage by breaking down the key phases:
The workflow begins by creating a complex curved surface that will serve as the foundation for our hexagonal pattern. This involves several steps:
-
Point Definition: Three distinct sets of 3D points define the profile curves at different heights - bottom, middle, and top levels with varying shapes and positions.
-
Wire Interpolation: Each point set is converted into smooth interpolated wires using
interpolatePoints(), creating the skeletal framework for the surface. -
Surface Lofting: The three wires are lofted together using
operations.loft()to create a complex curved surface that transitions smoothly between the different profile shapes.
This foundational surface provides the 3D canvas on which our hexagonal pattern will be projected.
The core innovation of this example lies in creating two complementary hexagonal patterns on the same surface:
-
Primary Pattern: The surface is subdivided using
face.subdivideToHexagonWires()with standard parameters, creating an array of hexagonal wire outlines. -
Secondary Pattern: A second subdivision is created using the same parameters but with added scaling factors (
scalePatternUandscalePatternVset to 0.7), producing smaller hexagons that nest within the primary pattern. -
Wire Direction Management: The wires from the secondary pattern are reversed using
wire.reversedWire()to ensure proper face creation when combined with the primary pattern wires.
The two wire sets are combined to create individual hexagonal frames:
-
Frame Assembly: Each hexagon frame is created by pairing a wire from the primary pattern with the corresponding reversed wire from the secondary pattern, then using
face.createFaceFromWires()to create the actual geometric face. -
Pattern-Based Filtering: Using
bitbybit.lists.getByPattern(), the frames are separated into logical groups. The patterns[true, true, false]and[false, false, true]create alternating groupings that distribute the frames across different visual layers. -
Compound Creation: Each filtered group is consolidated into a separate compound using
compound.makeCompound(), transforming hundreds of individual faces into just two cohesive entities.
The final phase demonstrates the rendering advantages of compounds:
-
Material Assignment: Different materials are created for each compound group - a purple metallic material for the first compound and a black metallic material for the second.
-
Single Draw Calls: Instead of rendering each hexagonal frame individually (which could be hundreds of draw calls), only two
bitbybit.draw.drawAnyAsync()calls are made - one for each compound. -
Unified Processing: The graphics pipeline processes each compound as a single mesh, dramatically reducing computational overhead while maintaining the visual complexity of the pattern.
This example demonstrates several critical performance optimizations that compounds enable:
GPU Efficiency: Without compounds, a pattern with 280 hexagonal frames would require 280 separate GPU draw calls. With compounds, this reduces to just 2 draw calls - a 99% reduction in rendering overhead.
Memory Consolidation: Individual geometries each require separate memory allocation and management. Compounds consolidate this into unified memory blocks, reducing fragmentation and improving cache efficiency.
Transformation Performance: If you needed to rotate or translate this entire pattern, compounds allow you to transform both groups with just two operations instead of potentially hundreds of individual transformations.
Material Management: Rather than managing materials for hundreds of individual objects, you only need to manage materials for the compound groups, simplifying state changes and reducing GPU state switching overhead.
This example illustrates several important patterns for effective compound usage:
-
Logical Grouping: Group geometries that share similar properties (materials, transformation behavior, visibility states) into the same compound.
-
Pattern-Based Organization: Use systematic filtering (like the pattern-based selection shown) to create logical hierarchies rather than arbitrary groupings.
-
Strategic Timing: Create compounds after all individual geometry generation is complete but before rendering operations begin.
-
Balance Complexity: While compounds improve performance, extremely large compounds can become unwieldy. Consider breaking very large patterns into multiple logical compound groups.
-
Material Strategy: Design your material workflow around compound groupings rather than individual geometries to maximize the rendering efficiency gains.
This advanced example demonstrates how compounds scale from simple grouping mechanisms to sophisticated performance optimization tools in complex parametric design workflows.