SceneKit - Géométrie custom et mapping de texture

bonjour !

Je galère depuis plusieurs jours autour de mon problème alors je m'en remets à vous pour avancer.

Je souhaite plaquer une texture sur un objet chargé dans SceneKit depuis un fichier dae mais qui ne contient pas de coordonnées UV. D'après ce que j'ai lu, si la géométrie ne contient pas de texcoords (les uvs), on peut changer la couleur de l'objet, mais on ne peut pas y appliquer de texture -> problème.

Mon approche est la suivante : je pars du principe que mon fichier dae ne fournit qu'une partie de la géométrie dont j'ai besoin (la position des vertex et les normales) et qu'il faut que je reconstruise une nouvelle géométrie utilisant ces vertex, ces normales, et un nouveau tableau d'UV calculé par mes soins.

J'utilise comme approximation pour le plaquage de la texture, la méthode sphérique (on imagine que l'objet est inscrit dans une sphère de rayon 1 et on calcule les UVs en fonction cf UV_mapping). Il faut d'abord récupérer les vertices de la géométrie (j'ai trouvé ça ici: Get Vertices). Évidement, le mapping devrait être parfait si l'objet est une sphère, et sera de moins en moins bon si l'objet s'éloigne de la forme d'une sphère, mais ça me convient.

J'ai essayé de faire un playground mais Playground+SceneKit+dae...

Voici les fonctions principales.

`

public static func computeUV(vertices: [SCNVector3]) -> [CGPoint]{

    var uvs: [CGPoint] = Array()

    vertices.forEach { (vertex) in

        let n = SCNVector3(vertex.x, vertex.y, vertex.z).normalized()

        let u = 0.5 + atan2(n.z, n.x) / ( 2.0 * .pi)
        let v = 0.5 - asin(n.y) / .pi

        let uv_vect = CGPoint(x: CGFloat(u), y: CGFloat(v))

        uvs.append(uv_vect)
        //uvs.insert(uv_vect, at: 0)
    }

    return uvs
}

 //extension de geometry (la même existe pour les normales)
func vertices(semantic: SCNGeometrySource.Semantic) -> [SCNVector3] {
    let vertexSources = sources(for: SCNGeometrySource.Semantic.vertex)
    if let vertexSource = vertexSources.first {
        let count = vertexSource.data.count / MemoryLayout<SCNVector3>.size

        //print("vertices.count: \(scnVector3Array.count)")
        return vertexSource.data.withUnsafeBytes {
            [SCNVector3](UnsafeBufferPointer<SCNVector3>(start: $0, count: count))
        }
    }
    return []
}

`

Et j'utilise tout ça ainsi :

`

        let vertices = sphereNode.geometry!.vertices()
        let normals = sphereNode.geometry!.normals()
        let uvs = computeUV(vertices: vertices)

   var sources: [SCNGeometrySource] = Array()

    // add vertices first
    let vertSource = SCNGeometrySource(vertices: vertices)
    sources.append(vertSource)

    // add normals
    let normalSource = SCNGeometrySource(normals: normals)
    sources.append(normalSource)

    // create the UV sources with the brand new uv coordinates computed earlier
    if uv.count > 0{
        let uvSource = SCNGeometrySource(textureCoordinates: uv)
        sources.append(uvSource)
    }

    // Create the new geometry using sources and elements of the geometry
    let newGeometryWithUV = SCNGeometry(sources: sources, elements: elements)

`

Je teste avec une simple sphère (faite dans blender, exportée en dae et chargée dans SceneKit). Je crée un materiel dans SceneKit à partir d'une image bien colorée :

Le matériel est ok :

Le mapping en revanche.... :

J'obtiens plusieurs couleurs par triangle, c'est un peu n'importe quoi :smile:

Vous avez une idée pour me débloquer ? Merci d'avance !

Mots clés:

Réponses

  • odmodm Membre

    Salut !

    Personne n'a d'idée ? Rolf, de mon côté j'ai très légèrement avancé, mais ce n'est vraiment pas terrible quand même.

    En réalité j'ai deux options :

    1. Je crée la nouvelle géométrie en utilisant les SCNGeometrySource de la géométrie du fichier dae (excepté pour les UVs qu'il faut que je recrée forcément)
    2. Je crée la nouvelle géométrie en créant des nouvelles SCNGeometrySource à partir des tableaux (vertices et normals) extraits par mes soins pour calculer les UVs.

    `
    let decodedVertices = node.geometry!.vertices()
    let decodedNormal = node.geometry!.normals()
    let decodedUvs = computeUVs(vertices: decodedVertices)

      let decodedVerticesSource = SCNGeometrySource(vertices: decodedVertices)
      let decodedUVsSource = SCNGeometrySource(textureCoordinates: decodedUvs)
      let decodedNormalSource = SCNGeometrySource(normals: decodedNormal)
    
      let decodedSources = [decodedVerticesSource, decodedNormalSource, decodedUVsSource] //option 1
      let daeSources = [daeVerticesSource, daeNormalSource, decodedUVsSource] // option 2
    
      let ele = SCNGeometryElement(indices: elements, primitiveType: .triangleStrip)
      //Solution 1
      node.geometry = SCNGeometry(sources: decodedSources, elements: [ele])
      //Solution 2
      //node.geometry = SCNGeometry(sources: daeSources, elements: [ele])
    

    `

    La solution 1 donne le résultat de l'image précédente (mapping complètement délirant) et la solution 2 casse complètement la géométrie (la géométrie est moins cassée si j'utilise .triangleStrip pour les éléments), mais le résultat n'est quand même pas bon :

    Toujours pas d'idée ? :'(

  • DrakenDraken Membre

    Euh .. le spécialiste de ce genre de chose c'était Ceroce, mais on le voit plus sur le forum depuis quelques mois. Je comprend vaguement ce que tu essayes de faire, mais pas assez pour t'aider.

  • odmodm Membre
    2 juil. modifié #4

    Merci pour ta réponse, après une investigation plus poussée, je suis remonté à ce que je pense être une source du problème : c'est le décodage des données binaires du .dae chargé par SceneKit (sous forme de NSData). Je m'explique.

    Lorsque j'explore le fichier dae (dans un lecteur de fichier texte) je vois les valeurs suivantes pour le simple cube initial de blender (la sphère c'est plus compliqué alors je me rabats sur un cube...), c'est 24 valeurs, donc 8 vecteurs de 3 (x, y, z) composantes :

    <source id="Cube-mesh-positions"> <float_array id="Cube-mesh-positions-array" count="24">1 1 -1 1 -1 -1 -1 -0.9999998 -1 -0.9999997 1 -1 1 0.9999995 1 0.9999994 -1.000001 1 -1 -0.9999997 1 -1 1 1</float_array> <technique_common> <accessor source="#Cube-mesh-positions-array" count="8" stride="3"> <param name="X" type="float"/> <param name="Y" type="float"/> <param name="Z" type="float"/> </accessor> </technique_common> </source>

    En revanche, dans le SceneKit Editor, il en voit un peu plus (36) - je me dis que c'est cohérent car il y a 6 faces, et 2 triangles par face et donc 3 vecteurs par triangles (3x2x6 = 36).

    Mais lorsque je décode les données, que je fixe la taille du buffer à 36 ou à 24, il me met toujours du "caca" dans les données, et elle ne sont pas ordonnées, comme par exemple :

    [1.0, -1.0, -1.0, 0.0, -1.0, -0.0, -1.0, -1.0, 0.9999998, 0.0, -1.0, -0.0, -0.9999997, -1.0, -1.0, 0.0, -1.0, -0.0, -1.0, 1.0, -1.0, 0.0, 1.0, -0.0, 0.9999994, 1.0, 1.000001, 0.0, 1.0, -0.0, 1.0, 1.0, -0.9999995, 0.0, 1.0, -0.0, 1.0, 1.0, -0.9999995, 1.0, -2.38419e-07, -0.0, 1.0, -1.0, 1.0, 1.0, -2.38419e-07, -0.0, 1.0, -1.0, -1.0, 1.0, -2.38419e-07, -0.0, 0.9999994, 1.0, 1.000001, 0.0, -4.76837e-07, 1.0, -1.0, -1.0, 0.9999998, 0.0, -4.76837e-07, 1.0, 1.0, -1.0, 1.0, 0.0, -4.76837e-07, 1.0]

    C'est clairement un problème, mais je n'arrive pas à décoder convenablement ces données. J'ai pourtant essayé de nombreuses méthodes différentes et la plus "propre" semble celle-ci mais n'est quand même pas satisfaisante :

    `
    extension Data {

    func toArray<T>(type: T.Type) -> [T] {
        return self.withUnsafeBytes {
            [T](UnsafeBufferPointer(start: $0, count: self.count/(MemoryLayout<T>.stride) ))
        }
    }
    

    }
    `

  • odmodm Membre

    Hello,

    Bon, le problème provient clairement de la façon dont les données binaires stockées dans le NSData sont décodées. Les données semblent être organisées comme ceci en mémoire : [[SCNVector3], [caca], [SCNVector3], [caca], ...]. Si on utilise la fonction withUnsafeBytes, le fait que les données ne soient pas contigües pose problème (en effet, la donnée est codée sur 12 bytes mais dans un stride de 24).

    Si je les décode "à la main" (inspiré ici : https://stackoverflow.com/questions/17250501/extracting-vertices-from-scenekit ), j'obtiens le bon résultat, mais j'ai un problème de mémoire au niveau du copyBytes et je ne l'explique pas. Après 2500 vertices, ça crash, malgré mes tentative de release du buffer. Des idées ?

    `
    func vertices(source: SCNGeometrySource) -> [SCNVector3] {

        let stride = source.dataStride // in bytes
        let offset = source.dataOffset // in bytes
    
        let componentsPerVector = source.componentsPerVector
        let bytesPerVector = componentsPerVector * source.bytesPerComponent
        let vectorCount = source.vectorCount
    
        var result = Array<SCNVector3>() // A new array for vertices
    
        // for each vector, read the bytes
        for i in 0...vectorCount - 1 {
    
            // The range of bytes for this vector
            // Start at current stride + offset and read the lenght of one vector
            let byteRange = NSMakeRange(i * stride + offset, bytesPerVector)
    
            let newRange = Range.init(byteRange)
    
            let buffer = UnsafeMutableBufferPointer<Float>.allocate(capacity: componentsPerVector)
    
            let bytesCopied = source.data.copyBytes(to: buffer, from: newRange)
    
            // At this point you can read the data from the float array
            let x = buffer[0]
            let y = buffer[1]
            let z = buffer[2]
    
            //save it as an SCNVector3
            result.append(SCNVector3(x, y, z))
    
            //buffer.baseAddress!.deinitialize(count: componentsPerVector)
            //buffer.baseAddress!.deallocate()
    
        }
    
     return result
     }
    

    `

  • CéroceCéroce Membre, Modérateur
    9 juil. modifié #6

    @odm a dit :
    Mon approche est la suivante : je pars du principe que mon fichier dae ne fournit qu'une partie de la géométrie dont j'ai besoin (la position des vertex et les normales) et qu'il faut que je reconstruise une nouvelle géométrie utilisant ces vertex, ces normales, et un nouveau tableau d'UV calculé par mes soins.

    D'une certaine manière, ton approche est bonne. S'il manque les texcoords, il faut effectivement lire l'existant pour avoir deux SCNGeometrySources (coords et normales), générer une SCNGeometrySource pour les texcoords, puis recréer une SCNGeometry avec les trois sources.

    Sauf que… comme tu le vois, le mapping des uv est un problème en soi!
    Déjà, calculer les coordonnées des sommets n'est pas simple parce qu'on va construire la sphère à l'aide d'une "triangle strip", puisque c'est le plus performant.

    http://www.learnopengles.com/tag/triangle-strips/

    L'inconvénient des triangles strips, c'est justement que les indices des coordonnées sont croisés. C'est pour cela que tu as des résultats auxquels tu ne t'attends pas.

    Je n'ai peut-être pas compris le besoin, mais il me semble qu'il vaudrait mieux appliquer une texture dans ton outil 3D. Ainsi, les UV sont définies par l'outil — en mode sphérique, cylindrique ou autre mode proposé — et tu n'as plus qu'à remplacer la texture!

  • CéroceCéroce Membre, Modérateur

    @odm a dit :

    Bon, le problème provient clairement de la façon dont les données binaires stockées dans le NSData sont décodées.

    Sans doute tu n'as pas compris comment étaient vraiment stockées les coordonnées.
    Par exemple un carré défini par deux triangles n'a que 4 sommets: en bas à gauche (0), en haut à gauche (1), en bas à droite (2), en haut à droite (3). On ne définit qu'une fois pour chaque sommet ses coordonnées, sa normale, ses uv.
    Ensuite, il faut fournir une liste d'indices qui dit: pour construire le carré fais un premier triangle avec 0, 1, 2, puis un second avec 1, 2, 3. Ou avec une triangle strip, on dit même: 0, 1, 2, 3.

    (je me suis peut-être trompé dans les ordres des indices, mais c'est l'idée).

Connectez-vous ou Inscrivez-vous pour répondre.