2021년 2월 4일 목요일

Autodesk Forge Digital Twin 코드 분석 및 UML 구조 역설계

이 글은 Autodesk Forge Digital Twin 소스 예제를 간단히 분석한다. 쉬운 이해를 위해, 구조와 소스코드를 먼저 분석하고, 이를 UML로 표현해 구조를 역설계 한다. 이를 통해, 실무적 관점에서 플랫폼 기반 디지털 트윈 서비스 개발 절차 및 방법, 고려사항 등을 확인해 봅니다.

개요
이 프로그램은 디지털 트윈의 특징 중 물리모델과 디지털모델의 연결, 실시간 IoT 센서 정보 반영, 시뮬레이션, 클라우드와 같은 기능을 구현한 것이다. 오토데스크 포지에 대한 설명은 다음 링크를 참고한다.
오토데스크 포지 디지털트윈 소프트웨어 아키텍처 분석 방법은 먼저 소스코드와 GUI 뷰를 분석한 후, 객체 및 클래스 정적 구조, 시퀀스 동적 구조를 역설계하는 순서로 진행한다. 

컴포넌트 구조 및 실행 방법
이 소프트웨어는 Autodesk Forge, Node.JS 자바스크립트 런타임 플랫폼, express 웹서버, Bootstrap UI 테마, MongoDB 데이터베이스를 사용하고 있다.

Autodesk Forge에 가입하고, 앞에 언급된 컴포넌트를 모두 설치한 후 다음 명령을 실행해 웹서버를 구동한다. 
git clone https://github.com/Autodesk-Forge/forge-digital-twin
npm install
export FORGE_CLIENT_ID=<client-id>
export FORGE_CLIENT_SECRET=<client-secret>
export FORGE_MODEL_URN=<model-urn>
export MONGODB_URL=<mongodb-connection-string>
npm start

3차원 디지털 모델은 아래 예제를 다운받아 SVF포맷으로 변환한 후 앞의 FORGE_MODEL_URN에 URN형식으로 경로를 제공한다.
View 구조 분석
HTML 뷰 렌더링 구조는 index.pug 에서 시작한다. 참고로, PUG(Python User Group)는 파이썬과 유사한 문법으로 HTML을 기술하는 HTML 렌더링 템플릿 미들웨어로 HTML을 간략한 문법으로 정의하고, 데이터베이스 객체와 바인딩을 쉽게 처리한다.
pug파일은 views 폴더에 저장되어 있다. 해당 pug 뷰를 분석하면 다음과 같은 웹 레이아웃을 구성하고 있는 것을 알 수 있다.
뷰 레이아웃 구조 분석

각 뷰를 정의한 pug는 script 함수를 통해 js 코드를 실행하게 되고, 실행된 js 코드에 의해 database 객체 데이터가 획득되어, 화면에 데이터를 출력한다. 비지니스 로직 최초 진입 부분은 main.js 의 Autodesk.Viewing.Initializer() 콜백 함수이고, 여기서 3차원 모델 로딩, 데이터 로딩 및 렌더링, 이벤트 등록 함수 호출이 실행된다.

소프트웨어 아키텍처 역설계
1. 정적구조 및 객체 정보모델 역설계
소스코드 분석 내용을 바탕으로 구조를 UML로 역설계한다. 주요 구조는 다음과 같다. 전형적인 MVC 디자인 패턴 구조이다. 
Digital Twin MVC 구조

아래와 같이 DATABASE는 MONGODB 스키마에서 객체 속성값을 로드하고 필요한 곳에 제공하는 역할을 한다. 
데이터베이스 model 구조

script 는 비지니스 로직 역할을 수행한다. 데이터베이스와 UI를 중간에서 연결하며, CRUD에 대한 로직을 처리한다.
script 구조

routes는 URL에 따른 페이지 뷰를 렌더링할 때 라우팅하는 역할을 한다.
routes 구조

Forge 뷰어의 객체 정보 모델 데이터 구조는 Autodesk 공식 문서에 설명되지 않아 사용 및 이해에 불편하다. 정보 모델 구조 분석을 위해 다음과 같이 DOM 구조를 덤프해 보았다. 
객체 정보 모델 구조 분석

포지의 정보 모델 구조는 다음과 같이 Composite 디자인 패턴을 사용하고 있어, 계층적 구조를 지원하며, 각 계층 요소인 node에서 object를 포함하고 있어, 속성을 관리한다. 
Autodesk Forge 객체 정보 모델 구조(UML)

property는 객체의 모든 속성들을 관리하는 데, 유연한 속성집합 관리를 위해, 속성은 {name, value} 구조로 되어 있다. name에 해당하는 displayName에 대한 displayValue는 type에 따라 정수, 실수, 문자열 등으로 해석되어야 한다. units는 값의 단위를 문자열로 정의한다. 각 속성값은 displayCategory로 그룹핑된다. 참고로, 각 객체의 속성 접근은 비동기 콜백 함수를 이용해 획득해야 한다. 다음은 그 예를 보여준다.

            var roomProperties = [];
            viewer.search('Room', function (idArray) {
                $.each(idArray, function(num, dbid) {
                    viewer.model.getProperties(dbid, function(result) {
                        if (result.properties) {
                            roomProperties.push(result);
                            console.log(result);
                            return;
                        }
                        return;
                    });
                });
            }); 

2. 동적구조 역설계
각 컴포넌트와 객체가 서로간에 어떤 순서로 호출을 처리하는 지 동적구조를 역설계한다. 호출방식은 Node.js 및 웹 특성상 웹서버나 미들웨어가 이벤트 기반 실행 처리하는 구조이므로, 직접적으로 드러나 있지는 않다. 분석 시 프로토콜대로 약속된 이벤트에서 호출된다는 것을 고려한다. 특히, 웹 기반 동작 환경(예. 자바스크립트 등)에서는 getProperties() 같은 주요 포지 함수 호출이 비동기 콜백함수로 처리되므로, 객체와 데이터를 연계시키기가 매우 까다롭다. 이 경우, promise, async등을 사용해 실행 시퀀스 구조를 개발해야 한다. 

포지의 큰 실행 흐름은 인증, 모델 버킷 생성, 모델 업로드, 뷰 실행, 비지니스 로직 실행이다. 다음 그림은 이를 보여준다.  

유스케이스 구조는 다음과 같다. 
유스케이스 구조

user가 login하면, authorize하고, 웹서버 실행해 접속을 listen한다. 접속되면, node.js 의 라우터가 index.pug를 렌더링한다. index는 header, viewer, sidebar, footer pug파일을 포함한다. viewer는 3D 모델(예. 기계, 건축, 건설, BIM 모델 파일 등)을 렌더링하는 3D viewer를 포함한다. sidebar는 데이터 계층구조를 나타내는 performance, maintenance 등을 렌더링한다. 도킹되는 패널은 footer.pug에서 포함된 main.js, performance.js, maintenance.js 등을 포함한다. 데이터는 database를 통해 fetch하여 가져온다. 이 중에 주요 시퀀스를 분석해 보자.

login의 server.js 시퀀스: express의 set함수로 웹페이지 렌더링 뷰는 pug로 설정하고, 라우팅을 위해, 기본 페이지 루트('/')를 index 페이지로 설정한다. 이외, 인증 페이지, 구매 및 유지보수 뷰 페이지 라우팅 URL을 설정하고, 서버를 실행(listen)한다. 다음 UML 시퀀스 구조는 이를 표현한다. 

main.js 시퀀스. 이 시퀀스는 다음과 같이 Autodesk 3차원 모델 뷰어를 생성하고 옵션을 설정한다. main.js의 실행 진입점은 Autodesk.Viewing.Initializer() callback 함수이다. 여기서, 모델, 데이터베이스 CRUD를 위한 함수 호출 및 이벤트 등록이 실행된다.

maintenance.js 시퀀스. 이 시퀀스는 탭 패널에 출력할 데이터를 얻어 값을 설정하고, click, active 등 이벤트가 발생되면, 다시 데이터를 업데이트하는 적절한 함수를 실행한다.  

나머지, performance.js 등도 이와 유사하게 동작한다.

소스 구조 분석
github 폴더 구조는 다음과 같다. 웹서버 시작은 server.js 코드에서 시작된다. 주요 소스는 model, views, public/script 폴더내의 /routes/auth.js, /routes/index.js, /routes/maintenance.js, /routes/procurement.js, /script/main.js, /script/maintenance.js, /script/performance.js, /script/procurement.js 이다.

server.js 에서 클라이언트 접속 listen 대기 후 로그인되면 웹페이지 파일 index.pug 이 렌더링되고, 이 파일에 포함된 header, viewer, footer등 pug파일이 렌더링된다. footer는 실행될 자바스크립트를 포함시켜 인터페이스에 필요한 모델, 데이터 로딩 함수를 호출한다. 다음은 그 주요 코드이다. 
// footer.pug
...
script(src='/scripts/performance.js')
script(src='/scripts/maintenance.js')
...

// main.js
Autodesk.Viewing.Initializer(options, () => {
    mainViewer = new Autodesk.Viewing.Private.GuiViewer3D();
    mainViewer.start();
    loadModel(DEMO_URN /* set by the server-side template engine */);
});

function loadModel(urn) {
    return new Promise(function(resolve, reject) {
    ...
                    initPerformanceTab(mainViewer);
                    initMaintenanceTab(mainViewer);
                    initProcurementTab(mainViewer);
                    initViewer(mainViewer);
                    resolve();
                })
    ...
    });
}

// performance.js
...
function initPerformanceTab(mainViewer) {
...
}

function createEngineSpeedChart() {
        const chart = new Chart(ctx, {
        }
}

server.js 분석
const express = require('express');  // 라이브러리 모듈 가져오기
const path = require('path');
const helmet = require('helmet');
const morgan = require('morgan');

const { FORGE_CLIENT_ID, FORGE_CLIENT_SECRET, FORGE_MODEL_URN, MONGODB_URL } = process.env;  // 환경변수 설정

const db = require('./model/db');   // DB 설정
const app = express();

app.use('/', require('./routes/index'));   // 웹서버 라우팅 주소 설정
app.use('/api/auth', require('./routes/auth'));
app.use('/api/procurement', require('./routes/procurement'));
app.use('/api/maintenance', require('./routes/maintenance'));

const port = process.env.PORT || 3000;
db.connect()
  .then(() => app.listen(port, () => console.log(`Server listening on port ${port}`))) // 서버실행

routes/auth.js 분석
let router = express.Router();
let auth = new AuthenticationClient(process.env.FORGE_CLIENT_ID, process.env.FORGE_CLIENT_SECRET);  // Forge 인증

router.get('/token', async function(req, res, next) {
    try {
        const result = await auth.authenticate(['viewables:read']);
        res.json({ access_token: result });
    } 
});

routes/index.js 분석
router.get('/', function(req, res) {
    res.render('index', { analytics: process.env.GOOGLE_ANALYTICS_TAG, urn: process.env.FORGE_MODEL_URN });
});

routes/maintenance.js 분석
const { Part, Review, Issue } = require('../model/maintenance');  // 유지보수 부품, 리뷰, 이슈 객체 생성
const PartTableLimit = 2048; // note: the jet engine model has cca 1600 parts
const ReviewTableLimit = 256;
const IssueTableLimit = 256;

function countParts() {
    return new Promise(function(resolve, reject) {
        Part.count({}, (err, count) => {        });  // 부품 갯수 
    });
}

function countReviews() {
    return new Promise(function(resolve, reject) {
        Review.count({}, (err, count) => {     });  // 검토 갯수
    });
}

function countIssues() {
    return new Promise(function(resolve, reject) {
        Issue.count({}, (err, count) => { }  // 이슈 갯수
        });
    });
}

function findOrCreatePart(id) {
    return new Promise(function(resolve, reject) {
        Part.findOne({ id }, (err, part) => {
                resolve(part);  // 부품 검색
            }
        });
    });
}

router.get('/issues', function(req, res) {
    let query = {};
    Issue.find(query, (err, issues) => {
            res.json(issues);  // 이슈 검색
        }
    });
});

view/index.pug
pug 형식으로 HTML view 정의하였다. 
doctype html
head
  include partials/head.pug
#container
  #header
    img(src='/images/forge-logo-gray.png' alt='Autodesk Forge Digital Twin Demo')
    h Digital Twin Demo
  #viewer
  #sidebar
    include partials/sidebar.pug
  include partials/footer.pug

view/maintenance.pug
ul#maintenance-tabs.nav.nav-tabs.mb-3.nav-fill(role='tablist')
  li.nav-item
    a#maintenance-revisions-tab.nav-link.active(data-toggle='tab' href='#maintenance-revisions' role='tab' aria-controls='maintenance-revisions' aria-selected='true') Revisions
  li.nav-item
    a#maintenance-issues-tab.nav-link(data-toggle='tab' href='#maintenance-issues' role='tab' aria-controls='maintenance-issues' aria-selected='false') Issues
  li.nav-item
    a#maintenance-statistics-tab.nav-link(data-toggle='tab' href='#maintenance-statistics' role='tab' aria-controls='maintenance-statistics' aria-selected='false') Statistics
  li.nav-item
    a#maintenance-instructions-tab.nav-link(data-toggle='tab' href='#maintenance-instructions' role='tab' aria-controls='maintenance-instructions' aria-selected='false') Docs
#maintenance-tabs-content.tab-content
  #maintenance-revisions.tab-pane.fade.show.active(role='tabpanel' aria-labelledby='maintenance-revisions-tab')
    include ./maintenance-revisions.pug
  #maintenance-issues.tab-pane.fade(role='tabpanel' aria-labelledby='maintenance-issues-tab')
    include ./maintenance-issues.pug
  #maintenance-statistics.tab-pane.fade(role='tabpanel' aria-labelledby='maintenance-statistics-tab')
    include ./maintenance-stats.pug    
  #maintenance-instructions.tab-pane.fade(role='tabpanel' aria-labelledby='maintenance-instructions-tab')
    include ./maintenance-instructions.pug   

public/scripts/main.js 분석
let mainViewer = null;
Autodesk.Viewing.Initializer(options, () => {
    mainViewer = new Autodesk.Viewing.Private.GuiViewer3D(
        document.getElementById('viewer'),
        { extensions: ['HeatmapExtension', 'IssuesExtension', 'AnimationExtension'] }
    );
    mainViewer.start();  // 뷰어 시작
    loadModel(DEMO_URN /* set by the server-side template engine */);  // 모델 로딩
});

function loadModel(urn) {
    return new Promise(function(resolve, reject) {
        function onDocumentLoadSuccess(doc) {
            const node = doc.getRoot().getDefaultGeometry();
            mainViewer.loadDocumentNode(doc, node);
        }
        Autodesk.Viewing.Document.load('urn:' + urn, onDocumentLoadSuccess, onDocumentLoadFailure);  // 모델 로딩
    });
}

function initViewer(viewer) {
    viewer.setQualityLevel(/* ambient shadows */ false, /* antialiasing */ true);  // 뷰어 옵션 설정
    viewer.setGroundShadow(true);
    viewer.setGroundReflection(false);
    viewer.setGhosting(true);
    viewer.setEnvMapBackground(true);
    viewer.setLightPreset(5);
    viewer.setSelectionColor(new THREE.Color(0xEBB30B));
}

public/scripts/maintenance.js 분석
function initMaintenanceTab(mainViewer) {
    let maintenanceViewer = null;

    async function updateRevisions(reload, partIds) {
        updateRevisionsTable();
        updateRevisionsPagination();
    }

    function updateRevisionsTable() // 리비전 테이블 화면 갱신 
    {
        for (let i = currPage * pageSize; i < (currPage + 1) * pageSize && i < list.length; i++) {
            const review = list[i];
            const date = new Date(review.createdAt);
            const $row = $(`
                <tr>
                    <th scope="row">${date.toLocaleDateString()}</th>
                    <td><a href="#" class="part-link">${review.partId}</a></td>
                    <td>${review.author}</td>
                    <td style="color: ${review.passed ? 'green' : 'red'};">${review.passed ? 'Good' : 'Bad'}</td>
                </tr>
            `);
            $tbody.append($row);
        }
    }

    function updateIssuesTable() // 이슈 테이블 화면 갱신 
    {
        for (let i = currPage * pageSize; i < (currPage + 1) * pageSize && i < list.length; i++) {
            const issue = list[i];
            const date = new Date(issue.createdAt);
            const $row = $(`
                <tr>
                    <th scope="row">${date.toLocaleDateString()}</th>
                    <td><a href="#" class="part-link">${issue.partId}</a></td>
                    <td>${issue.author}</td>
                    <td>${issue.text}</td>
                </tr>
            `);
            $tbody.append($row);
        }
    }
}

model/db.js 분석
const mongoose = require('mongoose');  // 몽고 디비 획득

class Database {
    async connect() { 
        try {
            await mongoose.connect(process.env.MONGODB_URL);  // 몽고 디비 연결
        } catch(err) {
        }
    }
}

module.exports = new Database();

model/maintenance.js 분석
const mongoose = require('mongoose');
const validator = require('validator');

// 부품 스키마 정의
const partSchema = new mongoose.Schema({
    id: {
        type: Number,
        required: true
    },
    status: {
        type: String,
        validate: validatePartStatus
    },
    nextReview: {
        type: Date
    }
});

// 리뷰 스키마 정의
const reviewSchema = new mongoose.Schema({
    createdAt: {
        type: Date,
        required: true
    },
    author: {
        type: String,
        required: true,
        validate: validateAuthor
    },
    passed: {
        type: Boolean,
        required: true
    },
    description: {
        type: String,
        validate: validateRevisionDescription
    },
    partId: {
        type: Number,
        required: true
    }
});

module.exports = {
    Part: mongoose.model('Part', partSchema),
    Review: mongoose.model('Review', reviewSchema),
    Issue: mongoose.model('Issue', issueSchema)
};

마무리
Autodesk Forge Digital Twin 웹 서비스는 전형적인 MVC(Model View Control) 디자인 패턴 구조로, 각 폴더의 model, views, public/script는 각각 MVC에 대응된다. 

시퀀스를 보면, start.js는 라우팅 설정을 하고, 웹서버를 우선 실행한다. routes는 인터넷 주소 입력 시 라우팅된 각 뷰 페이지를 redirect해서 렌더링되게 한다. model에서 DB to Object 맵핑을 처리하고, 맵핑된 object를 통해 view에 내용을 렌더링한다. view의 이벤트는 scripts에서 받아 처리한다. 

결론적으로 Autodesk Forge 기반 디지털 트윈 서비스 개발 순서는 다음과 같다.
1. 이해당자사, 요구사항, 유스케이스 및 아키텍처 설계. 예) SRS, BEP, SAD, UML, 의사코드 등
2. 디지털 모델과 데이터베이스 정의, 설계 및 개발. 예) DB Schema, BIM modeling 등 
3. 디지털 데이터 소스 준비. 예) IoT, RFID, Sensor, Agent 등
4. 웹서버 HW 및 SW 컴포넌트 준비. 예) 서버, Node.JS, DB, Express, Dashboard, ETL 등 
5. Autodesk Forge App 생성
6. GUI 뷰 설계. 예) HTML, CSS, PUG 등
7. 비지니스 로직 코딩
1) 모델 로딩 함수
2) 데이터 로딩 및 GUI 출력 함수
3) 데이터 CRUD 함수
4) 각종 이벤트 처리 함수
8. DNS 설정 및 IP 연결
9. 서비스 배포 및 운영

관련 소스는 뷰어를 조작하는 Forge API 사용방법을 충분히 보여주고 있어, 좋은 예제 역할을 하고 있다. 이제, 이를 이용해 BIM 모델, 데이터베이스와 IoT 데이터를 서로 연결해 보면 되겠다.

부록: Forge HTML DOM 구조 분석 및 Forge 뷰어 레이아웃 조정
3차원 모델에 데쉬보드를 추가하기 위해, Forge 화면 DOM 레이아웃 구조를 수정해야 한다. 개발 목적에 따라 모델과 데쉬보드 형식은 모두 다르므로, 오토데스크에서 제공하는 템플릿 코드를 그대로 사용할 수가 없다. 이런 이유로 Forge HTML DOM 구조 분석이 필요하다. 기본 구조는 다음과 같다. 
    
DOM HTML layout

DOM element는 화면에 렌더링될 때 해당 웹페이지에 지정된 css, style에 따라 표시된다. 참고로, html페이지에 설정된 meta, link, script는 다음과 같이 순서 의존 관계가 있으므로, 순서가 달라지면 제대로 표시되지 않을 수 있다.

Revit 3D viewer 옆에 차트를 만들고, 아래에 표를 배치하기 위해, Bootstrap layout 규약에 맞게 class에 row와 col을 적절히 지정한 후, 포지 뷰어가 생성되면 dashboard element안에 차트 canvas를 스크립트에서 생성해 주었다.
     <div class="container-fluid fill">
        <div class="row fill">               
            <div class="col-sm-7 fill">          
                <div id="forgeViewer"> 
                </div>
            </div>   
            <div class="col-sm-12 fill">
                <h4>IoT dataset</h4>
                <div class="table-responsive">
                    <table></table>
                </div>
            </div>
        </div>
     </div>

     <script>
        var row = $(".row").children();
        if(row.length > 0)
            $(row[0]).removeClass('col-sm-7').addClass('col-sm-9 transition-width').after('<div class="col-sm-3 transition-width" id="dashboard"></div>');
     </script> 

하지만, 결과는 제대로 처리되지 않는다. 이유는 지정된 viewer div 엘리먼트 안에서 adsk-viewing-viewer를 Forge가 생성할 때, 스타일을 강제적으로 height, width를 100%로 설정해 버리기 때문이다. 크롬 디버깅을 통해 화면 DOM의 해당 element 스타일을 확인해 보면 다음과 같이, css나 style을 외부에서 정의해 주어도 초기화되는 것을 알 수 있다. 

이런 이유로, 자바스크립트에서 다음과 같이 forge 뷰어가 생성된 후, 해당 element에 강제적으로 높이와 폭을 지정해준다.
    var style3D = "height: 60%; width: 65%; overflow: hidden;";
    $('.adsk-viewing-viewer').attr('style', style3D);

그럼, 다음과 같이 레이아웃이 제대로 표시되는 것을 확인할 수 있다. 
이외 다양한 레이아웃은 레퍼런스를 참고한다.

레퍼런스
추신: Forge와 같이 외부에서 개발된 요소를 가져다 쓸때 DOM을 개발회사 개발자가 강제 설정해 처리하는 경우가 종종 있다. 이 경우에도 이틀 동안 삽질 했는 데, 본인이 Bootstrap 레이아웃 규정에 맞게 DOM element 스타일 등 속성을 정의했음에도 제대로 화면 출력이 안되었다. 결국 어디가 잘못되었는 지 확인하기 위해서, 오토데스크에서 제공되는 해당 오픈소스를 모두 분석해야 했다. 먼가 이상하면, 어디서 속성이 변경될 수 있다는 것을 의심하고, css, style 등을 확인해 보고, 디버깅하여 DOM이 어떻게 변경되는 지를 확인해 보는 것이 좋다. 

추신: Forge의 기본 렌더링 기능은 좋으나, 특별히, 특정 재질이나 형태를 추가하고자 하면, THREE.js 를 직접 접근해 사용해야 하는 불편함이 있다. 이 경우, 아래 링크를 참고한다. 

SLAM 지원 오픈소스 OctoMap 소개

이 글은 SLAM을 지원하는 OctoMap을 간략히 설명하다.

OctoMap 라이브러리는 3D 포인트 클라우드 그리드 매핑 접근 방식을 구현하여, 로봇 공학에 적합한 C로 데이터 구조 및 매핑 알고리즘을 제공한다. SLAM 맵구현은 octree를 기반한다. 소스코드 및 라이브러리는 다음 링크에서 제공한다.
기능은 다음과 같다.
  • Full 3D 모델 제공. 맵은 사전 가정없이 환경 모델링 할 수 있다. 로봇의 경우, 환경에 대한 자율 탐색과 같이 알려지지 않은 영역에 대한 정보는 중요하며, 이 기술은 관련 정보를 자동 생성한다. 
  • 업데이트 가능. 새로운 센서 판독 값을 추가 할 수 있다. 동적 물체, 환경 변화로 인한 센서 노이즈를 제거한다. 여러 로봇이 동일한 맵에 데이터 제공 가능하며, 이전 기록된 맵은 새로운 영역을 탐색할 때 확장 할 수 있다. 지도는 필요에 따라 동적으로 확장된다. 지도는 다중 해상도이므로 높은 수준은 거친 지도를 사용할 수 있고, 로컬 플래너는 미세 해상도를 사용하여 작업 할 수 있다.  
  • 최적화. 맵은 메모리와 디스크 모두에 효율적으로 저장된다. 대역폭 제약 조건을 고려해, 로봇 간 교환 압축 파일을 생성 할 수 있다.

관련 자료는 다음 링크에서 다운로드 가능하다.

식물 질병 식별 인공지능 앱 개발 방법 소개

 이 글은 식물 질병 식별 인공지능 앱 개발 방법 소개한다.

이 문제를 해결하기 위한 다양한 방법이 있었다. 그 중에 다음 링크는 시간 투자 대 효과면에서 괜찬은 방법이다. 
최근 구글 컨퍼런스에서 유사한 내용이 설명된 적이 있다. 그 부분은 이 글을 참고한다.

2021년 2월 2일 화요일

오픈소스 활용한 Autodesk Forge 기반 BIM 모델 및 속성 뷰어 개발 방법

이 글은 오토데스크에서 제공하는 오픈소스를 활용한 Autodesk Forge 기반 BIM 모델 및 속성 뷰어 개발 방법을 간단히 다룬다. 아울러, 포지의 주요 기능을 확인한다.

이 글을 따라하면 다음과 같은 앱을 개발하고, 이를 커스터마이징할 수 있을 것이다.

Autodesk Forge 기반 BIM 모델 및 속성 뷰어

머리말

Autodesk Forge는 클라우드 기반으로 BIM360 등 솔류션과 연결되어, 디지털트윈, IoT, 데이터 분석 및 시뮬레이션 등 다양한 응용을 위한 플랫폼이 될 수 있다. 

Autodesk는 해당 분야를 선점하기 위해, 다양한 오픈소스 정책을 제공하고 있다. 이 글은 클라우드 기반 BIM 모델 및 속성 뷰어를 개발하기 위해, 오토데스크에서 제공한 오픈소스를 이용하는 방법을 간단히 알아본다. 참고로, 분석된 코드 구조 설명은 다음 링크를 참고한다.

Forge는 사용자가 개발한 앱을 생성, 관리하고, 앱에서 오토데스크 클라우드 플랫폼의 기능을 호출할 수 있는 API(Application Program Interface)를 제공한다. 아울러, API 사용 시 인증 방법을 제공하여 보안을 제공한다. Forge에서 제공되는 뷰어는 RVT, DWG, DXF, IFC, 3DS, DAE, OBJ, FBX, STEP, STL 등 거의 대부분의 2차원 및 3차원 모델 캐드 포맷을 지원한다(지원 형식). Forge App은 인터넷 기반으로 컴퓨터 뿐 아니라 스마트폰, 스마트패드 등 인터넷 웹이 접근되는 모든 곳에서 사용할 수 있다. 

Forge기반 앱 개발 순서는 보통 다음과 같다.

1. Forge 포털에 접속
2. Forge 포털에서 Create App 선택 및 앱 생성
3. App의 인증키에 해당하는 Client ID및 Secret ID 획득
4. NodeJS 기반 Javascript 등 언어를 이용해 Forge API 호출하여 앱 프로그램 코딩
5. NodeJS 기반일 경우, 앱에서 사용되는 패키지들 설치
6. 웹 서버 실행해 서비스 운영

시간 관계상 이 글에서는 앱 개발 시 사용하는 자바스크립트, NodeJS, 인증키에 대한 기본 설명은 하지 않는다. 이 기술에 대한 내용은 본 블로그 검색을 통해 확인할 수 있다. 오토데스크는 포지의 개발 지원을 위해, 수십개 이상의 서비스 유형에 대한 기본 템플릿을 오픈소스 형태로 깃허브(github)에 공개해 놓았다. 우리는 이를 활용할 것이다. 

BIM 모델 및 속성 뷰어 앱 개발

앱 개발 전에 이 글은 NodeJS를 웹 서버로 사용한다. 다음 웹사이트를 방문해 Node.js과 github 프로그램을 먼저 설치한다. 참고로, 다른 개발 환경은 여기를 참고해 해당 프로그램을 설치한다.

이제 다음 순서로 앱 개발해 본다.

1. Forge 포털 (forge.autodesk.com)에 접속 및 로그인

2. Forge 포털에서 Create App 메뉴 선택 및 앱 생성

메뉴를 선택하면, 앱 이름을 입력받는다. BIM viewer 등의 이름으로 입력한다. Callback URL은 "http://localhost:3000/api/forge/oauth/callback" 를 입력한다. 앱 정보 입력 마무리를 한다.

3. App의 인증키에 해당하는 Client ID및 Secret ID 획득

앱이 생성되면 Client ID, Secret ID를 다음과 같이 얻을 수 있다. 이 문자열은 코딩할 때 오토데스크 크라우드 플랫폼 API 호출 시 사용된다.  

입력 후 생성된 포지 앱 정보

4. NodeJS 기반 Javascript 등 언어를 이용해 Forge API 호출하여 앱 프로그램 코딩
이 단계는 오토데스크에서 제공하는 오픈소스를 사용한다. 
명령창(터미널)을 뛰우고 다음과 같이 실행한다.

생성된 폴더안에 들어간다. 

5. NodeJS 기반일 경우, 앱에서 사용되는 패키지들 설치
명령창에서 다음 명령을 입력해 앱에서 사용하는 패키지들을 설치한다.
npm install
npm init -y
npm install --save axios body-parser express
set FORGE_CLIENT_ID=<<앱의 CLIENT ID 입력>>
set FORGE_CLIENT_SECRET=<<앱의 CLIENT SECRET 입력>>

설치되는 패키지 모듈

6. 웹 서버 실행해 서비스 운영
다음 명령을 입력해, 앱 서버를 실행한다. 
npm start
웹서버 실행된 모습

만약, 인증키에서 에러가나면, 다음과 같이 앱 폴더 내의 start.js 소스코드를 편집해, 직접 해당 ID를 문자열로 직접 입력한다(아래 그림의 검정색 부분).

제대로 웹 서버가 실행되면, http://localhost:3000/ 에 접속해 본다. 
그럼, 다음과 같이 앱에서 생성된 웹 페이지가 렌더링된다. 파일 선택 버튼을 클릭하고, 래빗(revit)파일을 오토데스크 포지 클라우드에 업로드한다. 그럼, 버켓(bucket)에 해당 파일이 업로드된다.

업로드 끝난후 page 재갱신 버튼을 클릭하면, 다음과 같이 BIM 모델 및 속성 뷰어를 확인할 수 있다.

다음과 같이 BIM 모델 객체 트리, 속성창과 설정을 확인할 수 있다.

모델 단면 등을 검토할 수 있는 기능이 자동 제공된다. 

핵심 코드 설명
방금전 앱 생성으로 컴퓨터에 자동 생성된 폴더 및 소스 파일은 다음과 같다. 

동작순서는 다음과 같다. 
1. index.html 렌더링
2. Auth 인증 링크 클릭
3. Axios 이용해 인증 정보를 https://developer.api.autodesk.com/authentication/v1/authenticate 에 전달
4. 인증 성공하면, /api/forge/datamanagement/bucket/create 링크 열기. 모델 업로드할 버킷 키 POST하고 /api/forge/datamanagement/bucket/detail로 전송
5. /api/forge/datamanagement/bucket/detail 에서 버킷 키 전송에 성공하면, upload.html 폼 전송
6. 모델 업로드 폼에서 파일 선택 후 업로드하면, /api/forge/datamanagement/bucket/upload 전송
7. 모델 전송 성공하면, URN을 얻게된다. 업로드된 모델 URN을 /api/forge/modelderivative/ 에 전달
8. /api/forge/modelderivative/:urn 링크에 BIM 모델 URN, Format 등 전송하고, 성공하면, viewer.html 전송함. viewer에서는 업로드된 URN을 ForgeViewer에 전달해 모델을 렌더링함.

참고로, 1에서 7단계까지는 포지 앱 인증 및 모델 업로드에 해당하며, 실제 앱에서 비지니스 로직이 실행되는 부분은 8단계 이후가 된다. 

앱의 핵심 코드는 start.js와 viewer.html 파일안에 있다. 주요 부분만 설명하기로 한다.
var express = require('express');         // 웹서버로 express 모듈 패키지를 사용
var Axios = require('axios');               // http client 처리 axios 패키지 사용
var bodyParser = require('body-parser');    // JSON format 파싱 패키지 사용

// 웹 서버 기본 설정
var app = express();
app.use(bodyParser.json());
app.use(express.static(__dirname + '/www'));

// port 3000 에서 앱 서버 실행
app.set('port', 3000);
var server = app.listen(app.get('port'), function () {
    console.log('Server listening on port ' + server.address().port);
});

// Forge 앱 계정 인증키 설정. OAuth2 기술 사용
var FORGE_CLIENT_ID = 'Client ID';
var FORGE_CLIENT_SECRET = 'Secure ID';
var access_token = '';

// 인증키 처리 함수
app.get('/api/forge/oauth', function (req, res) {
    Axios({
        method: 'POST',
        url: 'https://developer.api.autodesk.com/authentication/v1/authenticate',
        headers: {
            'content-type': 'application/x-www-form-urlencoded',
        },
        data: querystring.stringify({
            client_id: FORGE_CLIENT_ID,
            client_secret: FORGE_CLIENT_SECRET,
            grant_type: 'client_credentials',
            scope: scopes
        })
    })
        .then(function (response) {
            // 인증 성공시 인증 토큰 획득. 토큰이 있어야 API 호출 가능
            access_token = response.data.access_token;
            console.log(response);
            res.redirect('/api/forge/datamanagement/bucket/create');
        })
});

var multer = require('multer');         // 모델 파일 업로드 위한 모듈 획득
var upload = multer({ dest: 'tmp/' }); // 모델 변환된 파일 임시 저장 폴더 설정

// 포지에 모델 파일 업로드 위한 버켓 설정 및 업로드
app.post('/api/forge/datamanagement/bucket/upload', upload.single('fileToUpload'), function (req, res) {
    var fs = require('fs'); // Node.js File system for reading files
    fs.readFile(req.file.path, function (err, filecontent) {
        Axios({
            method: 'PUT',
            url: 'https://developer.api.autodesk.com/oss/v2/buckets/' + encodeURIComponent(bucketKey) + '/objects/' + encodeURIComponent(req.file.originalname),
            headers: {
                Authorization: 'Bearer ' + access_token,
                'Content-Disposition': req.file.originalname,
                'Content-Length': filecontent.length
            },
            data: filecontent
        })
            .then(function (response) {
                // 업로드 성공 시 ModelDerivative 렌더링 페이지로 redirect 시킴.
                console.log(response);
                var urn = response.data.objectId.toBase64();
                res.redirect('/api/forge/modelderivative/' + urn);
            })
    });
});

// ModelDerivative 에서 모델 뷰어 처리함
app.get('/api/forge/modelderivative/:urn', function (req, res) {
    var urn = req.params.urn;
    var format_type = 'svf';
    var format_views = ['2d', '3d'];
    Axios({
        method: 'POST',
        url: 'https://developer.api.autodesk.com/modelderivative/v2/designdata/job',
        headers: {
            'content-type': 'application/json',
            Authorization: 'Bearer ' + access_token
        },
        data: JSON.stringify({
            'input': {
                'urn': urn   // 포지 버켓에 업로드된 모델 URN(Uniform Resource Name)
            },
        })
    })
        .then(function (response) {
            // 성공 시 뷰어 동작 시킴
            console.log(response);
            res.redirect('/viewer.html?urn=' + urn);
        })
});

<!DOCTYPE html>
<html>

<head>
    <title>Autodesk Forge: 3D Viewer App Sample</title>

    <meta http-equiv="cache-control" content="max-age=0" />
    <meta http-equiv="cache-control" content="no-cache" />
    <meta http-equiv="expires" content="0" />
    <meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT" />
    <meta http-equiv="pragma" content="no-cache" />
    <!-- Third Party package -->
    <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
    <!-- Autodesk Forge Viewer files (IMPORTANT) -->
    <link rel="stylesheet" href="https://developer.api.autodesk.com/modelderivative/v2/viewers/7.*/style.min.css" type="text/css">
    <script src="https://developer.api.autodesk.com/modelderivative/v2/viewers/7.*/viewer3D.min.js"></script>
    <style>
        /** Just simple CSS styling to make this page a little nicer **/
        body {
            margin: 0;
            padding: 0;
        }
    </style>
</head>

<body>
    <script>
        var viewer;
        var options = {
            env: 'AutodeskProduction',
            api: 'derivativeV2', // 뷰어에 사용하는 API
            getAccessToken: getForgeToken
        };
        var documentId = 'urn:' + getUrlParameter('urn');

        // 페이지 로딩 시 뷰어 정보 초기화됨
        Autodesk.Viewing.Initializer(options, function onInitialized(){
            // HTML 요소 중 MyViewerDiv 검색    
            var htmlElement = document.getElementById('MyViewerDiv');
            if (htmlElement) {
                // 검색된 요소에 BIM viewer attach함   
                viewer = new Autodesk.Viewing.GuiViewer3D(htmlElement);
                viewer.start();
                // 이 문서에 뷰어 로딩 처리함
                Autodesk.Viewing.Document.load(documentId, onDocumentLoadSuccess, onDocumentLoadFailure);
            }
        });        
  </script>
</body>

</html>

이제, 이 코드를 이용해, IoT 데이터와 BIM 객체를 연결하거나, 데쉬보드를 추가해 데이터를 가시화할 수 있을 것이다.

마무리
지금까지 간단히 오토데스크 포지를 이용한 BIM 모델 및 속성 뷰어 개발 방법을 살펴보았다. 관련 깃허브를 확인해 보면, 거의 대부분의 응용 영역에 대한 소스코드를 개발해 공개해 놓았는 데, 코드 품질이 매우 좋다. 
오토데스크 포지 깃허브

적절히 활용하면, IoT, 데이터 분석 및 시뮬레이션 등 다양한 응용 앱을 개발해 인터넷 웹 기반 서비스를 제공할 수 있을 것이다. 예를 들어, 이런 기술을 이용해 디지털트윈 서비스 개념을 구현하기 유용하다. 참고로, 디지털 트윈 개념적 이야기는 아래 글을 참고한다.

레퍼런스
부록: 포지 주요 기능 분석
포지 주요 기능은 포지 커뮤니티 블로그 코드 예제를 통해 확인할 수 있다. 주요 예제는 다음과 같다. 

부록: 포지 가격정책
물론 오토데스크 포지 사용비는 무료는 아니다. 이 링크에 가격정책이 표시되어 있고, 처음 사용시 90일 무료, 100크래딧을 준다. 

크래딧 의미는 다음 그림을 참고하라.
오토데스크 크래딧 정책

부록: 포지와 오토데스크 드라이브 연동
포지는 오토데스크 드라이브와 연동된다. 클라우드 드라이브이므로 인터넷 접속만 하면 언제든 관련 데이터를 포지로 가져올 수 있다. 깃허브 예제에 구글 드라이브에서 데이터 가져오는 방법도 있으니 참고 한다. 아울러, BIM 360과도 유기적으로 연동된다. 다만, 계정이 있어야 한다.
드라이브 폴더 모습
오토데스크 드라이브 기능 중 모델 체커 일부

추신. UNF에 온지 한달 지난 시점에서, 연구 행정관리로 밀려있었던 개발 내용들을 이곳 연구자들과 함께 정리하는 중. 미국은 연구자가 직접 개발하고 논문 쓰는 문화가 당연하고 일반적임^^ 한국 연구풍토는 관리해야 할 정책성 과제가 많고, 일이 몰리다보니 연구보다는 행정에 더 많은 시간을 빼앗기는 것 악순환이 문제. 돈대로 쓰고 결과는 없는 안타까운 일. 누구는 연구과제 관리하는 것도 연구라 말할지 모르겠지만, 본인은 어떤 기술 개발 과제에 본인이 직접 개발해 본게 없는 상태에서 하는 일들은 모두 행정이라 생각. 사실, 이런 일은 전공, 전문성이 1%도 필요 없이 엑셀, 워드만 잘하면 누구나 할 수 있다고 생각. 심하게는 기초적 일인 연구과제 발표자료도 외주화하는 이런 환경을 만들어 놓은 것은 사실 정부 기관 뿐 아니라 연구자 책임도 크다. 적당히 쉽게 주제 선택해 과제 수행하고 간접비나 이익 가져가면 된다는 편한 생각이 관행적으로 자리잡고 있음. 어느새 이런 일이 연구자가 해야 할 일처럼 착각하게 되었다. 여기에 해당 기술 전문성도 없어 겉포장 번지르한 과제 발표 결과에 면죄부 주는 평가 시스템 큰 역할. 이런 이유로, 결과는 얼마나 잘 포장하고 어떤 평가위원들이 오느냐에 따라 복걸복으로 결정되는 경우가 많음. 고질적 문제들. 이런 상황은 힘든 석박사 공부가 본인 브랜드로 계속 발전되기 정말 어려운 것이다. - 우리가 미국 실리콘밸리, 선진 유럽, 일본 심지어 중국 연구 결과에 못미치는 이유