Hardware instancing with frustnum culling/lighting

warning this code is a mess, for this demo code some parameters are adjusted.

for normal scenes it suppose generate tiles of instances in different areas thats what removal distance is used for, has the player would get further away it would remove old instances from the lists, and generate new instances around the player

a normal scene

max distance is set around 450 for a normal scene
removal distance is around 850

the culling function works better on normal scenes less flickering

currently the main problem is the culling function have yet to be able to get it much faster,
still works with around 20k instances, i havent been able to get quad trees or grids to work

# vertex shader:
v_shader = '''#version 330

struct p3d_DirectionalLightParameters {
    vec4 color;
    vec3 direction;
    sampler2DShadow shadowMap;
    mat4 shadowViewMatrix;
};

uniform p3d_DirectionalLightParameters my_directional_light;
uniform mat4 p3d_ModelViewProjectionMatrix;
uniform mat3 p3d_NormalMatrix;
uniform mat4 p3d_ModelViewMatrix;

in vec4 p3d_Vertex;
in vec3 p3d_Normal;
in vec2 p3d_MultiTexCoord0;
in vec4 offset;
in vec4 rotation; // Heading, Pitch, Roll
in vec4 scale;

out vec2 uv;
out vec4 shadow_uv;
out vec3 normal;
out vec4 fragPos;

mat4 rotationMatrixX(float angle) {
    float c = cos(angle);
    float s = sin(angle);
    return mat4(
        1.0, 0.0, 0.0, 0.0,
        0.0, c, -s, 0.0,
        0.0, s, c, 0.0,
        0.0, 0.0, 0.0, 1.0
    );
}

mat4 rotationMatrixY(float angle) {
    float c = cos(angle);
    float s = sin(angle);
    return mat4(
        c, 0.0, s, 0.0,
        0.0, 1.0, 0.0, 0.0,
        -s, 0.0, c, 0.0,
        0.0, 0.0, 0.0, 1.0
    );
}

mat4 rotationMatrixZ(float angle) {
    float c = cos(angle);
    float s = sin(angle);
    return mat4(
        c, -s, 0.0, 0.0,
        s, c, 0.0, 0.0,
        0.0, 0.0, 1.0, 0.0,
        0.0, 0.0, 0.0, 1.0
    );
}

void main() {
    vec4 vertexPosition = p3d_Vertex;
    vec3 transformedNormal = p3d_Normal;

    // Apply uniform scale
    vertexPosition *= scale;

    // Convert degrees to radians
    float angleH = radians(rotation.x); // Heading (yaw)
    float angleP = radians(rotation.y); // Pitch
    float angleR = radians(rotation.z); // Roll

    // Create rotation matrices from Euler angles
    mat4 rotationMatrix = rotationMatrixY(angleH) * rotationMatrixX(angleP) * rotationMatrixZ(angleR);

    // Apply rotation
    vertexPosition = rotationMatrix * vertexPosition;
    transformedNormal = normalize(mat3(rotationMatrix) * p3d_Normal);

    // Apply offset
    vertexPosition += offset;

    // Position
    gl_Position = p3d_ModelViewProjectionMatrix * vertexPosition;

    // Normal
    normal = p3d_NormalMatrix * transformedNormal;

    // UV
    uv = p3d_MultiTexCoord0;

    // Shadows
    shadow_uv = my_directional_light.shadowViewMatrix * (p3d_ModelViewMatrix * vertexPosition);

    // Frag position
    fragPos = p3d_ModelViewMatrix * vertexPosition;
}'''
# fragment shader
f_shader = '''#version 330

struct p3d_DirectionalLightParameters {
    vec4 color;
    vec3 direction;
    sampler2DShadow shadowMap;
    mat4 shadowViewMatrix;
};

struct p3d_PointLightParameters {
    vec4 color;
    vec3 position;
    samplerCube shadowMap;
    vec3 attenuation;
};

struct p3d_SpotLightParameters {
    vec4 color;
    vec3 position;
    vec3 spotDirection;
    sampler2DShadow shadowMap;
    mat4 shadowViewMatrix;
    vec3 attenuation;
};

const int MAX_POINT_LIGHTS = 4;
const int MAX_SPOT_LIGHTS = 4;

uniform p3d_DirectionalLightParameters my_directional_light;
uniform p3d_PointLightParameters point_lights[MAX_POINT_LIGHTS];
uniform p3d_SpotLightParameters spot_lights[MAX_SPOT_LIGHTS];

uniform sampler2D p3d_Texture0;
uniform vec3 camera_pos;
uniform float shadow_blur;
uniform vec4 ambientLightColor;
uniform vec4 fogColor;
uniform float fogStart;
uniform float fogEnd;
uniform vec3 player_pos;
uniform bool enable_transparency;
uniform vec4 horizonColorb;

uniform int num_point_lights;
uniform int num_spot_lights;

in vec2 uv;
in vec4 shadow_uv;
in vec3 normal;
in vec4 fragPos;

out vec4 color;

float textureProjSoft(sampler2DShadow tex, vec4 uv, float bias, float blur) {
    float result = textureProj(tex, uv, bias);
    result += textureProj(tex, vec4(uv.xy + vec2(-0.326212, -0.405805) * blur, uv.z - bias, uv.w));
    result += textureProj(tex, vec4(uv.xy + vec2(-0.840144, -0.073580) * blur, uv.z - bias, uv.w));
    result += textureProj(tex, vec4(uv.xy + vec2(-0.695914, 0.457137) * blur, uv.z - bias, uv.w));
    result += textureProj(tex, vec4(uv.xy + vec2(-0.203345, 0.620716) * blur, uv.z - bias, uv.w));
    return result / 5.0; // Reduced number of samples
}

float calculatePointLightShadow(vec3 fragPos, vec3 lightPos, samplerCube shadowMap) {
    vec3 lightToFrag = fragPos - lightPos;
    float currentDepth = length(lightToFrag);
    float shadow = texture(shadowMap, lightToFrag).r;
    float bias = 0.05; // Adjust bias as needed
    return currentDepth - bias > shadow ? 0.5 : 1.0;
}

void main() {
    // Base color
    vec3 ambient = ambientLightColor.rgb;
    vec4 tex = texture(p3d_Texture0, uv);

    // Calculate directional light contribution
    vec3 dirLight = my_directional_light.color.rgb * max(dot(normalize(normal), my_directional_light.direction), 0.0);
    float dirLightShadow = textureProjSoft(my_directional_light.shadowMap, shadow_uv, 0.0001, shadow_blur);
    dirLightShadow = 0.5 + dirLightShadow * 0.5;
    dirLight *= dirLightShadow;

    // Calculate point light contributions with attenuation
    vec3 totalPointLight = vec3(0.0);
    for (int i = 0; i < num_point_lights; i++) {
        vec3 lightDir = point_lights[i].position - fragPos.xyz;
        float distance = length(lightDir);
        vec3 attenuationFactors = point_lights[i].attenuation; // Fetch attenuation from struct
        float attenuation = 1.0 / (attenuationFactors.x + attenuationFactors.y * distance + attenuationFactors.z * (distance * distance));
        vec3 pointLight = point_lights[i].color.rgb * max(dot(normalize(normal), normalize(lightDir)), 0.0);
        pointLight *= attenuation;
        pointLight *= calculatePointLightShadow(fragPos.xyz, point_lights[i].position, point_lights[i].shadowMap);
        totalPointLight += pointLight;
    }

    // Calculate spotlight contributions
    vec3 totalSpotLight = vec3(0.0);
    for (int i = 0; i < num_spot_lights; i++) {
        vec3 spotDirection = normalize(spot_lights[i].spotDirection);
        vec3 lightDir = spot_lights[i].position - fragPos.xyz;
        float distance = length(lightDir);
        vec3 attenuationFactors = spot_lights[i].attenuation;
        float attenuation = 1.0 / (attenuationFactors.x + attenuationFactors.y * distance + attenuationFactors.z * (distance * distance)); // Use attenuation from struct
        vec3 spotLight = spot_lights[i].color.rgb * max(dot(normalize(normal), -spotDirection), 0.0);
        float theta = dot(normalize(fragPos.xyz - spot_lights[i].position), spotDirection);
        float intensity = max(pow(theta, 10.0), 0.0); // Adjust the exponent to control the spotlight focus
        spotLight *= intensity * attenuation;
        totalSpotLight += spotLight;
    }

    // Combine all lighting
    vec3 finalLight = dirLight + ambient + totalPointLight + totalSpotLight;

    // Precompute fog factor
    float heightFogFactor = clamp((fogEnd - length(fragPos.xyz.y)) / (fogEnd - fogStart), 0.0, 1.0);
    float depthFogFactor = clamp((fogEnd - length(fragPos.xyz)) / (fogEnd - fogStart), 0.0, 1.0);
    float fogFactor = min(heightFogFactor, depthFogFactor);

    // Blend fog color with skybox color at the horizon
    vec4 horizonColor = mix(fogColor, horizonColorb, 0.5);
    vec4 foggedColor = mix(horizonColor, vec4(tex.rgb * finalLight, tex.a), fogFactor);

    // Calculate distance from player position
    float distance = length(fragPos.xyz - player_pos);

    // Define a threshold for alpha
    float alphaThreshold = 0.5;

    // Adjust alpha based on distance with a hard cut-off
    float alpha = enable_transparency ? (distance < 24.0 ? 0.0 : (tex.a > alphaThreshold ? 1.0 : 0.0)) : (tex.a > alphaThreshold ? 1.0 : 0.0);

    // Apply the alpha to the fogged color
    color = vec4(foggedColor.rgb, alpha);
}'''

import random

from direct.showbase.ShowBase import ShowBase

from panda3d.core import *

import time

import numpy as np

from direct.actor.Actor import Actor

from direct.task import Task

# Set the dimensions of the height map
height, width = 4486, 4486

# Generate the noise height map
noise_height_map = np.random.rand(height, width)



class MyApp(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)
        base.trackball.node().set_pos(4.7, 112.7, -9.7)
        base.trackball.node().set_hpr(61.5281, 12.0915, -18.2124)

        self.node = Actor('panda-model', {'walk' : 'panda-walk4'})
        self.node.loop('walk')
        self.node.setScale(0.01)
        self.node.reparentTo(render)
        # self.node.setPos(0,0,0)

        #lighting
        self.sun = DirectionalLight("Spot")
        # print(dir(self.sun),'attributes')
        self.sun_path = self.render.attachNewNode(self.sun)
        self.sun_path.node().set_shadow_caster(True, 4096, 4096)
        self.sun_path.node().set_color((0.9, 0.9, 0.8, 1.0))
        # self.sun_path.node().showFrustum()
        self.sun_path.node().get_lens().set_fov(40)
        # self.sun_path.node().attenuation = (1, 0.001, 0.0001)
        self.sun_path.node().get_lens().set_near_far(-400, 400)
        self.sun_path.node().get_lens().set_film_size(400)

        self.pivot = self.render.attachNewNode("pivot")
        self.pivot.setPos(0, 0, 0)  # Set the position of the pivot point

        self.sun_path.setHpr(0, 0, 0)
        self.sun_path.reparentTo(self.pivot)
        self.render.setLight(self.sun_path)
        #sun shader settings
        self.render.set_shader_input('my_directional_light',self.sun_path)
        self.render.set_shader_input("my_directional_light.direction", self.sun.getDirection())

        self.instanceshader = Shader.make(Shader.SL_GLSL,v_shader, f_shader)
        self.node.setShader(self.instanceshader)

        # self.render.set_shader_input('my_point_light',self.point_path)
        self.spotlight1 = Spotlight("spotlight1")
        self.spotlight1.setColor((10, 10, 10, 1))  # Brighter light (RGB values higher than 1)
        self.spotlight1.setAttenuation((1.0, 0.09, 0.032))
        
        self.spotlight1.setMaxDistance(10)
        self.spotlight1_path = self.render.attachNewNode(self.spotlight1)
        # self.spotlight1_path.setPos(-16, -31, 28) # Example position
        self.render.setLight(self.spotlight1_path)
        # self.spotlight1.showFrustum()


        self.spotlight2 = Spotlight("spotlight2")
        self.spotlight2.setAttenuation((1.0, 0.09, 0.032))
        self.spotlight2.setMaxDistance(10)
        self.spotlight2_path = self.render.attachNewNode(self.spotlight2)
        self.spotlight2_path.setPos(-22, -22, 26) # Example position
        self.render.setLight(self.spotlight2_path)
        # self.spotlight2.showFrustum()

        self.render.set_shader_input('num_spot_lights', 2)
        self.render.set_shader_input('my_spot_light', self.spotlight1_path)
        self.render.set_shader_input('spot_lights[0]', self.spotlight1_path)
        self.render.set_shader_input('spot_lights[1]', self.spotlight2_path)
        # self.spotlight1_path.reparentTo(self.point_path)
        
        self.render.set_shader_input(f'spot_lights[1].color', LVecBase4(0, 0, 0, 0))#off
        self.render.set_shader_input(f'spot_lights[0].color', LVecBase4(0, 0, 0, 0))#off
        for i in range(2,4):
            self.render.set_shader_input(f'spot_lights[{i}]',self.spotlight1_path)
            self.render.set_shader_input(f'spot_lights[{i}].color', LVecBase4(0, 0, 0, 0))#off


        # Initialize point lights
        self.point1 = PointLight("Point1")
        self.point1.setAttenuation((1.0, 0.09, 0.032))
        self.point1.setMaxDistance(50)
        self.point1_path = self.render.attachNewNode(self.point1)
        self.point1_path.setPos(-16, -31, 29) # Example position
        self.render.setLight(self.point1_path)

        self.point2 = PointLight("Point2")
        self.point2.setAttenuation((0.1, 0.43, 0.044))
        self.point2.setMaxDistance(2)
        self.point2_path = self.render.attachNewNode(self.point2)
        self.point2_path.setPos(-22, -22, 29) # Example position
        self.render.setLight(self.point2_path)


        #point light
        self.point = PointLight("Point")
        self.point.setAttenuation((1.0, 0.09, 0.032))  # Adjust these values to control the attenuation
        self.point.setMaxDistance(50)  # Set the maximum distance of the light's influence
        # self.point.showFrustum()
        self.point_path = self.render.attachNewNode(self.point)

        self.render.set_shader_input('num_point_lights', 2)
        # Set shader inputs for existing lights
        self.render.set_shader_input('point_lights[0]',self.point_path)
        self.render.set_shader_input('point_lights[1]',self.point2_path)
        self.render.set_shader_input('point_lights[1].color', LVecBase4(0, 0, 0, 0))#off
        self.render.set_shader_input('point_lights[0].color', LVecBase4(0, 0, 0, 0))#off
        for i in range(2,4):
            self.render.set_shader_input(f'point_lights[{i}]',self.point1_path)
            self.render.set_shader_input(f'point_lights[{i}].color', LVecBase4(0, 0, 0, 0))#off
        
        self.render.set_shader_input('shadow_blur',0.0005)
        self.render.set_shader_input('player_pos',(0,0,0))
        self.render.set_shader_input('enable_transparency',False)

        self.render.setShaderInput("fogColor", (0.5, 0.5, 0.5, 1.0)) # Set the fog color
        self.render.setShaderInput("fogStart", 300.0) # Set the fog start distance
        self.render.setShaderInput("fogEnd", 410.0) # Set the fog end distance

        self.render.setShaderInput("ambientLightColor", (0.1, 0.1, 0.1, 1.0))
        self.horizon_colorday = Vec4(0.529, 0.808, 0.980, 1)
        self.render.setShaderInput("horizonColorb", self.horizon_colorday)

        camera = base.cam
        lens = camera.node().getLens()
        lens.setNear(1)
        lens.setFar(700.0)

        positionlist=self.position_gen(2000,12,4486,4486,noise_height_map)

        self.offsetpositionlistbush=[]
        self.scalelistbush=[]
        self.rotationlistbush=[]

        #instancing
        self.removed_instances = []
        self.total_instances = 0
        self.instance_parent = None
        self.poswriter1=None
        self.rotwriter1=None
        self.scalewriter1=None

        (self.total_instances, 
        self.poswriter1, 
        self.rotwriter1, 
        self.scalewriter1,
        self.offsetpositionlistbush, 
        self.scalelistbush, 
        self.rotationlistbush) = self.setup_instancing(
            noise_height_map,
            self.node, 
            positionlist, 
            fromvalue=0, 
            tovalue=2000, 
            new_location=(0, 0, 0), 
            zlevel=None, 
            total_instances=self.total_instances, 
            poswriter=self.poswriter1,
            rotwriter=self.rotwriter1,
            scalewriter=self.scalewriter1,
            zrotonly=True
        )
        self.task = self.taskMgr.add(self.updateTask, "update")

    def updateTask(self, task):

        # if self.clearing == False: 
        # for i, item in enumerate(self.loadedfiles):
            # index = i * 6  # Calculate the base index for each file.
            # print(self.all_lists[index])
        poswriter_list = self.poswriter1
        rotwriter_list = self.rotwriter1
        scalewriter_list = self.scalewriter1

        position_list = self.offsetpositionlistbush
        scale_list = self.scalelistbush
        rotation_list = self.rotationlistbush
        #radius of 20
        visible_instances = self.cull_and_update_instances(self.node, base.cam, 20, poswriter_list, scalewriter_list, rotwriter_list, position_list, rotation_list, scale_list, max_distance=12050,removal_distance=109050)
        print(visible_instances)
        if visible_instances < 1:
            self.node.hide()
        else:
            self.node.show()
            self.node.setInstanceCount(visible_instances)
        return Task.cont

    def cull_and_update_instances(self, node, camera, radius, poswriter, scalewriter, rotwriter, positionlist, rotationlist, scalelist, max_distance=450, removal_distance=850):
        lens = camera.node().getLens()
        lensBounds = lens.makeBounds()
        visible_instances = 0
        removal_indices = []

        poswriter.setRow(0)  
        rotwriter.setRow(0)  
        scalewriter.setRow(0)  

        camera_pos = camera.getPos(node)  
        bounds = BoundingSphere()
        node_mat = node.getMat(camera)  # Cache the node's transformation

        for i, (pos, rotation, scale) in enumerate(zip(positionlist, rotationlist, scalelist)):
            instance_position = LPoint3f(*pos)
            distance_from_camera = (instance_position - camera_pos).length()

            if distance_from_camera > removal_distance:
                # Flag the index for removal
                removal_indices.append(i)
                continue

            newcenter = instance_position
            rotationin = LVecBase4f(rotation[0], rotation[1], rotation[2], 1)
            scalein = LVecBase4f(scale[0], scale[1], scale[2], 1)
                
            bounds.setCenter(newcenter)
            bounds.setRadius(radius)
            bounds.xform(node_mat)  # Apply cached transformation

            if lensBounds.contains(bounds) and distance_from_camera <= max_distance:
                poswriter.add_data3(newcenter)
                rotwriter.add_data4(rotationin)
                scalewriter.add_data4(scalein)
                visible_instances += 1

        # Remove flagged instances
        for idx in sorted(removal_indices, reverse=True):
            del positionlist[idx]
            del rotationlist[idx]
            del scalelist[idx]

        return visible_instances

    def position_gen(self, value=1000, seed=29, area_width=486, area_height=486, height_map=None, min_distance=10):
        random.seed(seed)
        grid_size = min_distance
        grid_width = area_width // grid_size
        grid_height = area_height // grid_size
        occupied_positions = []

        for i in range(value):
            while True:
                grid_x = random.randint(0, grid_width - 1)
                grid_y = random.randint(0, grid_height - 1)
                x = grid_x * grid_size + random.uniform(0, grid_size)
                y = grid_y * grid_size + random.uniform(0, grid_size)
                z = height_map[int(x)][int(y)]
                pos = (int(x), int(y), z)
                if pos not in occupied_positions:
                    occupied_positions.append(pos)
                    break

        return occupied_positions

    def calculate_normal(self, x, y, height_map):
        # Get the height of the neighbors or the vertex itself if it's an edge
        height_x_plus_1 = height_map[x + 1][y] if x < len(height_map) - 1 else height_map[x][y]
        height_x_minus_1 = height_map[x - 1][y] if x > 0 else height_map[x][y]
        height_y_plus_1 = height_map[x][y + 1] if y < len(height_map[0]) - 1 else height_map[x][y]
        height_y_minus_1 = height_map[x][y - 1] if y > 0 else height_map[x][y]

        dx = (height_x_plus_1 - height_x_minus_1) / 2.0
        dy = (height_y_plus_1 - height_y_minus_1) / 2.0
        normal = Vec3(-dx, -dy, 1).normalized()
        return normal

    def normal_to_euler(self,normal):
        # Normalize the normal vector
        normal = normal / np.linalg.norm(normal)
        
        # Calculate the Euler angles (Heading, Pitch, Roll)
        pitch = np.arcsin(normal[1])
        heading = -np.arctan2(normal[0], normal[2])  # Invert the heading
        roll = 0
        
        return heading, pitch, roll

    def setup_instancing(self, heightmapsgotfunc, nodefunc, positionlist=[], seed=29, fromvalue=0, tovalue=250, new_location=(0, 0, 0), zlevel=True, total_instances=0, poswriter=None, rotwriter=None, scalewriter=None, zrotonly=False):
        random.seed(seed)
        gnode = nodefunc.find("**/+GeomNode").node()
        offsetpositionlist=[]
        scalelist=[]
        rotationlist=[]
        vdata = gnode.modifyGeom(0).modifyVertexData()
        format = GeomVertexFormat(gnode.getGeom(0).getVertexData().getFormat())
        if not format.hasColumn("offset"):
            iformat = GeomVertexArrayFormat()
            iformat.setDivisor(1)
            iformat.addColumn("offset", 4, Geom.NT_stdfloat, Geom.C_other)
            iformat.addColumn("rotation", 4, Geom.NT_stdfloat, Geom.C_other)  # Add rotation column
            iformat.addColumn("scale", 4, Geom.NT_stdfloat, Geom.C_other)  # Add scale column
            format.addArray(iformat)
            format = GeomVertexFormat.registerFormat(format)
            vdata.setFormat(format)

        sorted_positions = positionlist if zlevel is None else sorted(positionlist, key=lambda pos: pos[2], reverse=zlevel)
        
        if poswriter is None:
            poswriter = GeomVertexWriter(vdata.modifyArray(2), 0)
        if rotwriter is None:
            rotwriter = GeomVertexWriter(vdata.modifyArray(2), 1)  # Writer for rotation
        if scalewriter is None:
            scalewriter = GeomVertexWriter(vdata.modifyArray(2), 2)  # Writer for scale

        self.instance_parent = NodePath('instances')
        for i in range(fromvalue, tovalue):
            placeholder = self.instance_parent.attachNewNode(f'Instance_{i}')
            x, y, z = sorted_positions[i]
            placeholder.setPos(x, y, z)
            poswriter.add_data3(x + new_location[0], y + new_location[1], z + new_location[2])
            offsetpositionlist.append([x + new_location[0], y + new_location[1], z + new_location[2]])
            normal = self.calculate_normal(x, y, heightmapsgotfunc)

            # Step 2: Convert normal to Euler angles
            heading, pitch, roll = self.normal_to_euler(normal)
            
            placeholder.lookAt(placeholder.getPos() + normal)
            h, p, r = placeholder.getHpr()
            if not zrotonly:
                if p >= 45:
                    random_heading = random.uniform(0, 360)
                    # placeholder.setHpr(h + random_heading, 0, 0)
                    heading, pitch, roll = 0,0,roll+random_heading
                    placeholder.setHpr(0, 0, r+heading)
                    # heading, pitch, roll = heading/10,pitch/10,roll+random_heading
                else:
                    random_heading = random.uniform(0, 50)
                    # placeholder.setHpr(h + 90 + random_heading, p / 3, r)
                    heading, pitch, roll = heading/2,pitch/2,roll+ 90 +random_heading
                    placeholder.setHpr(h+roll, p+pitch, r+heading)
            else:
                random_heading = random.uniform(0, 360)
                heading, pitch, roll = heading,pitch,roll+ 90 +random_heading
                placeholder.setHpr(h+roll, p+pitch, r+heading)
            # h, p, r = placeholder.getHpr()
            placeholder.setHpr(h, p-90, r)
            # instance = self.tree.instanceTo(placeholder)
            # instance.setTag('instance', 'true')
            rotwriter.add_data4(heading*45, pitch*45, roll*360, 1)
            # rotwriter.add_data4(h, p, r, 1)
            scale = random.uniform(0.9, 1.5)
            placeholder.setScale(scale)
            scalewriter.add_data4(scale, scale, scale, 1)
            rotationlist.append([heading*45, pitch*45, roll*360, 1])
            scalelist.append([scale, scale, scale, 1])

        # Move the whole area to the new location
        self.instance_parent.setPos(new_location)

        self.instance_parent.reparentTo(self.render)
        total_instances += (tovalue - fromvalue)
        nodefunc.setInstanceCount(total_instances)
        nodefunc.node().setBounds(OmniBoundingVolume())
        nodefunc.node().setFinal(True)

        return total_instances, poswriter, rotwriter, scalewriter,offsetpositionlist,scalelist,rotationlist
app = MyApp()
app.run()
1 Like