0

I have been trying for some time to implement an octree system in a class project aimed at 3D image rendering optimization. But I always block in the implementation of this one, indeed I have a basic class center that I must adapt to an octree system and I have an Octree and OctreeNOde class which implements the octree system and the creation of child nodes. But I still get code errors.

The goal is to divide the texture into octree to optimize rendering. But I get errors on methods traverse:

CalculateAverageColor of type Method 'CalculateAverageColor' has 2 parameter(s) but is invoked with 1 argument(s),Method 'Traverse' has 2 parameter(s) but is invoked with 1 argument (s).

I am showing my 3 classes here:

using ...

public class Center : MonoBehaviour
{
    private float _divisionResolution;

    public int Channel { get; set; } = 0;
    public int nbChannel;
    public int[] listChannel = {1};
    private int _nbChannelGlobal = 1;
    public int sizeChannel;
    public IDictionary<int, byte[]> lut = new Dictionary<int, byte[]>();
    internal Data _data;
    private int _nbOfSliceUsed;
    private bool _textureCreated = false;
    public bool lutChange = true;
    public int[] Size { get; set; }
    public Texture3D Texture { get; set; }
    private Octree _colorOctree;
    public OctreeNode octree;

    public void SetData(Data data, float divisionResolution=1, int nbChannelsGlobal=1)
{
    nbChannel = data._matriceImages[0].channels();
    sizeChannel = data._matriceImages[0].depth();
    // Extraction des données.
    _data = data;
    _nbChannelGlobal = nbChannelsGlobal;

    // Initialisation de la taille de la matrice de référence pour choisir les plan de coupe.
    transform.localScale = new Vector3((float)_data.Size["width"]/_data.Size.Values.Max(), (float)_data.Size["height"]/_data.Size.Values.Max(),
    (float)_data.Size["depth"]/_data.Size.Values.Max());

    // Créer l'octree
    Vector3Int dimensions = new Vector3Int(_data.Size["width"], _data.Size["height"], _data.Size["depth"]);
    Vector3Int position = Vector3Int.zero;
    _colorOctree = new Octree(position, dimensions);
    
    // Calculer la couleur moyenne pour chaque noeud de l'octree
    _colorOctree.CalculateAverageColor((node, color) => 
    {
        _data.SetPixelTexture(Texture, nbChannel, _nbChannelGlobal, listChannel, lut);
        Texture.Apply(false, true);
        color.r = 0f;
        color.g = 0f;
        color.b = 0f;
        int count = 0;
        for (int z = node.Position.z; z < node.Position.z + node.Dimensions.z; z++)
        {
            for (int y = node.Position.y; y < node.Position.y + node.Dimensions.y; y++)
            {
                for (int x = node.Position.x; x < node.Position.x + node.Dimensions.x; x++)
                {
                    count++;
                    color.r += Texture.GetPixel(x, y, z).r;
                    color.g += Texture.GetPixel(x, y, z).g;
                    color.b += Texture.GetPixel(x, y, z).b;
                }
            }
        }
        color /= count;
    });
}

private void Update()
{
    if ((_data != null && !_textureCreated) && Keyboard.current.kKey.isPressed || (lutChange && _data != null))
    {
        CreateTexture();
        _textureCreated = true;
        lutChange = false;
    }
}

private void CreateTexture()
{
    const TextureFormat format = TextureFormat.RGB24;
    
    // Création de la texture.
    Texture = new Texture3D(_data.Size["width"], _data.Size["height"], _data.Size["depth"], format, false);

    // Update de l'octree avec la nouvelle texture
    _colorOctree.Update(new Vector3Int(0, 0, 0),
        new Vector3Int(_data.Size["width"], _data.Size["height"], _data.Size["depth"]),
        (node, color) =>
        {
            color.r = 0f;
            color.g = 0f;
            color.b = 0f;
            int count = 0;
            for (int z = node.position.z; z < node.position.z + node.dimensions.z; z++)
            {
                for (int y = node.position.y; y < node.position.y + node.dimensions.y; y++)
                {
                    for (int x = node.position.x; x < node.position.x + node.dimensions.x; x++)
                    {
                        count++;
                        color.r += Texture.GetPixel(x, y, z).r;
                        color.g += Texture.GetPixel(x, y, z).g;
                        color.b += Texture.GetPixel(x, y, z).b;
                    }
                }
            }

            color /= count;
        });
    // Définir les données de la texture
    _colorOctree.Traverse((node, color) =>
    {
        int startIndex = ((node.position.z * Texture.width + node.Position.y) * Texture.width + node.Position.x) * 3;
        int count = 0;
        for (int z = node.Position.z; z < node.Position.z + node.Dimensions.z; z++)
        {
            for (int y = node.Position.y; y < node.Position.y + node.Dimensions.y; y++)
            {
                for (int x = node.Position.x; x < node.Position.x + node.Dimensions.x; x++)
                {
                    int index = ((z * Texture.width + y) * Texture.width + x) * 3;
                    Texture.SetPixel(x, y, z, color);
                    count++;
                }
            }
        }
        Texture.Apply(false, true);
    });

    // Aplliquer la texture au matériel 
    GetComponent<Renderer>().material.SetTexture("_Volume", Texture);
}
}
using ...

public class Octree
{
    private OctreeNode rootNode;

    public Octree(Texture3D texture)
    {
        // Déterminer les dimensions du nœud racine
        Vector3Int dimensions = new Vector3Int(texture.width, texture.height, texture.depth);
        rootNode = new OctreeNode(Vector3Int.zero, dimensions);

        // Calculer la couleur moyenne pour chaque nœud
        CalculateAverageColors(rootNode, texture);
    }

    public Octree(Vector3Int position, Vector3Int dimensions)
    {
        throw new System.NotImplementedException();
    }

    private void CalculateAverageColors(OctreeNode node, Texture3D texture)
    {
        // S'il s'agit d'un nœud feuille, calculez sa couleur moyenne et retournez la
        if (node.IsLeaf())
        {
            node.AverageColor = CalculateAverageColor(node.Bounds, texture);
            return;
        }

        // Sinon, calculez la couleur moyenne pour chaque nœud enfant
        foreach (OctreeNode childNode in node.Children)
        {
            CalculateAverageColors(childNode, texture);
        }

        // Calculer la couleur moyenne de ce nœud comme la moyenne des couleurs de ses enfants
        node.AverageColor = new Color();
        foreach (OctreeNode childNode in node.Children)
        {
            node.AverageColor += childNode.AverageColor;
        }
        node.AverageColor /= node.Children.Count();
    }

    public Color CalculateAverageColor(Bounds bounds, Texture3D texture)
    {
        // Calculer la couleur moyenne de la texture 
        Color averageColor = new Color();
        int count = 0;
        for (int x = (int)bounds.min.x; x < bounds.max.x; x++)
        {
            for (int y = (int)bounds.min.y; y < bounds.max.y; y++)
            {
                for (int z = (int)bounds.min.z; z < bounds.max.z; z++)
                {
                    averageColor += texture.GetPixel(x, y, z);
                    count++;
                }
            }
        }
        averageColor /= count;

        return averageColor;
    }

    public List<OctreeNode> GetNodesIntersectingBounds(Bounds bounds)
    {
        // Create a list of all nodes in the tree that intersect the given bounds
        List<OctreeNode> nodes = new List<OctreeNode>();
        Traverse(rootNode, node =>
        {
            if (node.Bounds.Intersects(bounds))
            {
                nodes.Add(node);
            }
        });
        return nodes;
    }

    public void Traverse(OctreeNode node, System.Action<OctreeNode> action)
    {
        // Effectuez l'action donnée sur ce nœud, puis effectuez-la de manière récursive sur ses enfants
        action(node);
        foreach (OctreeNode childNode in node.Children)
        {
            Traverse(childNode, action);
        }
    }

    public void Update(Vector3Int min, Vector3Int max, System.Action<OctreeNode, Color> action)
    {
        // Mettre à jour les couleurs moyennes de tous les nœuds 
        Traverse(rootNode, node =>
        {
            if (node.Bounds.Intersects(new Bounds(min, max)))
            {
                action(node, CalculateAverageColor(node.Bounds, node.Texture));
            }
        });
    }
    
}
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class OctreeNode
{
    public Vector3Int position;
    public Vector3Int dimensions;
    private List<OctreeNode> children;
    private Color averageColor;
    private readonly List<OctreeNode> _children = null;

    public OctreeNode(Vector3Int position, Vector3Int dimensions)
    {
        this.position = position;
        this.dimensions = dimensions;
        this.children = new List<OctreeNode>();
        
        _children = Enumerable.Repeat<OctreeNode>(null, 8).ToList();
        
        // Créer 8 enfants 
        if (!IsLeaf())
        {
            Subdivide();
        }
    }

    public Color AverageColor { get; set; }
    public Bounds Bounds { get; set; }
    public IEnumerable<OctreeNode> Children { get { return children; } }
    public Texture3D Texture { get; set; }

    private void Subdivide()
    {
        int childSize = dimensions.x / 2;
        for (int x = 0; x <= 1; x++)
        {
            for (int y = 0; y <= 1; y++)
            {
                for (int z = 0; z <= 1; z++)
                {
                    Vector3Int childPosition = position + new Vector3Int(x * childSize, y * childSize, z * childSize);
                    OctreeNode childNode = new OctreeNode(childPosition, new Vector3Int(childSize, childSize, childSize));
                    children.Add(childNode);
                }
            }
        }
    }

    public void CalculateAverageColor(Texture3D texture)
    {
        if (IsLeaf())
        {
            // Calcul de la couleur moyenne
            Color sum = Color.black;
            int count = 0;

            for (int x = position.x; x < position.x + dimensions.x; x++)
            {
                for (int y = position.y; y < position.y + dimensions.y; y++)
                {
                    for (int z = position.z; z < position.z + dimensions.z; z++)
                    {
                        sum += texture.GetPixel(x, y, z);
                        count++;
                    }
                }
            }
            averageColor = sum / count;
        }
        else
        {
            // calcul de la couleur moyenne des enfants 
            Color sum = Color.black;
            int count = 0;

            foreach (OctreeNode child in children)
            {
                child.CalculateAverageColor(texture);
                sum += child.averageColor;
                count++;
            }
            averageColor = sum / count;
        }
    }

    public bool IsLeaf()
    {
        return dimensions.x == 1;
    }

    public Vector3Int GetPosition()
    {
        return position;
    }

    public Vector3Int GetDimensions()
    {
        return dimensions;
    }

    public List<OctreeNode> GetChildren()
    {
        return children;
    }

    public Color GetAverageColor()
    {
        return averageColor;
    }
}
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Lvs17
  • 1
  • What does "optimize rendering" mean? What is it you are trying to optimize? Using classes and iterators in this way will likely have a significant penalty for all the indirections. If your tree is non sparse it should be far simpler, and likely faster, to just keep multiple resolutions of your texture, similar to a [pyramid for 2D images](https://en.wikipedia.org/wiki/Pyramid_(image_processing)) – JonasH May 03 '23 at 07:44
  • my project is based on 3D visualization, we must be able to easily visualize very large data, that's why we need the optimization of visualization rendering. however I do not really understand the pyramid concept that you mentioned at the end of your remarks – Lvs17 May 03 '23 at 08:19
  • But how is the rendering done? raytracing? Just taking arbitrary slices of the data? For trees to make any sense you need to be able to reject entire branches, like checking if alpha is zero. But optimizing rendering of large images in large part boils down to using the hardware efficiently. It uses really different techniques than most regular logic. I.e. it would most likely use a bunch of pointers/spans. Not lists, objects, foreach, etc. – JonasH May 03 '23 at 08:29
  • Please clarify your specific problem or provide additional details to highlight exactly what you need. As it's currently written, it's hard to tell exactly what you're asking. – Community May 03 '23 at 09:56

1 Answers1

0

The compile error is about these lines

_colorOctree.CalculateAverageColor((node, color) => ...)

_colorOctree.Traverse((node, color) => ...)

This does not match the method signatures:

Color CalculateAverageColor(Bounds bounds, Texture3D texture)

void Traverse(OctreeNode node, System.Action action)

In both cases the first parameter is omitted. You should probably add a method overload that uses the root of the tree for the node/bounds.

But this is not the correct way to write a high performance octree. For any call to the traverse method you would need to create n iterators, do n * 2 indirect loads, n method calls, etc. When dealing with large data sets and high performance requirements these small penalties really starts to matter. Even worse, there is no "early out", so each call will process all branches of the tree, down to the leaf. So I do not really see the point of the tree.

A better approach if your tree is non sparse is to just use multiple volumes of different resolution. In 2D this is called a pyramid, I'm not sure there is a established term in 3D, 'Hyper-pyramid'?.

public class Octree
{
    private int[][,,] pyramid;

    public Octree()
    {
        pyramid = new int[3][,,];
        // zero layer is full resolution
        pyramid[0] = new int[8, 8, 8];
        pyramid[1] = new int[4, 4, 4];
        pyramid[2] = new int[2, 2, 2];

        // fill zero layer with data
        pyramid[0][2, 2, 2] = 42;
        pyramid[0][7, 3, 2] = 98;

        // compute the max value for other layers
        for (int layerIndex = 1; layerIndex < pyramid.Length; layerIndex++)
        {
            var currentLayer = pyramid[layerIndex];
            var largerLayer = pyramid[layerIndex - 1];
            var xsize = currentLayer.GetLength(0);
            var ysize = currentLayer.GetLength(1);
            var zsize = currentLayer.GetLength(2);
            for (int x = 0; x < xsize; x++)
            {
                for (int y = 0; y < ysize; y++)
                {
                    for (int z = 0; z < zsize; z++)
                    {
                        var max = largerLayer[x * 2, y * 2, z * 2];
                        max = System.Math.Max(max, largerLayer[x * 2, y * 2, z * 2 + 1]);
                        max = System.Math.Max(max, largerLayer[x * 2, y * 2 + 1, z * 2]);
                        max = System.Math.Max(max, largerLayer[x * 2, y * 2 + 1, z * 2 + 1]);
                        max = System.Math.Max(max, largerLayer[x * 2 + 1, y * 2, z * 2]);
                        max = System.Math.Max(max, largerLayer[x * 2 + 1, y * 2, z * 2 + 1]);
                        max = System.Math.Max(max, largerLayer[x * 2 + 1, y * 2 + 1, z * 2]);
                        max = System.Math.Max(max, largerLayer[x * 2 + 1, y * 2 + 1, z * 2 + 1]);
                        currentLayer[x, y, z] = max;
                    }
                }
            }
        }
    }

    public int SampleIfPositive(int x, int y, int z)
    {
        int value = 0;
        for (int i = pyramid.Length - 1; i >= 0; i--)
        {
            var xs = x >> i;
            var ys = y >> i;
            var zs = z >> i;
            value = pyramid[i][xs, ys, zs];
            if (value <= 0) return 0;
        }
        return value;
    }
}

Note the SampleIfPositive method. Since each layer is half the size of the previous we can use shifts to compute the indices of each layer, and this is super fast. And since the layers store the max values of all contained nodes we can quickly return if there is any zero values. There are still some indirect memory accesses, but since this is so much simpler the CPU will have a much better chance at predicting these.

Of course this example is highly simplified. The all the sizes are hard coded, and a power of two, there are no bounds checks etc. And in real life you probably want to store more than the max value, or update some visibility flag. But if you store more data it should be in a struct to avoid the overheads of objects. But I hope the example will provide some insight into how lower level code can be written to avoid some of the overheads of object orientation.

Even more important, tree approaches is mostly useful for things like raytracing where you can quickly skip large, empty, sections of the volume. If you just want to render a slice there is no reason to just access the highest resolution volume directly. If you want to avoid aliasing you can select a suitable level from the pyramid and sample that.

Non tree approaches, like using 16x16x16 blocks, but no finer or coarser, are also fairly common. This block approach can also help with data locality.

JonasH
  • 28,608
  • 2
  • 10
  • 23