viewer.c.md 20 KB

This is the old version of the viewer. It does generate a vertex buffer only if the model is animated (simply because you can't animate a mesh without one), but even then it uses direct OpenGL commands. This is not as effective as the current version, but I kept this too because the way how it displays faces is more readable and helps understanding M3D structures better.

/*
 * m3dview/viewer.c
 *
 * Copyright (C) 2019 bzt (bztsrc@gitlab)
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use, copy,
 * modify, merge, publish, distribute, sublicense, and/or sell copies
 * of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 * @brief simple portable Model 3D viewer - Simple OpenGL commands
 * https://gitlab.com/bztsrc/model3d
 *
 */

#include "viewer.h"
#include "font6.h"
#define M3D_IMPLEMENTATION
#define M3D_ASCII
#include <m3d.h>
#include <gl.h>
#include <unistd.h>

/* variables */
unsigned char *buff = NULL;
unsigned int size, actionid = 0, frame = -1U, fpsdivisor = 1, lastframe = 0;
char *wintitle = NULL, infostr[64];
m3d_t *model = NULL;
int screenw = 640, screenh = 480, doframe = 0;
int mousebtn = 0, mousemove = 0, mousex = 0, mousey = 0;
float mindist = 1, maxdist = 25, distance = 5.5, pitch = 0/*-35.264*/, yaw = 0/*45*/;
float light_position[4] = { -1, 2, 2, 0 }, light_color[4] = { 1, 1, 0.5, 1 }, shadow_color[4] = { 0, 0, 0, 0 };
unsigned int numvbo = 0, numtexture, texture[32];
m3dv_t *vbo = NULL;
unsigned char checker_data[4*128*128];

/**
 * File reader callback for the M3D SDK, also used to load the model
 */
unsigned char *readfile(char *fn, unsigned int *size)
{
    FILE *f;
    unsigned char *ret = NULL;

    *size = 0;
    f = fopen(fn, "rb");
    if(f) {
        fseek(f, 0L, SEEK_END);
        *size = (unsigned int)ftell(f);
        fseek(f, 0L, SEEK_SET);
        ret = (unsigned char*)malloc(*size + 1);
        if(ret) {
            fread(ret, *size, 1, f);
            ret[*size] = 0;
        } else
            *size = 0;
        fclose(f);
    }
    return ret;
}

/**
 * Parse command line and load a model
 */
void load(int argc, char **argv)
{
    /* check arguments */
    if(argc < 2) {
        printf("Model 3D Viewer by bzt Copyright (C) 2019 MIT license\n\n"
           "./m3dview <m3d file>"
#ifdef PREVIEW
           " [out.png]"
#endif
           "\n\n");
        exit(0);
    }
    /* read the file */
    buff = readfile(argv[1], &size);
    if(!buff) error("unable to load model file");
    /* decode bit-chunk into in-memory C structures */
    model = m3d_load(buff, readfile, free, NULL);
    if(!model) error("unable to parse model");
    /* set up window title */
    wintitle = (char*)malloc(strlen(model->name) + strlen(model->license) + 32);
    if(!wintitle) error("unable to allocate memory");
    strcpy(wintitle, "Model 3D Viewer: ");
    strcat(wintitle, model->name);
    if(model->license[0]) {
        strcat(wintitle, " (");
        strcat(wintitle, model->license);
        strcat(wintitle, ")");
    }
    sprintf(infostr, "%d triangles, %d vertices (%d bit), %d actions",
        model->numface, model->numvertex, model->vc_s << 3, model->numaction);
}

/**
 * Multiply a vertex with a transformation matrix
 */
void vec3_mul_mat4(m3dv_t *out, m3dv_t *v, float *mat)
{
    out->x = mat[ 0] * v->x + mat[ 1] * v->y + mat[ 2] * v->z + mat[ 3];
    out->y = mat[ 4] * v->x + mat[ 5] * v->y + mat[ 6] * v->z + mat[ 7];
    out->z = mat[ 8] * v->x + mat[ 9] * v->y + mat[10] * v->z + mat[11];
}

/**
 * Convert a standard uint32_t color into OpenGL color
 */
void set_color(uint32_t c, float *f) {
    if(!c) {
        f[0] = f[1] = f[2] = 0.0; f[3] = 1.0;
    } else {
        f[3] = ((float)((c >> 24)&0xff)) / 255;
#ifndef PREVIEW
        f[2] = ((float)((c >> 16)&0xff)) / 255;
        f[1] = ((float)((c >>  8)&0xff)) / 255;
        f[0] = ((float)((c >>  0)&0xff)) / 255;
#else
        f[2] = ((float)((c >> 16)&0xff)) / 256;
        f[1] = ((float)((c >>  8)&0xff)) / 256;
        f[0] = ((float)((c >>  0)&0xff)) / 256;
#endif
    }
}

/**
 * Set material to use.
 */
void set_material(unsigned int mi)
{
    unsigned int i, t;
    float color[4];
    m3dm_t *m;

    if(mi == -1U || mi >= model->nummaterial) return;
    m = &model->material[mi];
    glEnd();
    /* reset GL material */
    glDisable(GL_TEXTURE_2D);
    glColor4f(0.5, 0.3, 0.1, 1);
    set_color(0, (float*)&color);
    glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, (GLfloat*)&color);
    glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, (GLfloat*)&color);
    glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, (GLfloat*)&color);
    glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, 0.0);
    /* update with what we have in this new material */
    for(i = 0; i < m->numprop; i++) {
        switch(m->prop[i].type) {
            case m3dp_Kd:
                set_color(m->prop[i].value.color, (float*)&color);
                glColor4fv((GLfloat*)&color);
            break;
            case m3dp_Ka:
                set_color(m->prop[i].value.color, (float*)&color);
                glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, (GLfloat*)&color);
            break;
            case m3dp_Ks:
                set_color(m->prop[i].value.color, (float*)&color);
                glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, (GLfloat*)&color);
            break;
            case m3dp_Ke:
                set_color(m->prop[i].value.color, (float*)&color);
                glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, (GLfloat*)&color);
            break;
            case m3dp_Ns:
                glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, m->prop[i].value.fnum);
            break;
            case m3dp_map_Kd:
                if(numtexture) {
                    t = m->prop[i].value.textureid < numtexture ? m->prop[i].value.textureid + 1 : 0;
                    glEnable(GL_TEXTURE_2D);
                    glBindTexture(GL_TEXTURE_2D, texture[t]);
                }
            break;
        }
    }
    glBegin(GL_TRIANGLES);
    return;
}

/**
 * Set FPS divisor for animation debugging
 */
void fpsdiv(int idx)
{
    switch(idx) {
        case 1: fpsdivisor = 2; break;
        case 2: fpsdivisor = 3; break;
        case 3: fpsdivisor = 5; break;
        case 4: fpsdivisor = 10; break;
        case 5: fpsdivisor = 15; break;
        case 6: fpsdivisor = 20; break;
        case 7: fpsdivisor = 25; break;
        case 8: fpsdivisor = 30; break;
        case 9: fpsdivisor = 60; break;
        default: fpsdivisor = 1; break;
    }
}

/**
 * Switch to next frame
 */
void nextframe(void)
{
    doframe = 0; frame++;
    continous();
}

/**
 * Switch to previous frame
 */
void prevframe(void)
{
    doframe = 0; frame--;
    continous();
}

/**
 * Toggle continous playback
 */
void continous(void)
{
    char *title, *name;

    doframe ^= 1;
    if(actionid < model->numaction) {
        if(frame == -1U) frame = model->action[actionid].numframe - 1;
        if(frame > model->action[actionid].numframe - 1) frame = 0;
    } else
        frame = 0;

    name = doframe ? (actionid < model->numaction ? model->action[actionid].name : "(bind-pose)") : model->name;

    title = (char*)malloc(strlen(name) + 64);
    if(title) {
        if(doframe)
            sprintf(title, "Model 3D Viewer: %s (frame %4d / %4d)", name,
                actionid < model->numaction ? frame + 1 : 1,
                actionid < model->numaction ? model->action[actionid].numframe : 1);
        else
            sprintf(title, "Model 3D Viewer: %s", model->name);
        set_title(title);
        free(title);
    }
}

/**
 * Zoom in
 */
void zoomin(void)
{
    distance -= 0.01 * distance;
    if(distance < 0.000001) distance = 0.000001;
}

/**
 * Zoom out
 */
void zoomout(void)
{
    distance += 0.01 * distance;
    if(distance > 100000) distance = 100000;
}

/**
 * Print an UTF-8 string
 */
void glPrint(char *s)
{
    unsigned int c;
    while(*s) {
        if((*s & 128) != 0) {
            if(!(*s & 32)) { c = ((*s & 0x1F)<<6)|(*(s+1) & 0x3F); s++; } else
            if(!(*s & 16)) { c = ((*s & 0xF)<<12)|((*(s+1) & 0x3F)<<6)|(*(s+2) & 0x3F); s += 2; } else
            if(!(*s & 8)) { c = ((*s & 0x7)<<18)|((*(s+1) & 0x3F)<<12)|((*(s+2) & 0x3F)<<6)|(*(s+3) & 0x3F); *s += 3; }
            else c = 0;
        } else c = *s;
        s++;
        if(c >= (unsigned int)(sizeof(font)/sizeof(font[0]))) c = 0;
        glBitmap(FONT_WIDTH, FONT_HEIGHT, 0, 0, (float)FONT_WIDTH, 0.0, ((unsigned char*)&font + c * FONT_HEIGHT));
    }
}

/**
 * Set up OpenGL context
 */
void setupgl(void)
{
    unsigned int i, j, k;

    glEnable(GL_MULTISAMPLE);
    glEnable(GL_NORMALIZE);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glClearColor(0.4, 0.4, 0.4, 1.0);
    glShadeModel(GL_SMOOTH);
    glFrontFace(GL_CCW);
    glEnable(GL_LIGHTING);
    glEnable(GL_LIGHT0);
    glLightfv(GL_LIGHT0, GL_POSITION, light_position);
    glLightfv(GL_LIGHT0, GL_SPECULAR, light_color);
    glLightfv(GL_LIGHT0, GL_AMBIENT, shadow_color);
    glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE);
    glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, GL_TRUE);
    glViewport(0, 0, screenw, screenh);
    /* set up vbo array */
    if(model->numface && model->numbone && model->numskin && model->numaction) {
        numvbo = model->numface * 3 /* triangle */ * 2 /* vertex + normal */;
        vbo = (m3dv_t*)malloc(numvbo * sizeof(m3dv_t));
        if(!vbo) error("unable to allocate memory");
        /* create separate vertex records in vbo, because animation may change differently
         * for different triangles that share vertex coordinates */
        for(i = k = 0; i < model->numface; i++)
            for(j = 0; j < 3; j++, k += 2) {
                memcpy(&vbo[k+0], &model->vertex[model->face[i].vertex[j]], sizeof(m3dv_t));
                memcpy(&vbo[k+1], &model->vertex[model->face[i].normal[j]], sizeof(m3dv_t));
                /* to avoid allocating extra memory, we store the original vertex id in vbo color */
                vbo[k+0].color = model->face[i].vertex[j];
                vbo[k+1].color = model->face[i].normal[j];
                /* copy vertex skinid to normal, normals usually are skin neutral in models */
                vbo[k+1].skinid = vbo[k+0].skinid;
            }
    }
    /* set up GL textures */
    glGetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, (GLint*)&numtexture);
    if(numtexture > 31) numtexture = 31;
    if(numtexture < 1) return;
    memset(texture, 0, sizeof(texture));
    glGenTextures(numtexture, (GLuint*)&texture);
    for (j = k = 0; j < 128; j++)
        for (i = 0; i < 128; i++) {
            checker_data[k++] = 127 + ((((i>>5) & 1) ^ ((j>>5) & 1)) << 5);
            checker_data[k++] = 85;
            checker_data[k++] = 25;
            checker_data[k++] = 255;
        }
    glBindTexture(GL_TEXTURE_2D, texture[0]);
    glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 128, 128, 0, GL_RGBA, GL_UNSIGNED_BYTE, checker_data);
    /* add textures from model */
    if(model->numtexture) {
        for(i = 0; i < numtexture && i < model->numtexture; i++) {
            if(!model->texture[i].w || !model->texture[i].h || !model->texture[i].d) {
                fprintf(stderr, "m3dview: unable to load texture '%s'\n", model->texture[i].name);
                texture[1 + i] = texture[0];
                continue;
            }
            glBindTexture(GL_TEXTURE_2D, texture[1 + i]);
            glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR_MIPMAP_LINEAR);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, model->texture[i].w, model->texture[i].h, 0, GL_RGBA, GL_UNSIGNED_BYTE,
                model->texture[i].d);
        }
    }
}

/**
 * Animate the mesh
 */
void animate_model(unsigned int msec)
{
    unsigned int i, j, s;
    m3db_t *animpose;
    m3dv_t tmp1, tmp2;

    if(actionid < model->numaction && doframe) {
        /* if we are frame debugging, use the exact timestamp of the frame as msec */
        msec = model->action[actionid].frame[frame].msec;
    } else
        /* otherwise modify by the debugging fps divisor (is 1 by default) */
        msec /= fpsdivisor;

    /* don't regenerate if we have the same timestamp as last time */
    if(msec == lastframe) return;
    lastframe = msec;

    /* get the animation-pose skeleton*/
    animpose = m3d_pose(model, actionid, msec);
    /* convert mesh vertices from bind-pose into animation-pose */
    for(i = 0; i < numvbo; i++) {
        s = vbo[i].skinid;
        if(s != -1U) {
            vbo[i].x = vbo[i].y = vbo[i].z = 0;
            for(j = 0; j < M3D_NUMBONE && model->skin[s].weight[j] > 0.0; j++) {
                /* transfer from bind-pose model-space into bone-local space */
                vec3_mul_mat4(&tmp1, &model->vertex[vbo[i].color], (float*)&model->bone[ model->skin[s].boneid[j] ].mat4);
                /* transfer from bone-local space into animation-pose model-space */
                vec3_mul_mat4(&tmp2, &tmp1, (float*)&animpose[ model->skin[s].boneid[j] ].mat4);
                /* adjust with weight and accumulate */
                vbo[i].x += tmp2.x * model->skin[s].weight[j];
                vbo[i].y += tmp2.y * model->skin[s].weight[j];
                vbo[i].z += tmp2.z * model->skin[s].weight[j];
            }
        }
    }
    free(animpose);
}

/**
 * Display the model
 */
void display(unsigned int msec)
{
    unsigned int i, j, k, mi = -1U, c = 0, lc = 0;
    m3dv_t *v;
    float color[4] = { 0, 0, 0, 1 }, fov;

    /* handle model rotation */
    if(mousemove) {
        yaw -= mousex * 0.3;
        pitch -= mousey * 0.2;
        if (pitch < -90) pitch = -90;
        if (pitch > 90) pitch = 90;
        if (yaw < 0) yaw += 360;
        if (yaw > 360) yaw -= 360;
        mousemove = 0;
    }
    /* switch action to animate */
    if(actionid == -1U) actionid = model->numaction;
    else if((unsigned int)actionid > model->numaction) actionid = 0;

    /* display model */
#ifdef PREVIEW
    if(!msec)
        glClearColor(1.0, 1.0, 1.0, 1.0);
#endif
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    fov = 0.221694 * (mindist/5);
    glFrustum(-fov, fov, -fov, fov, mindist/5, maxdist*5);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glEnable(GL_DEPTH_TEST);
    glTranslatef(0, 0, -distance*1.1);
    glRotatef(-pitch, 1, 0, 0);
    glRotatef(-yaw, 0, 1, 0);
    glTranslatef(0, 0, 0);
    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);

    /* draw the coordinate system */
    if(msec) {
        glDisable(GL_LIGHTING);
        glBegin(GL_LINES);
        glColor4f(1, 0, 0, 1);
        glVertex3f(0, 0, 0); glVertex3f(1.001, 0, 0);
        glColor4f(0.5, 0.4, 0.4, 1);
        glVertex3f(0, 0, 0); glVertex3f(-1.001, 0, 0);
        glColor4f(0, 1, 0, 1);
        glVertex3f(0, 0, 0); glVertex3f(0, 1.001, 0);
        glColor4f(0.4, 0.5, 0.4, 1);
        glVertex3f(0, 0, 0); glVertex3f(0, -1.001, 0);
        glColor4f(0, 0, 1, 1);
        glVertex3f(0, 0, 0); glVertex3f(0, 0, 1.001);
        glColor4f(0.4, 0.4, 0.5, 1);
        glVertex3f(0, 0, 0); glVertex3f(0, 0, -1.001);
        glEnd();
    }
    glColor4f(0.5, 0.3, 0.1, 1);

    /* draw the mesh */
    glEnable(GL_COLOR_MATERIAL);
    glEnable(GL_LIGHTING);
    glEnable(GL_LIGHT0);
    if(model->numtexture) {
        glEnable(GL_TEXTURE_2D);
        glBindTexture(GL_TEXTURE_2D, texture[0]);
    }
    glAlphaFunc(GL_GREATER, 0.2);
    glEnable(GL_ALPHA_TEST);
    glEnable(GL_BLEND);
    /* animate the mesh if we have a vertex buffer object */
    if(vbo)
        animate_model(msec);
    /* you should use glDrawArrays here. this is a simple, most portable viewer */
    glBegin(GL_TRIANGLES);
    for(i = k = 0; i < model->numface; i++) {
        /* if material changed */
        if(mi != model->face[i].materialid) {
            mi = model->face[i].materialid;
            set_material(mi);
            lc = 0;
        }
        /* we have triangles */
        for(j = 0; j < 3; j++, k += 2) {
            /* get normals and color */
            if(vbo) {
                v = &vbo[k+1];
                glNormal3f(v->x, v->y, v->z);
                v = &vbo[k];
                /* so save space, we've stored the original vertex id in vbo colors */
                c = model->vertex[vbo[k].color].color;
            } else {
                v = &model->vertex[model->face[i].normal[j]];
                glNormal3f(v->x, v->y, v->z);
                v = &model->vertex[model->face[i].vertex[j]];
                c = v->color;
            }
            /* if there's no material and color changed */
            if(mi == -1U && c != lc) {
                lc = c;
                set_color(c, (float*)&color);
                glColor4fv((GLfloat*)&color);
            }
            if(model->face[i].texcoord[j] != -1U) {
                glTexCoord2f(model->tmap[model->face[i].texcoord[j]].u, 1.0 - model->tmap[model->face[i].texcoord[j]].v);
            }
            /* add the vertex */
            glVertex3f(v->x, v->y, v->z);
        }
    }
    glEnd();

    glDisable(GL_BLEND);
    glDisable(GL_ALPHA_TEST);
    glDisable(GL_TEXTURE_2D);
    glDisable(GL_LIGHTING);
    glDisable(GL_COLOR_MATERIAL);
    glDisable(GL_DEPTH_TEST);
    /* on screen text */
    if(msec) {
        glMatrixMode(GL_PROJECTION);
        glLoadIdentity();
        glOrtho(0, screenw, screenh, 0, -1, 1);
        glMatrixMode(GL_MODELVIEW);
        glLoadIdentity();
        glColor4f(1, 1, 1, 1);
        glPushClientAttrib( GL_CLIENT_PIXEL_STORE_BIT );
        glPixelStorei( GL_UNPACK_SWAP_BYTES,  GL_FALSE );
        glPixelStorei( GL_UNPACK_LSB_FIRST,   GL_FALSE );
        glPixelStorei( GL_UNPACK_ROW_LENGTH,  0        );
        glPixelStorei( GL_UNPACK_SKIP_ROWS,   0        );
        glPixelStorei( GL_UNPACK_SKIP_PIXELS, 0        );
        glPixelStorei( GL_UNPACK_ALIGNMENT,   1        );
        glRasterPos2f(0.0, (float)FONT_HEIGHT);
        glPrint(model->name);
        glPrint(" (");
        glPrint(model->license[0] ? model->license : "no license");
        glPrint(", ");
        glPrint(model->author[0] ? model->author : "no author");
        glPrint(")");
        glRasterPos2f(0.0, (float)(FONT_HEIGHT*2));
        glPrint(infostr);
        if(model->numaction) {
            glPrint(", current: ");
            glPrint(actionid < model->numaction ? model->action[actionid].name : "(bind-pose)");
        }
        glPopClientAttrib();
    }
}

/**
 * Print an error message and quit
 */
void error(char *msg)
{
    fprintf(stderr, "m3dview: %s\n", msg);
    cleanup();
    exit(1);
}

/**
 * Clean up on exit
 */
void cleanup()
{
    if(model) m3d_free(model);
    if(vbo) free(vbo);
    if(buff) free(buff);
}