メインコンテンツへスキップ
見出し画像

「星の形」を考える。※GIF付き

    おはこんばんにちは

    Rikurikuです~。


    皆さん、子供が描く星を想像してください。

    画像

    まあ、大抵はこんな感じですよね(偏見)。

    実際これが星のイメージなんだから、
    こういう形をしているんでしょう(偏見)。


    では、ちょっと考えてみましょう。

    AIを駆使してとあるコードを作りました。

    <!DOCTYPE html>
    <html lang="ja">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Dodecahedron Viewer</title>
        <style>
            body {
                margin: 0;
                overflow: hidden;
                background-color: #ffffff;
            }
    
            canvas {
                display: block;
            }
    
            .slider-container {
                position: absolute;
                bottom: 20px;
                left: 50%;
                transform: translateX(-50%);
                text-align: center;
                font-family: sans-serif;
                background: rgba(255, 255, 255, 0.8);
                padding: 10px;
                border-radius: 8px;
            }
        </style>
        <script type="importmap">
            {
                "imports": {
                    "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
                    "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
                }
            }
        </script>
    </head>
    
    <body>
        <div class="slider-container">
            <label for="explodeSlider">Face Distance</label><br>
            <input type="range" id="explodeSlider" min="-2" max="2" step="0.01" value="0" style="width: 200px;">
        </div>
        <script type="module">
            import * as THREE from 'three';
            import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
    
            // Scene Setup
            const scene = new THREE.Scene();
            scene.background = new THREE.Color(0xffffff); // White background
    
            // Camera Setup
            const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
            camera.position.z = 3;
    
            // Renderer Setup
            const renderer = new THREE.WebGLRenderer({ antialias: true });
            renderer.setSize(window.innerWidth, window.innerHeight);
            document.body.appendChild(renderer.domElement);
    
            // Lighting
            const ambientLight = new THREE.AmbientLight(0x404040, 1.5); // Soft white light
            scene.add(ambientLight);
    
            // Headlamp: Attach DirectionalLight to camera so it stays with the viewer
            const directionLight = new THREE.DirectionalLight(0xffffff, 2);
            directionLight.position.set(0, 0, 1); // From camera towards scene
            camera.add(directionLight);
            scene.add(camera); // Required because light is child of camera
    
            // Geometry: Star Dodecahedron Generation
            // 1. Create base Dodecahedron to get face data
            const baseGeometry = new THREE.DodecahedronGeometry(1).toNonIndexed();
            const basePos = baseGeometry.attributes.position;
            const baseNorm = baseGeometry.attributes.normal;
    
            const newPositions = [];
            const newNormals = [];
    
            // Helper to compare vectors with tolerance
            function isSame(v1, v2) {
                return v1.distanceTo(v2) < 0.01; // Relaxed tolerance
            }
    
            // Group vertices by Face Normal to identify pentagons
            const faces = [];
            // Instead of a map key, let's iterate and group by dot product similarity
            for (let i = 0; i < baseNorm.count; i += 3) {
                const n = new THREE.Vector3().fromBufferAttribute(baseNorm, i);
                const v1 = new THREE.Vector3().fromBufferAttribute(basePos, i);
                const v2 = new THREE.Vector3().fromBufferAttribute(basePos, i + 1);
                const v3 = new THREE.Vector3().fromBufferAttribute(basePos, i + 2);
    
                // Find existing face with similar normal
                let face = faces.find(f => f.normal.dot(n) > 0.999);
                if (!face) {
                    face = { normal: n, vertices: [] };
                    faces.push(face);
                }
                face.vertices.push(v1, v2, v3);
            }
    
            // Process each face to create a Star
            faces.forEach(face => {
                // 1. Deduplicate vertices
                const uniqueVerts = [];
                face.vertices.forEach(v => {
                    if (!uniqueVerts.some(uv => isSame(uv, v))) {
                        uniqueVerts.push(v);
                    }
                });
    
                if (uniqueVerts.length !== 5) {
                    // If it's not 5, something is wrong with the tolerance or grouping.
                    // It might be a regular dodecahedron always has 5 verts per face.
                    // Just in case, let's try to handle it or skip gracefully.
                    console.warn("Face is not a pentagon?", uniqueVerts.length);
                    return;
                }
    
                // 2. Calculate Center
                const center = new THREE.Vector3();
                uniqueVerts.forEach(v => center.add(v));
                center.divideScalar(5);
    
                // 3. Sort vertices
                // Create a basis frame ON the face plane
                const normal = face.normal.clone().normalize();
                // Robust base vector selection
                let up = new THREE.Vector3(0, 1, 0);
                if (Math.abs(normal.dot(up)) > 0.9) up.set(0, 0, 1);
    
                const tangent = new THREE.Vector3().crossVectors(normal, up).normalize();
                const bitangent = new THREE.Vector3().crossVectors(normal, tangent);
    
                uniqueVerts.sort((a, b) => {
                    const vecA = new THREE.Vector3().subVectors(a, center);
                    const vecB = new THREE.Vector3().subVectors(b, center);
                    const angleA = Math.atan2(vecA.dot(bitangent), vecA.dot(tangent));
                    const angleB = Math.atan2(vecB.dot(bitangent), vecB.dot(tangent));
                    return angleA - angleB;
                });
    
                // 4. Create Star Geometry
                // Need Tip Angle = 36 degrees.
                // Triangle formed by Top(Tip), Left(V1), Right(V2).
                // Side length of pentagon (s) = distance(V1, V2).
                // Triangle altitude (h) from midpoint to Tip.
                // tan(36/2) = tan(18) = (s/2) / h
                // h = (s/2) / tan(18)
    
                for (let i = 0; i < 5; i++) {
                    const valley = uniqueVerts[i];
                    const nextValley = uniqueVerts[(i + 1) % 5];
    
                    const sideLength = valley.distanceTo(nextValley);
                    const h = (sideLength / 2.0) / Math.tan(THREE.MathUtils.degToRad(18));
    
                    const midpoint = new THREE.Vector3().addVectors(valley, nextValley).multiplyScalar(0.5);
    
                    // Direction from Center to Midpoint is roughly the direction to extend (for regular pentagon)
                    // Actually, strict geometry: direction is Midpoint - Center normalized?
                    // Yes, for a regular pentagon, the apothem aligns with the star point direction.
                    const dir = new THREE.Vector3().subVectors(midpoint, center).normalize();
    
                    // Tip Position = Midpoint + dir * h
                    const tip = new THREE.Vector3().copy(midpoint).add(dir.multiplyScalar(h));
    
                    // Triangle 1: Center -> Valley -> Tip
                    newPositions.push(center.x, center.y, center.z);
                    newPositions.push(valley.x, valley.y, valley.z);
                    newPositions.push(tip.x, tip.y, tip.z);
                    newNormals.push(normal.x, normal.y, normal.z);
                    newNormals.push(normal.x, normal.y, normal.z);
                    newNormals.push(normal.x, normal.y, normal.z);
    
                    // Triangle 2: Center -> Tip -> NextValley
                    newPositions.push(center.x, center.y, center.z);
                    newPositions.push(tip.x, tip.y, tip.z);
                    newPositions.push(nextValley.x, nextValley.y, nextValley.z);
                    newNormals.push(normal.x, normal.y, normal.z);
                    newNormals.push(normal.x, normal.y, normal.z);
                    newNormals.push(normal.x, normal.y, normal.z);
                }
            });
    
            // Build Final Geometry
            let geometry = new THREE.BufferGeometry();
            geometry.setAttribute('position', new THREE.Float32BufferAttribute(newPositions, 3));
            geometry.setAttribute('normal', new THREE.Float32BufferAttribute(newNormals, 3));
    
            // Store original positions and normals for the explode effect
            const positionAttribute = geometry.attributes.position;
            const normalAttribute = geometry.attributes.normal;
            const originalPositions = positionAttribute.array.slice();
    
            // Material: Bright Light Blue
            const material = new THREE.MeshPhongMaterial({
                color: 0x00BFFF,
                flatShading: true,
                shininess: 30,
                polygonOffset: true,
                polygonOffsetFactor: 1, // Push mesh back slightly to prevent z-fighting with edges
                polygonOffsetUnits: 1,
                side: THREE.DoubleSide
            });
    
            const dodecahedron = new THREE.Mesh(geometry, material);
            scene.add(dodecahedron);
    
            // Edges: Make boundaries visible
            const edgesMaterial = new THREE.LineBasicMaterial({ color: 0x005580, linewidth: 2 }); // Darker blue edges
            let edges = new THREE.LineSegments(new THREE.EdgesGeometry(geometry), edgesMaterial);
            dodecahedron.add(edges);
    
            // Controls: OrbitControls handles the rotation logic correctly (avoiding simple Euler gimbal lock)
            const controls = new OrbitControls(camera, renderer.domElement);
            controls.enableDamping = true; // Smooth rotation
            controls.dampingFactor = 0.05;
            controls.enablePan = false; // Keep object centered
    
            // Interaction Logic:
            // Left click drag rotates the view (OrbitControls default).
            // This effectively changes the "viewing angle" as requested, while keeping the object stationary or appearing to rotate.
    
            // Explode Function
            function updateExplode(distance) {
                const positions = geometry.attributes.position.array;
                const normals = geometry.attributes.normal.array;
    
                for (let i = 0; i < positions.length; i += 3) {
                    // Determine face center direction?
                    // Actually for non-indexed flat geometry, the vertex normal points away from center of the face usually?
                    // For a Dodecahedron, vertex normals of flat faces ARE the face normals. It works perfectly.
    
                    positions[i] = originalPositions[i] + normals[i] * distance;
                    positions[i + 1] = originalPositions[i + 1] + normals[i + 1] * distance;
                    positions[i + 2] = originalPositions[i + 2] + normals[i + 2] * distance;
                }
                geometry.attributes.position.needsUpdate = true;
    
                // Re-generate edges to match new geometry
                dodecahedron.remove(edges);
                edges.geometry.dispose();
                edges = new THREE.LineSegments(new THREE.EdgesGeometry(geometry), edgesMaterial);
                dodecahedron.add(edges);
            }
    
            // Listener
            document.getElementById('explodeSlider').addEventListener('input', (e) => {
                updateExplode(parseFloat(e.target.value));
            });
    
            // Animation Loop
            function animate() {
                requestAnimationFrame(animate);
                controls.update(); // Required if damping or autoRotate
                renderer.render(scene, camera);
            }
    
            animate();
    
            // Handle Window Resize
            window.addEventListener('resize', onWindowResize, false);
    
            function onWindowResize() {
                camera.aspect = window.innerWidth / window.innerHeight;
                camera.updateProjectionMatrix();
                renderer.setSize(window.innerWidth, window.innerHeight);
            }
        </script>
    </body>
    
    </html>

    使わなくて良いです(重要)。

    起動すると、

    画像

    小星形十二面体

    が出ます。

    少しいじると、


    画像

    大星形十二面体

    が出ます。

    少しいじると、

    画像

    十二枚の星


    が出ます(?)。


    結局の所、どれが正解なのか?


    画像
    画像
    画像

    解なし(?????)



    追記


    小星形十二面体から大星形十二面体まで


    画像

    終わりです。




     
     
     
    フォロバ100%です。暇な時間を折り紙でつぶす人。たまに自分でも何を言っているのか分からなくなる。でもマイクラもフォトナもやる。(?)実は折り紙より得意。 …………………………

    あなたへのおすすめ