From 7256502afa0babe60fcafbd2888cd3e33c3f9b6b Mon Sep 17 00:00:00 2001 From: xolatile Date: Wed, 16 Jul 2025 23:07:43 +0200 Subject: Source code, broken... --- src/engine/3dgui.cpp | 1398 +++++++++++++++ src/engine/animmodel.h | 1617 ++++++++++++++++++ src/engine/bih.cpp | 330 ++++ src/engine/bih.h | 79 + src/engine/blend.cpp | 862 ++++++++++ src/engine/blob.cpp | 727 ++++++++ src/engine/client.cpp | 274 +++ src/engine/command.cpp | 3484 ++++++++++++++++++++++++++++++++++++++ src/engine/console.cpp | 785 +++++++++ src/engine/decal.cpp | 642 +++++++ src/engine/depthfx.h | 193 +++ src/engine/dynlight.cpp | 227 +++ src/engine/engine.h | 613 +++++++ src/engine/explosion.h | 249 +++ src/engine/glare.cpp | 71 + src/engine/grass.cpp | 356 ++++ src/engine/lensflare.h | 193 +++ src/engine/lightmap.cpp | 2729 ++++++++++++++++++++++++++++++ src/engine/lightmap.h | 146 ++ src/engine/lightning.h | 123 ++ src/engine/main.cpp | 1422 ++++++++++++++++ src/engine/master.cpp | 718 ++++++++ src/engine/material.cpp | 886 ++++++++++ src/engine/md3.h | 183 ++ src/engine/md5.h | 419 +++++ src/engine/menus.cpp | 783 +++++++++ src/engine/model.h | 90 + src/engine/movie.cpp | 1157 +++++++++++++ src/engine/mpr.h | 575 +++++++ src/engine/normal.cpp | 383 +++++ src/engine/obj.h | 191 +++ src/engine/octa.cpp | 1880 +++++++++++++++++++++ src/engine/octa.h | 340 ++++ src/engine/octaedit.cpp | 2977 ++++++++++++++++++++++++++++++++ src/engine/octarender.cpp | 1803 ++++++++++++++++++++ src/engine/physics.cpp | 2057 +++++++++++++++++++++++ src/engine/pvs.cpp | 1315 +++++++++++++++ src/engine/ragdoll.h | 534 ++++++ src/engine/rendergl.cpp | 2388 ++++++++++++++++++++++++++ src/engine/rendermodel.cpp | 1142 +++++++++++++ src/engine/renderparticles.cpp | 1551 +++++++++++++++++ src/engine/rendersky.cpp | 774 +++++++++ src/engine/rendertarget.h | 464 +++++ src/engine/rendertext.cpp | 392 +++++ src/engine/renderva.cpp | 1896 +++++++++++++++++++++ src/engine/server.cpp | 1154 +++++++++++++ src/engine/serverbrowser.cpp | 751 +++++++++ src/engine/shader.cpp | 1522 +++++++++++++++++ src/engine/shadowmap.cpp | 329 ++++ src/engine/skelmodel.h | 1861 ++++++++++++++++++++ src/engine/smd.h | 447 +++++ src/engine/sound.cpp | 991 +++++++++++ src/engine/textedit.h | 770 +++++++++ src/engine/texture.cpp | 3644 ++++++++++++++++++++++++++++++++++++++++ src/engine/texture.h | 779 +++++++++ src/engine/vertmodel.h | 490 ++++++ src/engine/water.cpp | 1061 ++++++++++++ src/engine/world.cpp | 1391 +++++++++++++++ src/engine/world.h | 59 + src/engine/worldio.cpp | 1388 +++++++++++++++ 60 files changed, 58055 insertions(+) create mode 100644 src/engine/3dgui.cpp create mode 100644 src/engine/animmodel.h create mode 100644 src/engine/bih.cpp create mode 100644 src/engine/bih.h create mode 100644 src/engine/blend.cpp create mode 100644 src/engine/blob.cpp create mode 100644 src/engine/client.cpp create mode 100644 src/engine/command.cpp create mode 100644 src/engine/console.cpp create mode 100644 src/engine/decal.cpp create mode 100644 src/engine/depthfx.h create mode 100644 src/engine/dynlight.cpp create mode 100644 src/engine/engine.h create mode 100644 src/engine/explosion.h create mode 100644 src/engine/glare.cpp create mode 100644 src/engine/grass.cpp create mode 100644 src/engine/lensflare.h create mode 100644 src/engine/lightmap.cpp create mode 100644 src/engine/lightmap.h create mode 100644 src/engine/lightning.h create mode 100644 src/engine/main.cpp create mode 100644 src/engine/master.cpp create mode 100644 src/engine/material.cpp create mode 100644 src/engine/md3.h create mode 100644 src/engine/md5.h create mode 100644 src/engine/menus.cpp create mode 100644 src/engine/model.h create mode 100644 src/engine/movie.cpp create mode 100644 src/engine/mpr.h create mode 100644 src/engine/normal.cpp create mode 100644 src/engine/obj.h create mode 100644 src/engine/octa.cpp create mode 100644 src/engine/octa.h create mode 100644 src/engine/octaedit.cpp create mode 100644 src/engine/octarender.cpp create mode 100644 src/engine/physics.cpp create mode 100644 src/engine/pvs.cpp create mode 100644 src/engine/ragdoll.h create mode 100644 src/engine/rendergl.cpp create mode 100644 src/engine/rendermodel.cpp create mode 100644 src/engine/renderparticles.cpp create mode 100644 src/engine/rendersky.cpp create mode 100644 src/engine/rendertarget.h create mode 100644 src/engine/rendertext.cpp create mode 100644 src/engine/renderva.cpp create mode 100644 src/engine/server.cpp create mode 100644 src/engine/serverbrowser.cpp create mode 100644 src/engine/shader.cpp create mode 100644 src/engine/shadowmap.cpp create mode 100644 src/engine/skelmodel.h create mode 100644 src/engine/smd.h create mode 100644 src/engine/sound.cpp create mode 100644 src/engine/textedit.h create mode 100644 src/engine/texture.cpp create mode 100644 src/engine/texture.h create mode 100644 src/engine/vertmodel.h create mode 100644 src/engine/water.cpp create mode 100644 src/engine/world.cpp create mode 100644 src/engine/world.h create mode 100644 src/engine/worldio.cpp (limited to 'src/engine') diff --git a/src/engine/3dgui.cpp b/src/engine/3dgui.cpp new file mode 100644 index 0000000..f1f6ef2 --- /dev/null +++ b/src/engine/3dgui.cpp @@ -0,0 +1,1398 @@ +// creates multiple gui windows that float inside the 3d world + +// special feature is that its mostly *modeless*: you can use this menu while playing, without turning menus on or off +// implementationwise, it is *stateless*: it keeps no internal gui structure, hit tests are instant, usage & implementation is greatly simplified + +#include "engine.h" + +#include "textedit.h" + +static bool layoutpass, actionon = false; +static int mousebuttons = 0; +static struct gui *windowhit = NULL; + +static float firstx, firsty; + +enum {FIELDCOMMIT, FIELDABORT, FIELDEDIT, FIELDSHOW, FIELDKEY}; + +static int fieldmode = FIELDSHOW; +static bool fieldsactive = false; + +static bool hascursor; +static float cursorx = 0.5f, cursory = 0.5f; + +#define SHADOW 4 +#define ICON_SIZE (FONTH-SHADOW) +#define SKIN_W 256 +#define SKIN_H 128 +#define SKIN_SCALE 4 +#define INSERT (3*SKIN_SCALE) +#define MAXCOLUMNS 16 + +VARP(guiautotab, 6, 16, 40); +VARP(guiclicktab, 0, 0, 1); +VARP(guifadein, 0, 1, 1); +VARP(guipreviewtime, 0, 15, 1000); + +static int lastpreview = 0; + +static inline bool throttlepreview(bool loaded) +{ + if(loaded) return true; + if(totalmillis - lastpreview < guipreviewtime) return false; + lastpreview = totalmillis; + return true; +} + +struct gui : g3d_gui +{ + struct list + { + int parent, w, h, springs, curspring, column; + }; + + int firstlist, nextlist; + int columns[MAXCOLUMNS]; + + static vector lists; + static float hitx, hity; + static int curdepth, curlist, xsize, ysize, curx, cury; + static bool shouldmergehits, shouldautotab; + + static void reset() + { + lists.setsize(0); + } + + static int ty, tx, tpos, *tcurrent, tcolor; //tracking tab size and position since uses different layout method... + + bool allowautotab(bool on) + { + bool oldval = shouldautotab; + shouldautotab = on; + return oldval; + } + + void autotab() + { + if(tcurrent) + { + if(layoutpass && !tpos) tcurrent = NULL; //disable tabs because you didn't start with one + if(shouldautotab && !curdepth && (layoutpass ? 0 : cury) + ysize > guiautotab*FONTH) tab(NULL, tcolor); + } + } + + bool shouldtab() + { + if(tcurrent && shouldautotab) + { + if(layoutpass) + { + int space = guiautotab*FONTH - ysize; + if(space < 0) return true; + int l = lists[curlist].parent; + while(l >= 0) + { + space -= lists[l].h; + if(space < 0) return true; + l = lists[l].parent; + } + } + else + { + int space = guiautotab*FONTH - cury; + if(ysize > space) return true; + int l = lists[curlist].parent; + while(l >= 0) + { + if(lists[l].h > space) return true; + l = lists[l].parent; + } + } + } + return false; + } + + bool visible() { return (!tcurrent || tpos==*tcurrent) && !layoutpass; } + + //tab is always at top of page + void tab(const char *name, int color) + { + if(curdepth != 0) return; + if(color) tcolor = color; + tpos++; + if(!name) name = intstr(tpos); + int w = max(text_width(name) - 2*INSERT, 0); + if(layoutpass) + { + ty = max(ty, ysize); + ysize = 0; + } + else + { + cury = -ysize; + int h = FONTH-2*INSERT, + x1 = curx + tx, + x2 = x1 + w + ((skinx[3]-skinx[2]) + (skinx[5]-skinx[4]))*SKIN_SCALE, + y1 = cury - ((skiny[6]-skiny[1])-(skiny[3]-skiny[2]))*SKIN_SCALE-h, + y2 = cury; + bool hit = tcurrent && windowhit==this && hitx>=x1 && hity>=y1 && hitx=0) + { + lists[curlist].w = xsize; + lists[curlist].h = ysize; + } + list &l = lists.add(); + l.parent = curlist; + l.springs = 0; + l.column = -1; + curlist = lists.length()-1; + xsize = ysize = 0; + } + else + { + curlist = nextlist++; + if(curlist >= lists.length()) // should never get here unless script code doesn't use same amount of lists in layout and render passes + { + list &l = lists.add(); + l.parent = curlist; + l.springs = 0; + l.column = -1; + l.w = l.h = 0; + } + list &l = lists[curlist]; + l.curspring = 0; + if(l.springs > 0) + { + if(ishorizontal()) xsize = l.w; else ysize = l.h; + } + else + { + xsize = l.w; + ysize = l.h; + } + } + curdepth++; + } + + void poplist() + { + if(!lists.inrange(curlist)) return; + list &l = lists[curlist]; + if(layoutpass) + { + l.w = xsize; + l.h = ysize; + if(l.column >= 0) columns[l.column] = max(columns[l.column], ishorizontal() ? ysize : xsize); + } + curlist = l.parent; + curdepth--; + if(lists.inrange(curlist)) + { + int w = xsize, h = ysize; + if(ishorizontal()) cury -= h; else curx -= w; + list &p = lists[curlist]; + xsize = p.w; + ysize = p.h; + if(!layoutpass && p.springs > 0) + { + list &s = lists[p.parent]; + if(ishorizontal()) xsize = s.w; else ysize = s.h; + } + layout(w, h); + } + } + + int text (const char *text, int color, const char *icon) { autotab(); return button_(text, color, icon, false, false); } + int button(const char *text, int color, const char *icon) { autotab(); return button_(text, color, icon, true, false); } + int title (const char *text, int color, const char *icon) { autotab(); return button_(text, color, icon, false, true); } + + void separator() { autotab(); line_(FONTH/3); } + void progress(float percent) { autotab(); line_((FONTH*4)/5, percent); } + + //use to set min size (useful when you have progress bars) + void strut(float size) { layout(isvertical() ? int(size*FONTW) : 0, isvertical() ? 0 : int(size*FONTH)); } + //add space between list items + void space(float size) { layout(isvertical() ? 0 : int(size*FONTW), isvertical() ? int(size*FONTH) : 0); } + + void spring(int weight) + { + if(curlist < 0) return; + list &l = lists[curlist]; + if(layoutpass) { if(l.parent >= 0) l.springs += weight; return; } + int nextspring = min(l.curspring + weight, l.springs); + if(nextspring <= l.curspring) return; + if(ishorizontal()) + { + int w = xsize - l.w; + layout((w*nextspring)/l.springs - (w*l.curspring)/l.springs, 0); + } + else + { + int h = ysize - l.h; + layout(0, (h*nextspring)/l.springs - (h*l.curspring)/l.springs); + } + l.curspring = nextspring; + } + + void column(int col) + { + if(curlist < 0 || !layoutpass || col < 0 || col >= MAXCOLUMNS) return; + list &l = lists[curlist]; + l.column = col; + } + + int layout(int w, int h) + { + if(layoutpass) + { + if(ishorizontal()) + { + xsize += w; + ysize = max(ysize, h); + } + else + { + xsize = max(xsize, w); + ysize += h; + } + return 0; + } + else + { + bool hit = ishit(w, h); + if(ishorizontal()) curx += w; + else cury += h; + return (hit && visible()) ? mousebuttons|G3D_ROLLOVER : 0; + } + } + + bool mergehits(bool on) + { + bool oldval = shouldmergehits; + shouldmergehits = on; + return oldval; + } + + bool ishit(int w, int h, int x = curx, int y = cury) + { + if(shouldmergehits) return windowhit==this && (ishorizontal() ? hitx>=x && hitx=y && hity=x && hity>=y && hitx=0 && visible()) + { + bool hit = ishit(size+SHADOW, size+SHADOW); + float xs = size, ys = size, xi = curx, yi = cury; + if(overlaid && hit && actionon) + { + hudnotextureshader->set(); + gle::colorf(0, 0, 0, 0.75f); + rect_(xi+SHADOW, yi+SHADOW, xs, ys); + hudshader->set(); + } + int x1 = int(floor(screenw*(xi*scale.x+origin.x))), y1 = int(floor(screenh*(1 - ((yi+ys)*scale.y+origin.y)))), + x2 = int(ceil(screenw*((xi+xs)*scale.x+origin.x))), y2 = int(ceil(screenh*(1 - (yi*scale.y+origin.y)))); + glDisable(GL_BLEND); + modelpreview::start(x1, y1, x2-x1, y2-y1, overlaid!=NULL); + game::renderplayerpreview(model, team, weap); + modelpreview::end(); + hudshader->set(); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glEnable(GL_BLEND); + if(overlaid) + { + if(hit) + { + hudnotextureshader->set(); + glBlendFunc(GL_ZERO, GL_SRC_COLOR); + gle::colorf(1, 0.5f, 0.5f); + rect_(xi, yi, xs, ys); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + hudshader->set(); + } + if(overlaid[0]) text_(overlaid, xi + xs/12, yi + ys - ys/12 - FONTH, hit ? 0xFF0000 : 0xFFFFFF, hit, hit); + if(!overlaytex) overlaytex = textureload("data/guioverlay.png", 3); + gle::color(light); + glBindTexture(GL_TEXTURE_2D, overlaytex->id); + rect_(xi, yi, xs, ys, 0); + } + } + return layout(size+SHADOW, size+SHADOW); + } + + int modelpreview(const char *name, int anim, float sizescale, const char *overlaid, bool throttle) + { + autotab(); + if(sizescale==0) sizescale = 1; + int size = (int)(sizescale*2*FONTH)-SHADOW; + if(name[0] && visible() && (!throttle || throttlepreview(modelloaded(name)))) + { + bool hit = ishit(size+SHADOW, size+SHADOW); + float xs = size, ys = size, xi = curx, yi = cury; + if(overlaid && hit && actionon) + { + hudnotextureshader->set(); + gle::colorf(0, 0, 0, 0.75f); + rect_(xi+SHADOW, yi+SHADOW, xs, ys); + hudshader->set(); + } + int x1 = int(floor(screenw*(xi*scale.x+origin.x))), y1 = int(floor(screenh*(1 - ((yi+ys)*scale.y+origin.y)))), + x2 = int(ceil(screenw*((xi+xs)*scale.x+origin.x))), y2 = int(ceil(screenh*(1 - (yi*scale.y+origin.y)))); + glDisable(GL_BLEND); + modelpreview::start(x1, y1, x2-x1, y2-y1, overlaid!=NULL); + model *m = loadmodel(name); + if(m) + { + entitylight light; + light.color = vec(1, 1, 1); + light.dir = vec(0, -1, 2).normalize(); + vec center, radius; + m->boundbox(center, radius); + float yaw; + vec o = calcmodelpreviewpos(radius, yaw).sub(center); + rendermodel(&light, name, anim, o, yaw, 0, 0, NULL, NULL, 0); + } + modelpreview::end(); + hudshader->set(); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glEnable(GL_BLEND); + if(overlaid) + { + if(hit) + { + hudnotextureshader->set(); + glBlendFunc(GL_ZERO, GL_SRC_COLOR); + gle::colorf(1, 0.5f, 0.5f); + rect_(xi, yi, xs, ys); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + hudshader->set(); + } + if(overlaid[0]) text_(overlaid, xi + xs/12, yi + ys - ys/12 - FONTH, hit ? 0xFF0000 : 0xFFFFFF, hit, hit); + if(!overlaytex) overlaytex = textureload("data/guioverlay.png", 3); + gle::color(light); + glBindTexture(GL_TEXTURE_2D, overlaytex->id); + rect_(xi, yi, xs, ys, 0); + } + } + return layout(size+SHADOW, size+SHADOW); + } + + int prefabpreview(const char *prefab, const vec &color, float sizescale, const char *overlaid, bool throttle) + { + autotab(); + if(sizescale==0) sizescale = 1; + int size = (int)(sizescale*2*FONTH)-SHADOW; + if(prefab[0] && visible() && (!throttle || throttlepreview(prefabloaded(prefab)))) + { + bool hit = ishit(size+SHADOW, size+SHADOW); + float xs = size, ys = size, xi = curx, yi = cury; + if(overlaid && hit && actionon) + { + hudnotextureshader->set(); + gle::colorf(0, 0, 0, 0.75f); + rect_(xi+SHADOW, yi+SHADOW, xs, ys); + hudshader->set(); + } + int x1 = int(floor(screenw*(xi*scale.x+origin.x))), y1 = int(floor(screenh*(1 - ((yi+ys)*scale.y+origin.y)))), + x2 = int(ceil(screenw*((xi+xs)*scale.x+origin.x))), y2 = int(ceil(screenh*(1 - (yi*scale.y+origin.y)))); + glDisable(GL_BLEND); + modelpreview::start(x1, y1, x2-x1, y2-y1, overlaid!=NULL); + previewprefab(prefab, color); + modelpreview::end(); + hudshader->set(); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glEnable(GL_BLEND); + if(overlaid) + { + if(hit) + { + hudnotextureshader->set(); + glBlendFunc(GL_ZERO, GL_SRC_COLOR); + gle::colorf(1, 0.5f, 0.5f); + rect_(xi, yi, xs, ys); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + hudshader->set(); + } + if(overlaid[0]) text_(overlaid, xi + FONTH/2, yi + FONTH/2, hit ? 0xFF0000 : 0xFFFFFF, hit, hit); + if(!overlaytex) overlaytex = textureload("data/guioverlay.png", 3); + gle::color(light); + glBindTexture(GL_TEXTURE_2D, overlaytex->id); + rect_(xi, yi, xs, ys, 0); + } + } + return layout(size+SHADOW, size+SHADOW); + } + + void slider(int &val, int vmin, int vmax, int color, const char *label) + { + autotab(); + int x = curx; + int y = cury; + line_((FONTH*2)/3); + if(visible()) + { + if(!label) label = intstr(val); + int w = text_width(label); + + bool hit; + int px, py, offset = vmin < vmax ? clamp(val, vmin, vmax) : clamp(val, vmax, vmin); + if(ishorizontal()) + { + hit = ishit(FONTH, ysize, x, y); + px = x + (FONTH-w)/2; + py = y + (ysize-FONTH) - ((ysize-FONTH)*(offset-vmin))/((vmax==vmin) ? 1 : (vmax-vmin)); //vmin at bottom + } + else + { + hit = ishit(xsize, FONTH, x, y); + px = x + FONTH/2 - w/2 + ((xsize-w)*(offset-vmin))/((vmax==vmin) ? 1 : (vmax-vmin)); //vmin at left + py = y; + } + + if(hit) color = 0xFF0000; + text_(label, px, py, color, hit && actionon, hit); + if(hit && actionon) + { + int vnew = (vmin < vmax ? 1 : -1)+vmax-vmin; + if(ishorizontal()) vnew = int((vnew*(y+ysize-FONTH/2-hity))/(ysize-FONTH)); + else vnew = int((vnew*(hitx-x-FONTH/2))/(xsize-w)); + vnew += vmin; + vnew = vmin < vmax ? clamp(vnew, vmin, vmax) : clamp(vnew, vmax, vmin); + if(vnew != val) val = vnew; + } + } + } + + char *field(const char *name, int color, int length, int height, const char *initval, int initmode) + { + return field_(name, color, length, height, initval, initmode, FIELDEDIT); + } + + char *keyfield(const char *name, int color, int length, int height, const char *initval, int initmode) + { + return field_(name, color, length, height, initval, initmode, FIELDKEY); + } + + char *field_(const char *name, int color, int length, int height, const char *initval, int initmode, int fieldtype = FIELDEDIT) + { + editor *e = useeditor(name, initmode, false, initval); // generate a new editor if necessary + if(layoutpass) + { + if(initval && e->mode==EDITORFOCUSED && (e!=currentfocus() || fieldmode == FIELDSHOW)) + { + if(strcmp(e->lines[0].text, initval)) e->clear(initval); + } + e->linewrap = (length<0); + e->maxx = (e->linewrap) ? -1 : length; + e->maxy = (height<=0)?1:-1; + e->pixelwidth = abs(length)*FONTW; + if(e->linewrap && e->maxy==1) + { + int temp; + text_bounds(e->lines[0].text, temp, e->pixelheight, e->pixelwidth); //only single line editors can have variable height + } + else + e->pixelheight = FONTH*max(height, 1); + } + int h = e->pixelheight; + int w = e->pixelwidth + FONTW; + + bool wasvertical = isvertical(); + if(wasvertical && e->maxy != 1) pushlist(); + + char *result = NULL; + if(visible() && !layoutpass) + { + e->rendered = true; + + bool hit = ishit(w, h); + if(hit) + { + if(mousebuttons&G3D_DOWN) //mouse request focus + { + if(fieldtype==FIELDKEY) e->clear(); + useeditor(name, initmode, true); + e->mark(false); + fieldmode = fieldtype; + } + } + bool editing = (fieldmode != FIELDSHOW) && (e==currentfocus()); + if(hit && editing && (mousebuttons&G3D_PRESSED)!=0 && fieldtype==FIELDEDIT) e->hit(int(floor(hitx-(curx+FONTW/2))), int(floor(hity-cury)), (mousebuttons&G3D_DRAGGED)!=0); //mouse request position + if(editing && ((fieldmode==FIELDCOMMIT) || (fieldmode==FIELDABORT) || !hit)) // commit field if user pressed enter or wandered out of focus + { + if(fieldmode==FIELDCOMMIT || (fieldmode!=FIELDABORT && !hit)) result = e->currentline().text; + e->active = (e->mode!=EDITORFOCUSED); + fieldmode = FIELDSHOW; + } + else fieldsactive = true; + + e->draw(curx+FONTW/2, cury, color, hit && editing); + + hudnotextureshader->set(); + glDisable(GL_BLEND); + if(editing) gle::colorf(1, 0, 0); + else gle::colorub(color>>16, (color>>8)&0xFF, color&0xFF); + rect_(curx, cury, w, h, true); + glEnable(GL_BLEND); + hudshader->set(); + } + layout(w, h); + + if(e->maxy != 1) + { + int slines = e->limitscrolly(); + if(slines > 0) + { + int pos = e->scrolly; + slider(e->scrolly, slines, 0, color, NULL); + if(pos != e->scrolly) e->cy = e->scrolly; + } + if(wasvertical) poplist(); + } + + return result; + } + + void rect_(float x, float y, float w, float h, bool lines = false) + { + gle::defvertex(2); + gle::begin(lines ? GL_LINE_LOOP : GL_TRIANGLE_STRIP); + gle::attribf(x, y); + gle::attribf(x + w, y); + if(lines) gle::attribf(x + w, y + h); + gle::attribf(x, y + h); + if(!lines) gle::attribf(x + w, y + h); + xtraverts += gle::end(); + } + + void rect_(float x, float y, float w, float h, int usetc) + { + gle::defvertex(2); + gle::deftexcoord0(); + gle::begin(GL_TRIANGLE_STRIP); + static const vec2 tc[5] = { vec2(0, 0), vec2(1, 0), vec2(1, 1), vec2(0, 1), vec2(0, 0) }; + gle::attribf(x, y); gle::attrib(tc[usetc]); + gle::attribf(x + w, y); gle::attrib(tc[usetc+1]); + gle::attribf(x, y + h); gle::attrib(tc[usetc+3]); + gle::attribf(x + w, y + h); gle::attrib(tc[usetc+2]); + xtraverts += gle::end(); + } + + void text_(const char *text, int x, int y, int color, bool shadow, bool force = false) + { + if(shadow) draw_text(text, x+SHADOW, y+SHADOW, 0x00, 0x00, 0x00, -0xC0); + draw_text(text, x, y, color>>16, (color>>8)&0xFF, color&0xFF, force ? -0xFF : 0xFF); + } + + void background(int color, int inheritw, int inherith) + { + if(layoutpass) return; + hudnotextureshader->set(); + gle::colorub(color>>16, (color>>8)&0xFF, color&0xFF, 0x80); + int w = xsize, h = ysize; + if(inheritw>0) + { + int parentw = curlist, parentdepth = 0; + for(;parentdepth < inheritw && lists[parentw].parent>=0; parentdepth++) + parentw = lists[parentw].parent; + list &p = lists[parentw]; + w = p.springs > 0 && (curdepth-parentdepth)&1 ? lists[p.parent].w : p.w; + } + if(inherith>0) + { + int parenth = curlist, parentdepth = 0; + for(;parentdepth < inherith && lists[parenth].parent>=0; parentdepth++) + parenth = lists[parenth].parent; + list &p = lists[parenth]; + h = p.springs > 0 && !((curdepth-parentdepth)&1) ? lists[p.parent].h : p.h; + } + rect_(curx, cury, w, h); + hudshader->set(); + } + + void icon_(Texture *t, bool overlaid, int x, int y, int size, bool hit, const char *title = NULL) + { + float scale = float(size)/max(t->xs, t->ys); //scale and preserve aspect ratio + float xs = t->xs*scale, ys = t->ys*scale; + x += int((size-xs)/2); + y += int((size-ys)/2); + const vec &color = hit ? vec(1, 0.5f, 0.5f) : (overlaid ? vec(1, 1, 1) : light); + glBindTexture(GL_TEXTURE_2D, t->id); + if(hit && actionon) + { + gle::colorf(0, 0, 0, 0.75f); + rect_(x+SHADOW, y+SHADOW, xs, ys, 0); + } + gle::color(color); + rect_(x, y, xs, ys, 0); + + if(overlaid) + { + if(!overlaytex) overlaytex = textureload("data/guioverlay.png", 3); + glBindTexture(GL_TEXTURE_2D, overlaytex->id); + gle::color(light); + rect_(x, y, xs, ys, 0); + if(title) text_(title, x + xs/12, y + ys - ys/12 - FONTH, hit ? 0xFF0000 : 0xFFFFFF, hit && actionon, hit); + } + } + + void previewslot(VSlot &vslot, bool overlaid, int x, int y, int size, bool hit) + { + Slot &slot = *vslot.slot; + if(slot.sts.empty()) return; + VSlot *layer = NULL; + Texture *t = NULL, *glowtex = NULL, *layertex = NULL; + if(slot.loaded) + { + t = slot.sts[0].t; + if(t == notexture) return; + Slot &slot = *vslot.slot; + if(slot.texmask&(1<slot->sts.empty()) layertex = layer->slot->sts[0].t; + } + } + else if(slot.thumbnail && slot.thumbnail != notexture) t = slot.thumbnail; + else return; + float xt = min(1.0f, t->xs/(float)t->ys), yt = min(1.0f, t->ys/(float)t->xs), xs = size, ys = size; + if(hit && actionon) + { + hudnotextureshader->set(); + gle::colorf(0, 0, 0, 0.75f); + rect_(x+SHADOW, y+SHADOW, xs, ys); + hudshader->set(); + } + SETSHADER(hudrgb); + gle::defvertex(2); + gle::deftexcoord0(); + const vec &color = hit ? vec(1, 0.5f, 0.5f) : (overlaid ? vec(1, 1, 1) : light); + vec2 tc[4] = { vec2(0, 0), vec2(1, 0), vec2(1, 1), vec2(0, 1) }; + float xoff = vslot.offset.x, yoff = vslot.offset.y; + if(vslot.rotation) + { + const texrotation &r = texrotations[vslot.rotation]; + if(r.swapxy) { swap(xoff, yoff); loopk(4) swap(tc[k].x, tc[k].y); } + if(r.flipx) { xoff *= -1; loopk(4) tc[k].x *= -1; } + if(r.flipy) { yoff *= -1; loopk(4) tc[k].y *= -1; } + } + loopk(4) { tc[k].x = tc[k].x/xt - xoff/t->xs; tc[k].y = tc[k].y/yt - yoff/t->ys; } + if(slot.loaded) gle::color(vec(color).mul(vslot.colorscale)); + else gle::color(color); + glBindTexture(GL_TEXTURE_2D, t->id); + gle::begin(GL_TRIANGLE_STRIP); + gle::attribf(x, y); gle::attrib(tc[0]); + gle::attribf(x+xs, y); gle::attrib(tc[1]); + gle::attribf(x, y+ys); gle::attrib(tc[3]); + gle::attribf(x+xs, y+ys); gle::attrib(tc[2]); + gle::end(); + if(glowtex) + { + glBlendFunc(GL_SRC_ALPHA, GL_ONE); + glBindTexture(GL_TEXTURE_2D, glowtex->id); + if(hit || overlaid) gle::color(vec(vslot.glowcolor).mul(color)); + else gle::color(vslot.glowcolor); + gle::begin(GL_TRIANGLE_STRIP); + gle::attribf(x, y); gle::attrib(tc[0]); + gle::attribf(x+xs, y); gle::attrib(tc[1]); + gle::attribf(x, y+ys); gle::attrib(tc[3]); + gle::attribf(x+xs, y+ys); gle::attrib(tc[2]); + gle::end(); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + } + if(layertex) + { + glBindTexture(GL_TEXTURE_2D, layertex->id); + gle::color(vec(color).mul(layer->colorscale)); + gle::begin(GL_TRIANGLE_STRIP); + gle::attribf(x+xs/2, y+ys/2); gle::attrib(tc[0]); + gle::attribf(x+xs, y+ys/2); gle::attrib(tc[1]); + gle::attribf(x+xs/2, y+ys); gle::attrib(tc[3]); + gle::attribf(x+xs, y+ys); gle::attrib(tc[2]); + gle::end(); + } + + hudshader->set(); + if(overlaid) + { + if(!overlaytex) overlaytex = textureload("data/guioverlay.png", 3); + glBindTexture(GL_TEXTURE_2D, overlaytex->id); + gle::color(light); + rect_(x, y, xs, ys, 0); + } + } + + void line_(int size, float percent = 1.0f) + { + if(visible()) + { + if(!slidertex) slidertex = textureload("data/guislider.png", 3); + glBindTexture(GL_TEXTURE_2D, slidertex->id); + if(percent < 0.99f) + { + gle::colorf(light.x, light.y, light.z, 0.375f); + if(ishorizontal()) + rect_(curx + FONTH/2 - size/2, cury, size, ysize, 0); + else + rect_(curx, cury + FONTH/2 - size/2, xsize, size, 1); + } + gle::color(light); + if(ishorizontal()) + rect_(curx + FONTH/2 - size/2, cury + ysize*(1-percent), size, ysize*percent, 0); + else + rect_(curx, cury + FONTH/2 - size/2, xsize*percent, size, 1); + } + layout(ishorizontal() ? FONTH : 0, ishorizontal() ? 0 : FONTH); + } + + void textbox(const char *text, int width, int height, int color) + { + width *= FONTW; + height *= FONTH; + int w, h; + text_bounds(text, w, h, width); + if(h > height) height = h; + if(visible()) draw_text(text, curx, cury, color>>16, (color>>8)&0xFF, color&0xFF, 0xFF, -1, width); + layout(width, height); + } + + int button_(const char *text, int color, const char *icon, bool clickable, bool center) + { + const int padding = 10; + int w = 0; + if(icon) w += ICON_SIZE; + if(icon && text) w += padding; + if(text) w += text_width(text); + + if(visible()) + { + bool hit = ishit(w, FONTH); + if(hit && clickable) color = 0xFF0000; + int x = curx; + if(isvertical() && center) x += (xsize-w)/2; + + if(icon) + { + if(icon[0] != ' ') + { + const char *ext = strrchr(icon, '.'); + defformatstring(tname, "packages/icons/%s%s", icon, ext ? "" : ".jpg"); + icon_(textureload(tname, 3), false, x, cury, ICON_SIZE, clickable && hit); + } + x += ICON_SIZE; + } + if(icon && text) x += padding; + if(text) text_(text, x, cury, color, center || (hit && clickable && actionon), hit && clickable); + } + return layout(w, FONTH); + } + + static Texture *skintex, *overlaytex, *slidertex; + static const int skinx[], skiny[]; + static const struct patch { ushort left, right, top, bottom; uchar flags; } patches[]; + + static void drawskin(int x, int y, int gapw, int gaph, int start, int n, int passes = 1, const vec &light = vec(1, 1, 1), float alpha = 0.80f)//int vleft, int vright, int vtop, int vbottom, int start, int n) + { + if(!skintex) skintex = textureload("data/guiskin.png", 3); + glBindTexture(GL_TEXTURE_2D, skintex->id); + int gapx1 = INT_MAX, gapy1 = INT_MAX, gapx2 = INT_MAX, gapy2 = INT_MAX; + float wscale = 1.0f/(SKIN_W*SKIN_SCALE), hscale = 1.0f/(SKIN_H*SKIN_SCALE); + + loopj(passes) + { + bool quads = false; + if(passes>1) glDepthFunc(j ? GL_LEQUAL : GL_GREATER); + gle::color(j ? light : vec(1, 1, 1), passes<=1 || j ? alpha : alpha/2); //ghost when its behind something in depth + loopi(n) + { + const patch &p = patches[start+i]; + int left = skinx[p.left]*SKIN_SCALE, right = skinx[p.right]*SKIN_SCALE, + top = skiny[p.top]*SKIN_SCALE, bottom = skiny[p.bottom]*SKIN_SCALE; + float tleft = left*wscale, tright = right*wscale, + ttop = top*hscale, tbottom = bottom*hscale; + if(p.flags&0x1) + { + gapx1 = left; + gapx2 = right; + } + else if(left >= gapx2) + { + left += gapw - (gapx2-gapx1); + right += gapw - (gapx2-gapx1); + } + if(p.flags&0x10) + { + gapy1 = top; + gapy2 = bottom; + } + else if(top >= gapy2) + { + top += gaph - (gapy2-gapy1); + bottom += gaph - (gapy2-gapy1); + } + + //multiple tiled quads if necessary rather than a single stretched one + int ystep = bottom-top; + int yo = y+top; + while(ystep > 0) + { + if(p.flags&0x10 && yo+ystep-(y+top) > gaph) + { + ystep = gaph+y+top-yo; + tbottom = ttop+ystep*hscale; + } + int xstep = right-left; + int xo = x+left; + float tright2 = tright; + while(xstep > 0) + { + if(p.flags&0x01 && xo+xstep-(x+left) > gapw) + { + xstep = gapw+x+left-xo; + tright = tleft+xstep*wscale; + } + if(!quads) + { + quads = true; + gle::defvertex(2); + gle::deftexcoord0(); + gle::begin(GL_QUADS); + } + gle::attribf(xo, yo); gle::attribf(tleft, ttop); + gle::attribf(xo+xstep, yo); gle::attribf(tright, ttop); + gle::attribf(xo+xstep, yo+ystep); gle::attribf(tright, tbottom); + gle::attribf(xo, yo+ystep); gle::attribf(tleft, tbottom); + if(!(p.flags&0x01)) break; + xo += xstep; + } + tright = tright2; + if(!(p.flags&0x10)) break; + yo += ystep; + } + } + if(quads) xtraverts += gle::end(); + else break; //if it didn't happen on the first pass, it won't happen on the second.. + } + if(passes>1) glDepthFunc(GL_ALWAYS); + } + + vec origin, scale, *savedorigin; + float dist; + g3d_callback *cb; + bool gui2d; + + static float basescale, maxscale; + static bool passthrough; + static float alpha; + static vec light; + + void adjustscale() + { + int w = xsize + (skinx[2]-skinx[1])*SKIN_SCALE + (skinx[10]-skinx[9])*SKIN_SCALE, h = ysize + (skiny[9]-skiny[7])*SKIN_SCALE; + if(tcurrent) h += ((skiny[5]-skiny[1])-(skiny[3]-skiny[2]))*SKIN_SCALE + FONTH-2*INSERT; + else h += (skiny[6]-skiny[3])*SKIN_SCALE; + + float aspect = forceaspect ? 1.0f/forceaspect : float(screenh)/float(screenw), fit = 1.0f; + if(w*aspect*basescale>1.0f) fit = 1.0f/(w*aspect*basescale); + if(h*basescale*fit>maxscale) fit *= maxscale/(h*basescale*fit); + origin = vec(0.5f-((w-xsize)/2 - (skinx[2]-skinx[1])*SKIN_SCALE)*aspect*scale.x*fit, 0.5f + (0.5f*h-(skiny[9]-skiny[7])*SKIN_SCALE)*scale.y*fit, 0); + scale = vec(aspect*scale.x*fit, scale.y*fit, 1); + } + + void start(int starttime, float initscale, int *tab, bool allowinput) + { + if(gui2d) + { + initscale *= 0.025f; + if(allowinput) hascursor = true; + } + basescale = initscale; + if(layoutpass) scale.x = scale.y = scale.z = guifadein ? basescale*min((totalmillis-starttime)/300.0f, 1.0f) : basescale; + alpha = allowinput ? 0.80f : 0.60f; + passthrough = scale.xo.y, origin.x-camera1->o.x); + hudmatrix = camprojmatrix; + hudmatrix.translate(origin); + hudmatrix.rotate_around_z(yaw - 90*RAD); + hudmatrix.rotate_around_x(-90*RAD); + hudmatrix.scale(-scale.x, scale.y, scale.z); + + vec dir; + lightreaching(origin, light, dir, false, 0, 0.5f); + float intensity = vec(yaw, 0.0f).dot(dir); + light.mul(1.0f + max(intensity, 0.0f)); + } + + resethudmatrix(); + hudshader->set(); + + drawskin(curx-skinx[2]*SKIN_SCALE, cury-skiny[6]*SKIN_SCALE, xsize, ysize, 0, 9, gui2d ? 1 : 2, light, alpha); + if(!tcurrent) drawskin(curx-skinx[5]*SKIN_SCALE, cury-skiny[6]*SKIN_SCALE, xsize, 0, 9, 1, gui2d ? 1 : 2, light, alpha); + } + } + + void adjusthorizontalcolumn(int col, int i) + { + int h = columns[col], dh = 0; + for(int d = 1; i >= 0; d ^= 1) + { + list &p = lists[i]; + if(d&1) { dh = h - p.h; if(dh <= 0) break; p.h = h; } + else { p.h += dh; h = p.h; } + i = p.parent; + } + ysize += max(dh, 0); + } + + void adjustverticalcolumn(int col, int i) + { + int w = columns[col], dw = 0; + for(int d = 0; i >= 0; d ^= 1) + { + list &p = lists[i]; + if(d&1) { p.w += dw; w = p.w; } + else { dw = w - p.w; if(dw <= 0) break; p.w = w; } + i = p.parent; + } + xsize = max(xsize, w); + } + + void adjustcolumns() + { + if(lists.inrange(curlist)) + { + list &l = lists[curlist]; + if(l.column >= 0) columns[l.column] = max(columns[l.column], ishorizontal() ? ysize : xsize); + } + int parent = -1, depth = 0; + for(int i = firstlist; i < lists.length(); i++) + { + list &l = lists[i]; + if(l.parent > parent) { parent = l.parent; depth++; } + else if(l.parent < parent) + { + while(parent > l.parent && depth > 0) + { + parent = lists[parent].parent; + depth--; + } + } + if(l.column >= 0) + { + if(depth&1) adjusthorizontalcolumn(l.column, i); + else adjustverticalcolumn(l.column, i); + } + } + } + + void end() + { + if(layoutpass) + { + adjustcolumns(); + xsize = max(tx, xsize); + ysize = max(ty, ysize); + ysize = max(ysize, (skiny[7]-skiny[6])*SKIN_SCALE); + if(tcurrent) *tcurrent = max(1, min(*tcurrent, tpos)); + if(gui2d) adjustscale(); + if(!windowhit && !passthrough) + { + float dist = 0; + if(gui2d) + { + hitx = (cursorx - origin.x)/scale.x; + hity = (cursory - origin.y)/scale.y; + } + else + { + plane p; + p.toplane(vec(origin).sub(camera1->o).set(2, 0).normalize(), origin); + if(p.rayintersect(camera1->o, camdir, dist) && dist>=0) + { + vec hitpos(camdir); + hitpos.mul(dist).add(camera1->o).sub(origin); + hitx = vec(-p.y, p.x, 0).dot(hitpos)/scale.x; + hity = -hitpos.z/scale.y; + } + } + if((mousebuttons & G3D_PRESSED) && (fabs(hitx-firstx) > 2 || fabs(hity - firsty) > 2)) mousebuttons |= G3D_DRAGGED; + if(dist>=0 && hitx>=-xsize/2 && hitx<=xsize/2 && hity<=0) + { + if(hity>=-ysize || (tcurrent && hity>=-ysize-(FONTH-2*INSERT)-((skiny[6]-skiny[1])-(skiny[3]-skiny[2]))*SKIN_SCALE && hitx<=tx-xsize/2)) + windowhit = this; + } + } + } + else + { + if(tcurrent && txgui(*this, layoutpass); + } +}; + +Texture *gui::skintex = NULL, *gui::overlaytex = NULL, *gui::slidertex = NULL; + +//chop skin into a grid +const int gui::skiny[] = {0, 7, 21, 34, 43, 48, 56, 104, 111, 117, 128}, + gui::skinx[] = {0, 11, 23, 37, 105, 119, 137, 151, 215, 229, 246, 256}; +//Note: skinx[3]-skinx[2] = skinx[7]-skinx[6] +// skinx[5]-skinx[4] = skinx[9]-skinx[8] +const gui::patch gui::patches[] = +{ //arguably this data can be compressed - it depends on what else needs to be skinned in the future + {1,2,3,6, 0}, // body + {2,9,5,6, 0x01}, + {9,10,3,6, 0}, + + {1,2,6,7, 0x10}, + {2,9,6,7, 0x11}, + {9,10,6,7, 0x10}, + + {1,2,7,9, 0}, + {2,9,7,9, 0x01}, + {9,10,7,9, 0}, + + {5,6,3,5, 0x01}, // top + + {2,3,1,2, 0}, // selected tab + {3,4,1,2, 0x01}, + {4,5,1,2, 0}, + {2,3,2,3, 0x10}, + {3,4,2,3, 0x11}, + {4,5,2,3, 0x10}, + {2,3,3,5, 0}, + {3,4,3,5, 0x01}, + {4,5,3,5, 0}, + + {6,7,1,2, 0}, // deselected tab + {7,8,1,2, 0x01}, + {8,9,1,2, 0}, + {6,7,2,3, 0x10}, + {7,8,2,3, 0x11}, + {8,9,2,3, 0x10}, + {6,7,3,5, 0}, + {7,8,3,5, 0x01}, + {8,9,3,5, 0}, +}; + +vector gui::lists; +float gui::basescale, gui::maxscale = 1, gui::hitx, gui::hity, gui::alpha; +bool gui::passthrough, gui::shouldmergehits = false, gui::shouldautotab = true; +vec gui::light; +int gui::curdepth, gui::curlist, gui::xsize, gui::ysize, gui::curx, gui::cury; +int gui::ty, gui::tx, gui::tpos, *gui::tcurrent, gui::tcolor; +static vector guis2d, guis3d; + +VARP(guipushdist, 1, 4, 64); + +bool g3d_input(const char *str, int len) +{ + editor *e = currentfocus(); + if(fieldmode == FIELDKEY || fieldmode == FIELDSHOW || !e) return false; + + e->input(str, len); + return true; +} + +bool g3d_key(int code, bool isdown) +{ + editor *e = currentfocus(); + if(fieldmode == FIELDKEY) + { + switch(code) + { + case SDLK_ESCAPE: + if(isdown) fieldmode = FIELDCOMMIT; + return true; + } + const char *keyname = getkeyname(code); + if(keyname && isdown) + { + if(e->lines.length()!=1 || !e->lines[0].empty()) e->insert(" "); + e->insert(keyname); + } + return true; + } + + if(code==-1 && g3d_windowhit(isdown, true)) return true; + else if(code==-3 && g3d_windowhit(isdown, false)) return true; + + if(fieldmode == FIELDSHOW || !e) + { + if(windowhit) switch(code) + { + case -4: // window "management" + if(isdown) + { + if(windowhit->gui2d) + { + vec origin = *guis2d.last().savedorigin; + int i = windowhit - &guis2d[0]; + for(int j = guis2d.length()-1; j > i; j--) *guis2d[j].savedorigin = *guis2d[j-1].savedorigin; + *windowhit->savedorigin = origin; + if(guis2d.length() > 1) + { + if(camera1->o.dist(*windowhit->savedorigin) <= camera1->o.dist(*guis2d.last().savedorigin)) + windowhit->savedorigin->add(camdir); + } + } + else windowhit->savedorigin->add(vec(camdir).mul(guipushdist)); + } + return true; + case -5: + if(isdown) + { + if(windowhit->gui2d) + { + vec origin = *guis2d[0].savedorigin; + loopj(guis2d.length()-1) *guis2d[j].savedorigin = *guis2d[j + 1].savedorigin; + *guis2d.last().savedorigin = origin; + if(guis2d.length() > 1) + { + if(camera1->o.dist(*guis2d.last().savedorigin) >= camera1->o.dist(*guis2d[0].savedorigin)) + guis2d.last().savedorigin->sub(camdir); + } + } + else windowhit->savedorigin->sub(vec(camdir).mul(guipushdist)); + } + return true; + } + + return false; + } + switch(code) + { + case SDLK_ESCAPE: //cancel editing without commit + if(isdown) fieldmode = FIELDABORT; + return true; + case SDLK_RETURN: + case SDLK_TAB: + if(e->maxy != 1) break; + case SDLK_KP_ENTER: + if(isdown) fieldmode = FIELDCOMMIT; //signal field commit (handled when drawing field) + return true; + } + if(isdown) e->key(code); + return true; +} + +void g3d_cursorpos(float &x, float &y) +{ + if(guis2d.length()) { x = cursorx; y = cursory; } + else x = y = 0.5f; +} + +void g3d_resetcursor() +{ + cursorx = cursory = 0.5f; +} + +FVARP(guisens, 1e-3f, 1, 1e3f); + +bool g3d_movecursor(int dx, int dy) +{ + if(!guis2d.length() || !hascursor) return false; + const float CURSORSCALE = 500.0f; + cursorx = max(0.0f, min(1.0f, cursorx+guisens*dx*(screenh/(screenw*CURSORSCALE)))); + cursory = max(0.0f, min(1.0f, cursory+guisens*dy/CURSORSCALE)); + return true; +} + +VARNP(guifollow, useguifollow, 0, 1, 1); +VARNP(gui2d, usegui2d, 0, 1, 1); + +void g3d_addgui(g3d_callback *cb, vec &origin, int flags) +{ + bool gui2d = flags&GUI_FORCE_2D || (flags&GUI_2D && usegui2d) || mainmenu; + if(!gui2d && flags&GUI_FOLLOW && useguifollow) origin.z = player->o.z-(player->eyeheight-1); + gui &g = (gui2d ? guis2d : guis3d).add(); + g.cb = cb; + g.origin = origin; + g.savedorigin = &origin; + g.dist = flags&GUI_BOTTOM && gui2d ? 1e16f : camera1->o.dist(g.origin); + g.gui2d = gui2d; +} + +void g3d_limitscale(float scale) +{ + gui::maxscale = scale; +} + +static inline bool g3d_sort(const gui &a, const gui &b) { return a.dist < b.dist; } + +bool g3d_windowhit(bool on, bool act) +{ + extern int cleargui(int n); + if(act) + { + if(actionon || windowhit) + { + if(on) { firstx = gui::hitx; firsty = gui::hity; } + mousebuttons |= (actionon=on) ? G3D_DOWN : G3D_UP; + } + } else if(!on && windowhit) cleargui(1); + return (guis2d.length() && hascursor) || (windowhit && !windowhit->gui2d); +} + +void g3d_render() +{ + windowhit = NULL; + if(actionon) mousebuttons |= G3D_PRESSED; + + gui::reset(); + guis2d.shrink(0); + guis3d.shrink(0); + + // call all places in the engine that may want to render a gui from here, they call g3d_addgui() + extern void g3d_texturemenu(); + + if(!mainmenu) g3d_texturemenu(); + g3d_mainmenu(); + if(!mainmenu) game::g3d_gamemenus(); + + guis2d.sort(g3d_sort); + guis3d.sort(g3d_sort); + + readyeditors(); + fieldsactive = false; + + hascursor = false; + + layoutpass = true; + loopv(guis2d) guis2d[i].draw(); + loopv(guis3d) guis3d[i].draw(); + layoutpass = false; + + if(guis3d.length()) + { + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_ALWAYS); + glDepthMask(GL_FALSE); + + loopvrev(guis3d) guis3d[i].draw(); + + glDepthFunc(GL_LESS); + glDepthMask(GL_TRUE); + glDisable(GL_DEPTH_TEST); + + glDisable(GL_BLEND); + } +} + +void g3d_render2d() +{ + if(guis2d.length()) + { + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + loopvrev(guis2d) guis2d[i].draw(); + + glDisable(GL_BLEND); + } + + flusheditors(); + if(!fieldsactive) fieldmode = FIELDSHOW; //didn't draw any fields, so lose focus - mainly for menu closed + textinput(fieldmode!=FIELDSHOW, TI_GUI); + keyrepeat(fieldmode!=FIELDSHOW, KR_GUI); + + mousebuttons = 0; +} + +void consolebox(int x1, int y1, int x2, int y2) +{ + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + float bw = x2 - x1, bh = y2 - y1, aspect = bw/bh, sh = bh, sw = sh*aspect; + bw *= float(4*FONTH)/(SKIN_H*SKIN_SCALE); + bh *= float(4*FONTH)/(SKIN_H*SKIN_SCALE); + sw /= bw + (gui::skinx[2]-gui::skinx[1] + gui::skinx[10]-gui::skinx[9])*SKIN_SCALE; + sh /= bh + (gui::skiny[9]-gui::skiny[7] + gui::skiny[6]-gui::skiny[4])*SKIN_SCALE; + pushhudmatrix(); + hudmatrix.translate(x1, y1, 0); + hudmatrix.scale(sw, sh, 1); + flushhudmatrix(); + gui::drawskin(-gui::skinx[1]*SKIN_SCALE, -gui::skiny[4]*SKIN_SCALE, int(bw), int(bh), 0, 9, 1, vec(1, 1, 1), 0.60f); + gui::drawskin((-gui::skinx[1] + gui::skinx[2] - gui::skinx[5])*SKIN_SCALE, -gui::skiny[4]*SKIN_SCALE, int(bw), 0, 9, 1, 1, vec(1, 1, 1), 0.60f); + pophudmatrix(); +} + diff --git a/src/engine/animmodel.h b/src/engine/animmodel.h new file mode 100644 index 0000000..16d5189 --- /dev/null +++ b/src/engine/animmodel.h @@ -0,0 +1,1617 @@ +VARFP(envmapmodels, 0, 1, 1, preloadmodelshaders(true)); +VARFP(bumpmodels, 0, 1, 1, preloadmodelshaders(true)); +VARP(fullbrightmodels, 0, 0, 200); + +struct animmodel : model +{ + struct animspec + { + int frame, range; + float speed; + int priority; + }; + + struct animpos + { + int anim, fr1, fr2; + float t; + + void setframes(const animinfo &info) + { + anim = info.anim; + if(info.range<=1) + { + fr1 = 0; + t = 0; + } + else + { + int time = info.anim&ANIM_SETTIME ? info.basetime : lastmillis-info.basetime; + fr1 = (int)(time/info.speed); // round to full frames + t = (time-fr1*info.speed)/info.speed; // progress of the frame, value from 0.0f to 1.0f + } + if(info.anim&ANIM_LOOP) + { + fr1 = fr1%info.range+info.frame; + fr2 = fr1+1; + if(fr2>=info.frame+info.range) fr2 = info.frame; + } + else + { + fr1 = min(fr1, info.range-1)+info.frame; + fr2 = min(fr1+1, info.frame+info.range-1); + } + if(info.anim&ANIM_REVERSE) + { + fr1 = (info.frame+info.range-1)-(fr1-info.frame); + fr2 = (info.frame+info.range-1)-(fr2-info.frame); + } + } + + bool operator==(const animpos &a) const { return fr1==a.fr1 && fr2==a.fr2 && (fr1==fr2 || t==a.t); } + bool operator!=(const animpos &a) const { return fr1!=a.fr1 || fr2!=a.fr2 || (fr1!=fr2 && t!=a.t); } + }; + + struct part; + + struct animstate + { + part *owner; + animpos cur, prev; + float interp; + + bool operator==(const animstate &a) const { return cur==a.cur && (interp<1 ? interp==a.interp && prev==a.prev : a.interp>=1); } + bool operator!=(const animstate &a) const { return cur!=a.cur || (interp<1 ? interp!=a.interp || prev!=a.prev : a.interp<1); } + }; + + struct linkedpart; + struct mesh; + + struct shaderparams + { + float spec, ambient, glow, glowdelta, glowpulse, specglare, glowglare, fullbright, envmapmin, envmapmax, scrollu, scrollv, alphatest; + + shaderparams() : spec(1.0f), ambient(0.3f), glow(3.0f), glowdelta(0), glowpulse(0), specglare(1), glowglare(1), fullbright(0), envmapmin(0), envmapmax(0), scrollu(0), scrollv(0), alphatest(0.9f) {} + }; + + struct shaderparamskey + { + static hashtable keys; + static int firstversion, lastversion; + + int version; + + shaderparamskey() : version(-1) {} + + bool checkversion() + { + if(version >= firstversion) return true; + version = lastversion; + if(++lastversion <= 0) + { + enumerate(keys, shaderparamskey, key, key.version = -1); + firstversion = 0; + lastversion = 1; + version = 0; + } + return false; + } + + static inline void invalidate() + { + firstversion = lastversion; + } + }; + + struct skin : shaderparams + { + part *owner; + Texture *tex, *masks, *envmap, *normalmap; + Shader *shader; + bool alphablend, cullface; + shaderparamskey *key; + + skin() : owner(0), tex(notexture), masks(notexture), envmap(NULL), normalmap(NULL), shader(NULL), alphablend(true), cullface(true), key(NULL) {} + + bool masked() const { return masks != notexture; } + bool envmapped() { return envmapmax>0 && envmapmodels; } + bool bumpmapped() { return normalmap && bumpmodels; } + bool tangents() { return bumpmapped(); } + bool alphatested() const { return alphatest > 0 && tex->type&Texture::ALPHA; } + + void setkey() + { + key = &shaderparamskey::keys[*this]; + } + + void setshaderparams(mesh *m, const animstate *as) + { + if(!Shader::lastshader) return; + + float mincolor = as->cur.anim&ANIM_FULLBRIGHT ? fullbrightmodels/100.0f : 0.0f; + if(fullbright) + { + gle::colorf(fullbright/2, fullbright/2, fullbright/2, transparent); + } + else + { + gle::color(vec(lightcolor).max(mincolor), transparent); + } + + if(key->checkversion() && Shader::lastshader->owner == key) return; + Shader::lastshader->owner = key; + + if(alphatested()) LOCALPARAMF(alphatest, alphatest); + + if(fullbright) + { + LOCALPARAMF(lightscale, 0, 0, 2); + } + else + { + float bias = max(mincolor-1.0f, 0.2f), scale = 0.5f*max(0.8f-bias, 0.0f), + minshade = scale*max(ambient, mincolor); + LOCALPARAMF(lightscale, scale - minshade, scale, minshade + bias); + } + float curglow = glow; + if(glowpulse > 0) + { + float curpulse = lastmillis*glowpulse; + curpulse -= floor(curpulse); + curglow += glowdelta*2*fabs(curpulse - 0.5f); + } + LOCALPARAMF(maskscale, 0.5f*spec, 0.5f*curglow, 16*specglare, 4*glowglare); + LOCALPARAMF(texscroll, scrollu*lastmillis/1000.0f, scrollv*lastmillis/1000.0f); + if(envmapped()) LOCALPARAMF(envmapscale, envmapmin-envmapmax, envmapmax); + } + + Shader *loadshader() + { + #define DOMODELSHADER(name, body) \ + do { \ + static Shader *name##shader = NULL; \ + if(!name##shader) name##shader = useshaderbyname(#name); \ + body; \ + } while(0) + #define LOADMODELSHADER(name) DOMODELSHADER(name, return name##shader) + #define SETMODELSHADER(m, name) DOMODELSHADER(name, (m)->setshader(name##shader)) + if(shader) return shader; + + string opts; + int optslen = 0; + if(alphatested()) opts[optslen++] = 'a'; + if(owner->tangents()) opts[optslen++] = 'q'; + if(bumpmapped()) opts[optslen++] = 'n'; + if(envmapped()) opts[optslen++] = 'e'; + if(masked()) opts[optslen++] = 'm'; + if(!fullbright && (masked() || spec>=0.01f)) opts[optslen++] = 's'; + opts[optslen++] = '\0'; + + defformatstring(name, "model%s", opts); + shader = generateshader(name, "modelshader \"%s\"", opts); + return shader; + } + + void cleanup() + { + if(shader && shader->standard) shader = NULL; + } + + void preloadBIH() + { + if(tex->type&Texture::ALPHA && !tex->alphamask) loadalphamask(tex); + } + + void preloadshader(bool force) + { + if(force) cleanup(); + loadshader(); + } + + void setshader(mesh *m, const animstate *as) + { + m->setshader(loadshader()); + } + + void bind(mesh *b, const animstate *as) + { + if(!cullface && enablecullface) { glDisable(GL_CULL_FACE); enablecullface = false; } + else if(cullface && !enablecullface) { glEnable(GL_CULL_FACE); enablecullface = true; } + + if(as->cur.anim&ANIM_NOSKIN) + { + if(enablealphablend) { glDisable(GL_BLEND); enablealphablend = false; } + if(shadowmapping) SETMODELSHADER(b, shadowmapcaster); + else /*if(as->cur.anim&ANIM_SHADOW)*/ SETMODELSHADER(b, notexturemodel); + return; + } + setshader(b, as); + setshaderparams(b, as); + int activetmu = 0; + if(tex!=lasttex) + { + glBindTexture(GL_TEXTURE_2D, tex->id); + lasttex = tex; + } + if(bumpmapped() && normalmap !=lastnormalmap) + { + glActiveTexture_(GL_TEXTURE3); + activetmu = 3; + glBindTexture(GL_TEXTURE_2D, normalmap->id); + lastnormalmap = normalmap; + } + if(tex->type&Texture::ALPHA) + { + if(alphablend) + { + if(!enablealphablend && !reflecting && !refracting) + { + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + enablealphablend = true; + } + } + else if(enablealphablend) { glDisable(GL_BLEND); enablealphablend = false; } + } + else if(enablealphablend && transparent>=1) { glDisable(GL_BLEND); enablealphablend = false; } + if(masked() && masks!=lastmasks) + { + glActiveTexture_(GL_TEXTURE1); + activetmu = 1; + glBindTexture(GL_TEXTURE_2D, masks->id); + lastmasks = masks; + } + if(envmapped()) + { + GLuint emtex = envmap ? envmap->id : closestenvmaptex; + if(lastenvmaptex!=emtex) + { + glActiveTexture_(GL_TEXTURE2); + activetmu = 2; + glBindTexture(GL_TEXTURE_CUBE_MAP, emtex); + lastenvmaptex = emtex; + } + } + if(activetmu != 0) glActiveTexture_(GL_TEXTURE0); + } + }; + + struct meshgroup; + + struct mesh + { + meshgroup *group; + char *name; + bool noclip; + + mesh() : group(NULL), name(NULL), noclip(false) + { + } + + virtual ~mesh() + { + DELETEA(name); + } + + virtual void calcbb(vec &bbmin, vec &bbmax, const matrix4x3 &m) {} + + virtual void genBIH(BIH::mesh &m) {} + void genBIH(skin &s, vector &bih, const matrix4x3 &t) + { + BIH::mesh &m = bih.add(); + m.xform = t; + m.tex = s.tex; + if(s.tex->type&Texture::ALPHA) m.flags |= BIH::MESH_ALPHA; + if(noclip) m.flags |= BIH::MESH_NOCLIP; + if(s.cullface) m.flags |= BIH::MESH_CULLFACE; + genBIH(m); + while(bih.last().numtris > BIH::mesh::MAXTRIS) + { + BIH::mesh &overflow = bih.dup(); + overflow.tris += BIH::mesh::MAXTRIS; + overflow.numtris -= BIH::mesh::MAXTRIS; + bih[bih.length()-2].numtris = BIH::mesh::MAXTRIS; + } + } + + virtual void setshader(Shader *s) + { + if(glaring) s->setvariant(0, 1); + else s->set(); + } + + template void smoothnorms(V *verts, int numverts, T *tris, int numtris, float limit, bool areaweight) + { + hashtable share; + int *next = new int[numverts]; + memset(next, -1, numverts*sizeof(int)); + loopi(numverts) + { + V &v = verts[i]; + v.norm = vec(0, 0, 0); + int idx = share.access(v.pos, i); + if(idx != i) { next[i] = next[idx]; next[idx] = i; } + } + loopi(numtris) + { + T &t = tris[i]; + V &v1 = verts[t.vert[0]], &v2 = verts[t.vert[1]], &v3 = verts[t.vert[2]]; + vec norm; + norm.cross(vec(v2.pos).sub(v1.pos), vec(v3.pos).sub(v1.pos)); + if(!areaweight) norm.normalize(); + v1.norm.add(norm); + v2.norm.add(norm); + v3.norm.add(norm); + } + vec *norms = new vec[numverts]; + memclear(norms, numverts); + loopi(numverts) + { + V &v = verts[i]; + norms[i].add(v.norm); + if(next[i] >= 0) + { + float vlimit = limit*v.norm.magnitude(); + for(int j = next[i]; j >= 0; j = next[j]) + { + V &o = verts[j]; + if(v.norm.dot(o.norm) >= vlimit*o.norm.magnitude()) + { + norms[i].add(o.norm); + norms[j].add(v.norm); + } + } + } + } + loopi(numverts) verts[i].norm = norms[i].normalize(); + delete[] next; + delete[] norms; + } + + template void buildnorms(V *verts, int numverts, T *tris, int numtris, bool areaweight) + { + loopi(numverts) verts[i].norm = vec(0, 0, 0); + loopi(numtris) + { + T &t = tris[i]; + V &v1 = verts[t.vert[0]], &v2 = verts[t.vert[1]], &v3 = verts[t.vert[2]]; + vec norm; + norm.cross(vec(v2.pos).sub(v1.pos), vec(v3.pos).sub(v1.pos)); + if(!areaweight) norm.normalize(); + v1.norm.add(norm); + v2.norm.add(norm); + v3.norm.add(norm); + } + loopi(numverts) verts[i].norm.normalize(); + } + + template void buildnorms(V *verts, int numverts, T *tris, int numtris, bool areaweight, int numframes) + { + if(!numverts) return; + loopi(numframes) buildnorms(&verts[i*numverts], numverts, tris, numtris, areaweight); + } + + static inline void fixqtangent(quat &q, float bt) + { + static const float bias = -1.5f/65535, biasscale = sqrtf(1 - bias*bias); + if(bt < 0) + { + if(q.w >= 0) q.neg(); + if(q.w > bias) { q.mul3(biasscale); q.w = bias; } + } + else if(q.w < 0) q.neg(); + } + + template static inline void calctangent(V &v, const vec &n, const vec &t, float bt) + { + matrix3 m; + m.c = n; + m.a = t; + m.b.cross(m.c, m.a); + quat q(m); + fixqtangent(q, bt); + v.tangent = q; + } + + template void calctangents(B *bumpverts, V *verts, TC *tcverts, int numverts, T *tris, int numtris, bool areaweight) + { + vec *tangent = new vec[2*numverts], *bitangent = tangent+numverts; + memclear(tangent, 2*numverts); + loopi(numtris) + { + const T &t = tris[i]; + const vec &e0 = verts[t.vert[0]].pos; + vec e1 = vec(verts[t.vert[1]].pos).sub(e0), e2 = vec(verts[t.vert[2]].pos).sub(e0); + + const vec2 &tc0 = tcverts[t.vert[0]].tc, + &tc1 = tcverts[t.vert[1]].tc, + &tc2 = tcverts[t.vert[2]].tc; + float u1 = tc1.x - tc0.x, v1 = tc1.y - tc0.y, + u2 = tc2.x - tc0.x, v2 = tc2.y - tc0.y; + vec u(e2), v(e2); + u.mul(v1).sub(vec(e1).mul(v2)); + v.mul(u1).sub(vec(e1).mul(u2)); + + if(vec().cross(e2, e1).dot(vec().cross(v, u)) >= 0) + { + u.neg(); + v.neg(); + } + + if(!areaweight) + { + u.normalize(); + v.normalize(); + } + + loopj(3) + { + tangent[t.vert[j]].sub(u); + bitangent[t.vert[j]].add(v); + } + } + loopi(numverts) + { + const vec &n = verts[i].norm, + &t = tangent[i], + &bt = bitangent[i]; + B &bv = bumpverts[i]; + matrix3 m; + m.c = n; + (m.a = t).project(m.c).normalize(); + m.b.cross(m.c, m.a); + quat q(m); + fixqtangent(q, m.b.dot(bt)); + bv.tangent = q; + } + delete[] tangent; + } + + template void calctangents(B *bumpverts, V *verts, TC *tcverts, int numverts, T *tris, int numtris, bool areaweight, int numframes) + { + loopi(numframes) calctangents(&bumpverts[i*numverts], &verts[i*numverts], tcverts, numverts, tris, numtris, areaweight); + } + }; + + struct meshgroup + { + meshgroup *next; + int shared; + char *name; + vector meshes; + + meshgroup() : next(NULL), shared(0), name(NULL) + { + } + + virtual ~meshgroup() + { + DELETEA(name); + meshes.deletecontents(); + DELETEP(next); + } + + virtual int findtag(const char *name) { return -1; } + virtual void concattagtransform(part *p, int i, const matrix4x3 &m, matrix4x3 &n) {} + + void calcbb(vec &bbmin, vec &bbmax, const matrix4x3 &m) + { + loopv(meshes) meshes[i]->calcbb(bbmin, bbmax, m); + } + + void genBIH(vector &skins, vector &bih, const matrix4x3 &t) + { + loopv(meshes) meshes[i]->genBIH(skins[i], bih, t); + } + + virtual void *animkey() { return this; } + virtual int totalframes() const { return 1; } + bool hasframe(int i) const { return i>=0 && i=0 && i+n<=totalframes(); } + int clipframes(int i, int n) const { return min(n, totalframes() - i); } + + virtual void cleanup() {} + virtual void preload(part *p) {} + virtual void render(const animstate *as, float pitch, const vec &axis, const vec &forward, dynent *d, part *p) {} + + void bindpos(GLuint ebuf, GLuint vbuf, void *v, int stride) + { + if(lastebuf!=ebuf) + { + gle::bindebo(ebuf); + lastebuf = ebuf; + } + if(lastvbuf!=vbuf) + { + gle::bindvbo(vbuf); + if(!lastvbuf) gle::enablevertex(); + gle::vertexpointer(stride, v); + lastvbuf = vbuf; + } + } + + void bindtc(void *v, int stride) + { + if(!enabletc) + { + gle::enabletexcoord0(); + enabletc = true; + } + if(lasttcbuf!=lastvbuf) + { + gle::texcoord0pointer(stride, v); + lasttcbuf = lastvbuf; + } + } + + void bindnormals(void *v, int stride) + { + if(!enablenormals) + { + gle::enablenormal(); + enablenormals = true; + } + if(lastnbuf!=lastvbuf) + { + gle::normalpointer(stride, v); + lastnbuf = lastvbuf; + } + } + + void bindtangents(void *v, int stride) + { + if(!enabletangents) + { + gle::enabletangent(); + enabletangents = true; + } + if(lastxbuf!=lastvbuf) + { + gle::tangentpointer(stride, v, GL_SHORT); + lastxbuf = lastvbuf; + } + } + + void bindbones(void *wv, void *bv, int stride) + { + if(!enablebones) + { + gle::enableboneweight(); + gle::enableboneindex(); + enablebones = true; + } + if(lastbbuf!=lastvbuf) + { + gle::boneweightpointer(stride, wv); + gle::boneindexpointer(stride, bv); + lastbbuf = lastvbuf; + } + } + }; + + virtual meshgroup *loadmeshes(const char *name, va_list args) { return NULL; } + + meshgroup *sharemeshes(const char *name, ...) + { + static hashnameset meshgroups; + if(!meshgroups.access(name)) + { + va_list args; + va_start(args, name); + meshgroup *group = loadmeshes(name, args); + va_end(args); + if(!group) return NULL; + meshgroups.add(group); + } + return meshgroups[name]; + } + + struct linkedpart + { + part *p; + int tag, anim, basetime; + vec translate; + vec *pos; + matrix4 matrix; + + linkedpart() : p(NULL), tag(-1), anim(-1), basetime(0), translate(0, 0, 0), pos(NULL) {} + }; + + struct part + { + animmodel *model; + int index; + meshgroup *meshes; + vector links; + vector skins; + vector *anims[MAXANIMPARTS]; + int numanimparts; + float pitchscale, pitchoffset, pitchmin, pitchmax; + vec translate; + + part(animmodel *model, int index = 0) : model(model), index(index), meshes(NULL), numanimparts(1), pitchscale(1), pitchoffset(0), pitchmin(0), pitchmax(0), translate(0, 0, 0) + { + loopk(MAXANIMPARTS) anims[k] = NULL; + } + virtual ~part() + { + loopk(MAXANIMPARTS) DELETEA(anims[k]); + } + + virtual void cleanup() + { + if(meshes) meshes->cleanup(); + loopv(skins) skins[i].cleanup(); + } + + void calcbb(vec &bbmin, vec &bbmax, const matrix4x3 &m) + { + matrix4x3 t = m; + t.scale(model->scale); + t.translate(translate); + meshes->calcbb(bbmin, bbmax, t); + loopv(links) + { + matrix4x3 n; + meshes->concattagtransform(this, links[i].tag, m, n); + n.translate(links[i].translate, model->scale); + links[i].p->calcbb(bbmin, bbmax, n); + } + } + + void genBIH(vector &bih, const matrix4x3 &m) + { + matrix4x3 t = m; + t.scale(model->scale); + t.translate(translate); + meshes->genBIH(skins, bih, t); + loopv(links) + { + matrix4x3 n; + meshes->concattagtransform(this, links[i].tag, m, n); + n.translate(links[i].translate, model->scale); + links[i].p->genBIH(bih, n); + } + } + + bool link(part *p, const char *tag, const vec &translate = vec(0, 0, 0), int anim = -1, int basetime = 0, vec *pos = NULL) + { + int i = meshes ? meshes->findtag(tag) : -1; + if(i<0) + { + loopv(links) if(links[i].p && links[i].p->link(p, tag, translate, anim, basetime, pos)) return true; + return false; + } + linkedpart &l = links.add(); + l.p = p; + l.tag = i; + l.anim = anim; + l.basetime = basetime; + l.translate = translate; + l.pos = pos; + return true; + } + + bool unlink(part *p) + { + loopvrev(links) if(links[i].p==p) { links.remove(i, 1); return true; } + loopv(links) if(links[i].p && links[i].p->unlink(p)) return true; + return false; + } + + void initskins(Texture *tex = notexture, Texture *masks = notexture, int limit = 0) + { + if(!limit) + { + if(!meshes) return; + limit = meshes->meshes.length(); + } + while(skins.length() < limit) + { + skin &s = skins.add(); + s.owner = this; + s.tex = tex; + s.masks = masks; + } + } + + bool envmapped() + { + loopv(skins) if(skins[i].envmapped()) return true; + return false; + } + + bool tangents() + { + loopv(skins) if(skins[i].tangents()) return true; + return false; + } + + void preloadBIH() + { + loopv(skins) skins[i].preloadBIH(); + } + + void preloadshaders(bool force) + { + loopv(skins) skins[i].preloadshader(force); + } + + void preloadmeshes() + { + if(meshes) meshes->preload(this); + } + + virtual void getdefaultanim(animinfo &info, int anim, uint varseed, dynent *d) + { + info.frame = 0; + info.range = 1; + } + + bool calcanim(int animpart, int anim, int basetime, int basetime2, dynent *d, int interp, animinfo &info, int &aitime) + { + uint varseed = uint((size_t)d); + info.anim = anim; + info.basetime = basetime; + info.varseed = varseed; + info.speed = anim&ANIM_SETSPEED ? basetime2 : 100.0f; + if((anim&ANIM_INDEX)==ANIM_ALL) + { + info.frame = 0; + info.range = meshes->totalframes(); + } + else + { + animspec *spec = NULL; + if(anims[animpart]) + { + int primaryidx = anim&ANIM_INDEX; + if(primaryidx < NUMANIMS) + { + vector &primary = anims[animpart][primaryidx]; + if(primary.length()) spec = &primary[uint(varseed + basetime)%primary.length()]; + } + if((anim>>ANIM_SECONDARY)&(ANIM_INDEX|ANIM_DIR)) + { + int secondaryidx = (anim>>ANIM_SECONDARY)&ANIM_INDEX; + if(secondaryidx < NUMANIMS) + { + vector &secondary = anims[animpart][secondaryidx]; + if(secondary.length()) + { + animspec &spec2 = secondary[uint(varseed + basetime2)%secondary.length()]; + if(!spec || spec2.priority > spec->priority) + { + spec = &spec2; + info.anim >>= ANIM_SECONDARY; + info.basetime = basetime2; + } + } + } + } + } + if(spec) + { + info.frame = spec->frame; + info.range = spec->range; + if(spec->speed>0) info.speed = 1000.0f/spec->speed; + } + else getdefaultanim(info, anim, uint(varseed + info.basetime), d); + } + + info.anim &= (1<hasframes(info.frame, info.range)) + { + if(!meshes->hasframe(info.frame)) return false; + info.range = meshes->clipframes(info.frame, info.range); + } + + if(d && interp>=0) + { + animinterpinfo &ai = d->animinterp[interp]; + if((info.anim&ANIM_CLAMP)==ANIM_CLAMP) aitime = min(aitime, int(info.range*info.speed*0.5e-3f)); + void *ak = meshes->animkey(); + if(d->ragdoll && !(anim&ANIM_RAGDOLL)) + { + ai.prev.range = ai.cur.range = 0; + ai.lastswitch = -1; + } + else if(ai.lastmodel!=ak || ai.lastswitch<0 || lastmillis-d->lastrendered>aitime) + { + ai.prev = ai.cur = info; + ai.lastswitch = lastmillis-aitime*2; + } + else if(ai.cur!=info) + { + if(lastmillis-ai.lastswitch>aitime/2) ai.prev = ai.cur; + ai.cur = info; + ai.lastswitch = lastmillis; + } + else if(info.anim&ANIM_SETTIME) ai.cur.basetime = info.basetime; + ai.lastmodel = ak; + } + return true; + } + + void render(int anim, int basetime, int basetime2, float pitch, const vec &axis, const vec &forward, dynent *d) + { + animstate as[MAXANIMPARTS]; + render(anim, basetime, basetime2, pitch, axis, forward, d, as); + } + + void render(int anim, int basetime, int basetime2, float pitch, const vec &axis, const vec &forward, dynent *d, animstate *as) + { + if(!(anim&ANIM_REUSE)) loopi(numanimparts) + { + animinfo info; + int interp = d && index+numanimparts<=MAXANIMPARTS ? index+i : -1, aitime = animationinterpolationtime; + if(!calcanim(i, anim, basetime, basetime2, d, interp, info, aitime)) return; + animstate &p = as[i]; + p.owner = this; + p.cur.setframes(info); + p.interp = 1; + if(interp>=0 && d->animinterp[interp].prev.range>0) + { + int diff = lastmillis-d->animinterp[interp].lastswitch; + if(diffaniminterp[interp].prev); + p.interp = diff/float(aitime); + } + } + } + + vec oaxis, oforward; + matrixstack[matrixpos].transposedtransformnormal(axis, oaxis); + float pitchamount = pitchscale*pitch + pitchoffset; + if((pitchmin || pitchmax) && pitchmin <= pitchmax) pitchamount = clamp(pitchamount, pitchmin, pitchmax); + if(as->cur.anim&ANIM_NOPITCH || (as->interp < 1 && as->prev.anim&ANIM_NOPITCH)) + pitchamount *= (as->cur.anim&ANIM_NOPITCH ? 0 : as->interp) + (as->interp < 1 && as->prev.anim&ANIM_NOPITCH ? 0 : 1-as->interp); + if(pitchamount) + { + ++matrixpos; + matrixstack[matrixpos] = matrixstack[matrixpos-1]; + matrixstack[matrixpos].rotate(pitchamount*RAD, oaxis); + } + matrixstack[matrixpos].transposedtransformnormal(forward, oforward); + + if(!(anim&ANIM_NORENDER)) + { + matrix4 modelmatrix; + modelmatrix.mul(shadowmapping ? shadowmatrix : camprojmatrix, matrixstack[matrixpos]); + if(model->scale!=1) modelmatrix.scale(model->scale); + if(!translate.iszero()) modelmatrix.translate(translate); + GLOBALPARAM(modelmatrix, modelmatrix); + + if(!(anim&ANIM_NOSKIN)) + { + if(envmapped()) GLOBALPARAM(modelworld, matrix3(matrixstack[matrixpos])); + + vec odir, ocampos; + matrixstack[matrixpos].transposedtransformnormal(lightdir, odir); + GLOBALPARAM(lightdir, odir); + matrixstack[matrixpos].transposedtransform(camera1->o, ocampos); + ocampos.div(model->scale).sub(translate); + GLOBALPARAM(modelcamera, ocampos); + } + } + + meshes->render(as, pitch, oaxis, oforward, d, this); + + if(!(anim&ANIM_REUSE)) + { + loopv(links) + { + linkedpart &link = links[i]; + link.matrix.translate(links[i].translate, model->scale); + + matrixpos++; + matrixstack[matrixpos].mul(matrixstack[matrixpos-1], link.matrix); + + if(link.pos) *link.pos = matrixstack[matrixpos].gettranslation(); + + if(!link.p) + { + matrixpos--; + continue; + } + + int nanim = anim, nbasetime = basetime, nbasetime2 = basetime2; + if(link.anim>=0) + { + nanim = link.anim | (anim&ANIM_FLAGS); + nbasetime = link.basetime; + nbasetime2 = 0; + } + link.p->render(nanim, nbasetime, nbasetime2, pitch, axis, forward, d); + + matrixpos--; + } + } + + if(pitchamount) matrixpos--; + } + + void setanim(int animpart, int num, int frame, int range, float speed, int priority = 0) + { + if(animpart<0 || animpart>=MAXANIMPARTS) return; + if(frame<0 || range<=0 || !meshes || !meshes->hasframes(frame, range)) + { + conoutf(CON_ERROR, "invalid frame %d, range %d in model %s", frame, range, model->name); + return; + } + if(!anims[animpart]) anims[animpart] = new vector[NUMANIMS]; + animspec &spec = anims[animpart][num].add(); + spec.frame = frame; + spec.range = range; + spec.speed = speed; + spec.priority = priority; + } + + virtual void loaded() + { + meshes->shared++; + loopv(skins) skins[i].setkey(); + } + }; + + enum + { + LINK_TAG = 0, + LINK_COOP, + LINK_REUSE + }; + + virtual int linktype(animmodel *m) const { return LINK_TAG; } + + void render(int anim, int basetime, int basetime2, float pitch, const vec &axis, const vec &forward, dynent *d, modelattach *a) + { + int numtags = 0; + if(a) + { + int index = parts.last()->index + parts.last()->numanimparts; + for(int i = 0; a[i].tag; i++) + { + numtags++; + + animmodel *m = (animmodel *)a[i].m; + if(!m) + { + if(a[i].pos) link(NULL, a[i].tag, vec(0, 0, 0), 0, 0, a[i].pos); + continue; + } + part *p = m->parts[0]; + switch(linktype(m)) + { + case LINK_TAG: + p->index = link(p, a[i].tag, vec(0, 0, 0), a[i].anim, a[i].basetime, a[i].pos) ? index : -1; + break; + + case LINK_COOP: + p->index = index; + break; + + default: + continue; + } + index += p->numanimparts; + } + } + + animstate as[MAXANIMPARTS]; + parts[0]->render(anim, basetime, basetime2, pitch, axis, forward, d, as); + + if(a) for(int i = numtags-1; i >= 0; i--) + { + animmodel *m = (animmodel *)a[i].m; + if(!m) + { + if(a[i].pos) unlink(NULL); + continue; + } + part *p = m->parts[0]; + switch(linktype(m)) + { + case LINK_TAG: + if(p->index >= 0) unlink(p); + p->index = 0; + break; + + case LINK_COOP: + p->render(anim, basetime, basetime2, pitch, axis, forward, d); + p->index = 0; + break; + + case LINK_REUSE: + p->render(anim | ANIM_REUSE, basetime, basetime2, pitch, axis, forward, d, as); + break; + } + } + } + + void render(int anim, int basetime, int basetime2, const vec &o, float yaw, float pitch, dynent *d, modelattach *a, const vec &color, const vec &dir, float trans) + { + yaw += spinyaw*lastmillis/1000.0f; + pitch += offsetpitch + spinpitch*lastmillis/1000.0f; + + vec axis(0, -1, 0), forward(1, 0, 0); + + matrixpos = 0; + matrixstack[0].identity(); + if(!d || !d->ragdoll || anim&ANIM_RAGDOLL) + { + matrixstack[0].settranslation(o); + matrixstack[0].rotate_around_z(yaw*RAD); + matrixstack[0].transformnormal(vec(axis), axis); + matrixstack[0].transformnormal(vec(forward), forward); + if(offsetyaw) matrixstack[0].rotate_around_z(offsetyaw*RAD); + } + else pitch = 0; + + if(anim&ANIM_NORENDER) + { + render(anim, basetime, basetime2, pitch, axis, forward, d, a); + if(d) d->lastrendered = lastmillis; + return; + } + + if(!(anim&ANIM_NOSKIN)) + { + transparent = trans; + lightdir = dir; + lightcolor = color; + + if(envmapped()) + { + setupenvmap: + closestenvmaptex = lookupenvmap(closestenvmap(o)); + GLOBALPARAM(lightdirworld, dir); + } + else if(a) for(int i = 0; a[i].tag; i++) if(a[i].m && a[i].m->envmapped()) goto setupenvmap; + } + + if(depthoffset && !enabledepthoffset) + { + enablepolygonoffset(GL_POLYGON_OFFSET_FILL); + enabledepthoffset = true; + } + + if(transparent<1) + { + if(anim&ANIM_GHOST) + { + glDepthFunc(GL_GREATER); + glDepthMask(GL_FALSE); + } + else if(alphadepth) + { + glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + render(anim|ANIM_NOSKIN, basetime, basetime2, pitch, axis, forward, d, a); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, fading ? GL_FALSE : GL_TRUE); + + glDepthFunc(GL_LEQUAL); + } + + if(!enablealphablend) + { + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + enablealphablend = true; + } + } + + render(anim, basetime, basetime2, pitch, axis, forward, d, a); + + if(transparent<1 && (alphadepth || anim&ANIM_GHOST)) + { + glDepthFunc(GL_LESS); + if(anim&ANIM_GHOST) glDepthMask(GL_TRUE); + } + + if(d) d->lastrendered = lastmillis; + } + + vector parts; + + animmodel(const char *name) : model(name) + { + } + + ~animmodel() + { + parts.deletecontents(); + } + + void cleanup() + { + loopv(parts) parts[i]->cleanup(); + } + + virtual void flushpart() {} + + part &addpart() + { + flushpart(); + part *p = new part(this, parts.length()); + parts.add(p); + return *p; + } + + void initmatrix(matrix4x3 &m) + { + m.identity(); + if(offsetyaw) m.rotate_around_z(offsetyaw*RAD); + if(offsetpitch) m.rotate_around_y(-offsetpitch*RAD); + } + + void genBIH(vector &bih) + { + if(parts.empty()) return; + matrix4x3 m; + initmatrix(m); + parts[0]->genBIH(bih, m); + } + + void preloadBIH() + { + model::preloadBIH(); + if(bih) loopv(parts) parts[i]->preloadBIH(); + } + + BIH *setBIH() + { + if(bih) return bih; + vector meshes; + genBIH(meshes); + bih = new BIH(meshes); + return bih; + } + + bool link(part *p, const char *tag, const vec &translate = vec(0, 0, 0), int anim = -1, int basetime = 0, vec *pos = NULL) + { + if(parts.empty()) return false; + return parts[0]->link(p, tag, translate, anim, basetime, pos); + } + + bool unlink(part *p) + { + if(parts.empty()) return false; + return parts[0]->unlink(p); + } + + bool envmapped() + { + loopv(parts) if(parts[i]->envmapped()) return true; + return false; + } + + virtual bool flipy() const { return false; } + virtual bool loadconfig() { return false; } + virtual bool loaddefaultparts() { return false; } + virtual void startload() {} + virtual void endload() {} + + bool load() + { + startload(); + bool success = loadconfig() && parts.length(); // configured model, will call the model commands below + if(!success) + success = loaddefaultparts(); // model without configuration, try default tris and skin + flushpart(); + endload(); + if(flipy()) translate.y = -translate.y; + + if(!success) return false; + loopv(parts) if(!parts[i]->meshes) return false; + + loaded(); + return true; + } + + void preloadshaders(bool force) + { + loopv(parts) parts[i]->preloadshaders(force); + } + + void preloadmeshes() + { + loopv(parts) parts[i]->preloadmeshes(); + } + + void setshader(Shader *shader) + { + if(parts.empty()) loaddefaultparts(); + loopv(parts) loopvj(parts[i]->skins) parts[i]->skins[j].shader = shader; + } + + void setenvmap(float envmapmin, float envmapmax, Texture *envmap) + { + if(parts.empty()) loaddefaultparts(); + loopv(parts) loopvj(parts[i]->skins) + { + skin &s = parts[i]->skins[j]; + if(envmapmax) + { + s.envmapmin = envmapmin; + s.envmapmax = envmapmax; + } + if(envmap) s.envmap = envmap; + } + } + + void setspec(float spec) + { + if(parts.empty()) loaddefaultparts(); + loopv(parts) loopvj(parts[i]->skins) parts[i]->skins[j].spec = spec; + } + + void setambient(float ambient) + { + if(parts.empty()) loaddefaultparts(); + loopv(parts) loopvj(parts[i]->skins) parts[i]->skins[j].ambient = ambient; + } + + void setglow(float glow, float delta, float pulse) + { + if(parts.empty()) loaddefaultparts(); + loopv(parts) loopvj(parts[i]->skins) + { + skin &s = parts[i]->skins[j]; + s.glow = glow; + s.glowdelta = delta; + s.glowpulse = pulse; + } + } + + void setglare(float specglare, float glowglare) + { + if(parts.empty()) loaddefaultparts(); + loopv(parts) loopvj(parts[i]->skins) + { + skin &s = parts[i]->skins[j]; + s.specglare = specglare; + s.glowglare = glowglare; + } + } + + void setalphatest(float alphatest) + { + if(parts.empty()) loaddefaultparts(); + loopv(parts) loopvj(parts[i]->skins) parts[i]->skins[j].alphatest = alphatest; + } + + void setalphablend(bool alphablend) + { + if(parts.empty()) loaddefaultparts(); + loopv(parts) loopvj(parts[i]->skins) parts[i]->skins[j].alphablend = alphablend; + } + + void setfullbright(float fullbright) + { + if(parts.empty()) loaddefaultparts(); + loopv(parts) loopvj(parts[i]->skins) parts[i]->skins[j].fullbright = fullbright; + } + + void setcullface(bool cullface) + { + if(parts.empty()) loaddefaultparts(); + loopv(parts) loopvj(parts[i]->skins) parts[i]->skins[j].cullface = cullface; + } + + void calcbb(vec ¢er, vec &radius) + { + if(parts.empty()) return; + vec bbmin(1e16f, 1e16f, 1e16f), bbmax(-1e16f, -1e16f, -1e16f); + matrix4x3 m; + initmatrix(m); + parts[0]->calcbb(bbmin, bbmax, m); + radius = bbmax; + radius.sub(bbmin); + radius.mul(0.5f); + center = bbmin; + center.add(radius); + } + + virtual void loaded() + { + scale /= 4; + if(parts.length()) parts[0]->translate = translate; + loopv(parts) parts[i]->loaded(); + } + + static bool enabletc, enablealphablend, enablecullface, enablenormals, enabletangents, enablebones, enabledepthoffset; + static vec lightdir, lightcolor; + static float transparent, lastalphatest; + static GLuint lastvbuf, lasttcbuf, lastnbuf, lastxbuf, lastbbuf, lastebuf, lastenvmaptex, closestenvmaptex; + static Texture *lasttex, *lastmasks, *lastnormalmap; + static int matrixpos; + static matrix4 matrixstack[64]; + + void startrender() + { + enabletc = enablealphablend = enablenormals = enabletangents = enablebones = enabledepthoffset = false; + enablecullface = true; + lastalphatest = -1; + lastvbuf = lasttcbuf = lastxbuf = lastnbuf = lastbbuf = lastebuf = lastenvmaptex = closestenvmaptex = 0; + lasttex = lastmasks = lastnormalmap = NULL; + transparent = 1; + shaderparamskey::invalidate(); + } + + static void disablebones() + { + gle::disableboneweight(); + gle::disableboneindex(); + enablebones = false; + } + + static void disabletangents() + { + gle::disabletangent(); + enabletangents = false; + } + + static void disabletc() + { + gle::disabletexcoord0(); + enabletc = false; + } + + static void disablenormals() + { + gle::disablenormal(); + enablenormals = false; + } + + static void disablevbo() + { + if(lastebuf) gle::clearebo(); + if(lastvbuf) + { + gle::clearvbo(); + gle::disablevertex(); + } + if(enabletc) disabletc(); + if(enablenormals) disablenormals(); + if(enabletangents) disabletangents(); + if(enablebones) disablebones(); + lastvbuf = lasttcbuf = lastxbuf = lastnbuf = lastbbuf = lastebuf = 0; + } + + void endrender() + { + if(lastvbuf || lastebuf) disablevbo(); + if(enablealphablend) glDisable(GL_BLEND); + if(!enablecullface) glEnable(GL_CULL_FACE); + if(enabledepthoffset) disablepolygonoffset(GL_POLYGON_OFFSET_FILL); + } +}; + +bool animmodel::enabletc = false, animmodel::enablealphablend = false, + animmodel::enablecullface = true, + animmodel::enablenormals = false, animmodel::enabletangents = false, animmodel::enablebones = false, animmodel::enabledepthoffset = false; +vec animmodel::lightdir(0, 0, 1), animmodel::lightcolor(1, 1, 1); +float animmodel::transparent = 1, animmodel::lastalphatest = -1; +GLuint animmodel::lastvbuf = 0, animmodel::lasttcbuf = 0, animmodel::lastnbuf = 0, animmodel::lastxbuf = 0, animmodel::lastbbuf = 0, + animmodel::lastebuf = 0, animmodel::lastenvmaptex = 0, animmodel::closestenvmaptex = 0; +Texture *animmodel::lasttex = NULL, *animmodel::lastmasks = NULL, *animmodel::lastnormalmap = NULL; +int animmodel::matrixpos = 0; +matrix4 animmodel::matrixstack[64]; + +static inline uint hthash(const animmodel::shaderparams &k) +{ + return memhash(&k, sizeof(k)); +} + +static inline bool htcmp(const animmodel::shaderparams &x, const animmodel::shaderparams &y) +{ + return !memcmp(&x, &y, sizeof(animmodel::shaderparams)); +} + +hashtable animmodel::shaderparamskey::keys; +int animmodel::shaderparamskey::firstversion = 0, animmodel::shaderparamskey::lastversion = 1; + +template struct modelloader : BASE +{ + static MDL *loading; + static string dir; + + modelloader(const char *name) : BASE(name) {} + + static bool animated() { return true; } + static bool multiparted() { return true; } + static bool multimeshed() { return true; } + + void startload() + { + loading = (MDL *)this; + } + + void endload() + { + loading = NULL; + } + + bool loadconfig() + { + formatstring(dir, "packages/models/%s", BASE::name); + defformatstring(cfgname, "packages/models/%s/%s.cfg", BASE::name, MDL::formatname()); + + identflags &= ~IDF_PERSIST; + bool success = execfile(cfgname, false); + identflags |= IDF_PERSIST; + return success; + } +}; + +template MDL *modelloader::loading = NULL; +template string modelloader::dir = {'\0'}; // crashes clang if "" is used here + +template struct modelcommands +{ + typedef struct MDL::part part; + typedef struct MDL::skin skin; + + static void setdir(char *name) + { + if(!MDL::loading) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; } + formatstring(MDL::dir, "packages/models/%s", name); + } + + #define loopmeshes(meshname, m, body) \ + if(!MDL::loading || MDL::loading->parts.empty()) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; } \ + part &mdl = *MDL::loading->parts.last(); \ + if(!mdl.meshes) return; \ + loopv(mdl.meshes->meshes) \ + { \ + MESH &m = *(MESH *)mdl.meshes->meshes[i]; \ + if(!strcmp(meshname, "*") || (m.name && !strcmp(m.name, meshname))) \ + { \ + body; \ + } \ + } + + #define loopskins(meshname, s, body) loopmeshes(meshname, m, { skin &s = mdl.skins[i]; body; }) + + static void setskin(char *meshname, char *tex, char *masks, float *envmapmax, float *envmapmin) + { + loopskins(meshname, s, + s.tex = textureload(makerelpath(MDL::dir, tex), 0, true, false); + if(*masks) + { + s.masks = textureload(makerelpath(MDL::dir, masks), 0, true, false); + s.envmapmax = *envmapmax; + s.envmapmin = *envmapmin; + } + ); + } + + static void setspec(char *meshname, int *percent) + { + float spec = 1.0f; + if(*percent>0) spec = *percent/100.0f; + else if(*percent<0) spec = 0.0f; + loopskins(meshname, s, s.spec = spec); + } + + static void setambient(char *meshname, int *percent) + { + float ambient = 0.3f; + if(*percent>0) ambient = *percent/100.0f; + else if(*percent<0) ambient = 0.0f; + loopskins(meshname, s, s.ambient = ambient); + } + + static void setglow(char *meshname, int *percent, int *delta, float *pulse) + { + float glow = 3.0f, glowdelta = *delta/100.0f, glowpulse = *pulse > 0 ? *pulse/1000.0f : 0; + if(*percent>0) glow = *percent/100.0f; + else if(*percent<0) glow = 0.0f; + glowdelta -= glow; + loopskins(meshname, s, { s.glow = glow; s.glowdelta = glowdelta; s.glowpulse = glowpulse; }); + } + + static void setglare(char *meshname, float *specglare, float *glowglare) + { + loopskins(meshname, s, { s.specglare = *specglare; s.glowglare = *glowglare; }); + } + + static void setalphatest(char *meshname, float *cutoff) + { + loopskins(meshname, s, s.alphatest = max(0.0f, min(1.0f, *cutoff))); + } + + static void setalphablend(char *meshname, int *blend) + { + loopskins(meshname, s, s.alphablend = *blend!=0); + } + + static void setcullface(char *meshname, int *cullface) + { + loopskins(meshname, s, s.cullface = *cullface!=0); + } + + static void setenvmap(char *meshname, char *envmap) + { + Texture *tex = cubemapload(envmap); + loopskins(meshname, s, s.envmap = tex); + } + + static void setbumpmap(char *meshname, char *normalmapfile) + { + Texture *normalmaptex = textureload(makerelpath(MDL::dir, normalmapfile), 0, true, false); + loopskins(meshname, s, s.normalmap = normalmaptex); + } + + static void setfullbright(char *meshname, float *fullbright) + { + loopskins(meshname, s, s.fullbright = *fullbright); + } + + static void setshader(char *meshname, char *shader) + { + loopskins(meshname, s, s.shader = lookupshaderbyname(shader)); + } + + static void setscroll(char *meshname, float *scrollu, float *scrollv) + { + loopskins(meshname, s, { s.scrollu = *scrollu; s.scrollv = *scrollv; }); + } + + static void setnoclip(char *meshname, int *noclip) + { + loopmeshes(meshname, m, m.noclip = *noclip!=0); + } + + static void setlink(int *parent, int *child, char *tagname, float *x, float *y, float *z) + { + if(!MDL::loading) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; } + if(!MDL::loading->parts.inrange(*parent) || !MDL::loading->parts.inrange(*child)) { conoutf(CON_ERROR, "no models loaded to link"); return; } + if(!MDL::loading->parts[*parent]->link(MDL::loading->parts[*child], tagname, vec(*x, *y, *z))) conoutf(CON_ERROR, "could not link model %s", MDL::loading->name); + } + + template void modelcommand(F *fun, const char *suffix, const char *args) + { + defformatstring(name, "%s%s", MDL::formatname(), suffix); + addcommand(newstring(name), (void (*)())fun, args); + } + + modelcommands() + { + modelcommand(setdir, "dir", "s"); + if(MDL::multimeshed()) + { + modelcommand(setskin, "skin", "sssff"); + modelcommand(setspec, "spec", "si"); + modelcommand(setambient, "ambient", "si"); + modelcommand(setglow, "glow", "siif"); + modelcommand(setglare, "glare", "sff"); + modelcommand(setalphatest, "alphatest", "sf"); + modelcommand(setalphablend, "alphablend", "si"); + modelcommand(setcullface, "cullface", "si"); + modelcommand(setenvmap, "envmap", "ss"); + modelcommand(setbumpmap, "bumpmap", "ss"); + modelcommand(setfullbright, "fullbright", "sf"); + modelcommand(setshader, "shader", "ss"); + modelcommand(setscroll, "scroll", "sff"); + modelcommand(setnoclip, "noclip", "si"); + } + if(MDL::multiparted()) modelcommand(setlink, "link", "iisfff"); + } +}; + diff --git a/src/engine/bih.cpp b/src/engine/bih.cpp new file mode 100644 index 0000000..e735f38 --- /dev/null +++ b/src/engine/bih.cpp @@ -0,0 +1,330 @@ +#include "engine.h" + +bool BIH::triintersect(const mesh &m, int tidx, const vec &mo, const vec &mray, float maxdist, float &dist, int mode) +{ + const tri &t = m.tris[tidx]; + vec a = m.getpos(t.vert[0]), b = m.getpos(t.vert[1]).sub(a), c = m.getpos(t.vert[2]).sub(a), + n = vec().cross(b, c), r = vec(a).sub(mo), e = vec().cross(r, mray); + float det = mray.dot(n), v, w, f; + if(det >= 0) + { + if(!(mode&RAY_SHADOW) && m.flags&MESH_CULLFACE) return false; + v = e.dot(c); + if(v < 0 || v > det) return false; + w = -e.dot(b); + if(w < 0 || v + w > det) return false; + f = r.dot(n)*m.scale; + if(f < 0 || f > maxdist*det || !det) return false; + } + else + { + v = e.dot(c); + if(v > 0 || v < det) return false; + w = -e.dot(b); + if(w > 0 || v + w < det) return false; + f = r.dot(n)*m.scale; + if(f > 0 || f < maxdist*det) return false; + } + float invdet = 1/det; + if(m.flags&MESH_ALPHA && (mode&RAY_ALPHAPOLY)==RAY_ALPHAPOLY && (m.tex->alphamask || (lightmapping <= 1 && loadalphamask(m.tex)))) + { + vec2 at = m.gettc(t.vert[0]), bt = m.gettc(t.vert[1]).sub(at).mul(v*invdet), ct = m.gettc(t.vert[2]).sub(at).mul(w*invdet); + at.add(bt).add(ct); + int si = clamp(int(m.tex->xs * at.x), 0, m.tex->xs-1), + ti = clamp(int(m.tex->ys * at.y), 0, m.tex->ys-1); + if(!(m.tex->alphamask[ti*((m.tex->xs+7)/8) + si/8] & (1<<(si%8)))) return false; + } + dist = f*invdet; + return true; +} + +struct traversestate +{ + BIH::node *node; + float tmin, tmax; +}; + +inline bool BIH::traverse(const mesh &m, const vec &o, const vec &ray, const vec &invray, float maxdist, float &dist, int mode, node *curnode, float tmin, float tmax) +{ + traversestate stack[128]; + int stacksize = 0; + ivec order(ray.x>0 ? 0 : 1, ray.y>0 ? 0 : 1, ray.z>0 ? 0 : 1); + vec mo = m.invxform.transform(o), mray = m.invxformnorm.transform(ray); + for(;;) + { + int axis = curnode->axis(); + int nearidx = order[axis], faridx = nearidx^1; + float nearsplit = (curnode->split[nearidx] - o[axis])*invray[axis], + farsplit = (curnode->split[faridx] - o[axis])*invray[axis]; + + if(nearsplit <= tmin) + { + if(farsplit < tmax) + { + if(!curnode->isleaf(faridx)) + { + curnode += curnode->childindex(faridx); + tmin = max(tmin, farsplit); + continue; + } + else if(triintersect(m, curnode->childindex(faridx), mo, mray, maxdist, dist, mode)) return true; + } + } + else if(curnode->isleaf(nearidx)) + { + if(triintersect(m, curnode->childindex(nearidx), mo, mray, maxdist, dist, mode)) return true; + if(farsplit < tmax) + { + if(!curnode->isleaf(faridx)) + { + curnode += curnode->childindex(faridx); + tmin = max(tmin, farsplit); + continue; + } + else if(triintersect(m, curnode->childindex(faridx), mo, mray, maxdist, dist, mode)) return true; + } + } + else + { + if(farsplit < tmax) + { + if(!curnode->isleaf(faridx)) + { + if(stacksize < int(sizeof(stack)/sizeof(stack[0]))) + { + traversestate &save = stack[stacksize++]; + save.node = curnode + curnode->childindex(faridx); + save.tmin = max(tmin, farsplit); + save.tmax = tmax; + } + else + { + if(traverse(m, o, ray, invray, maxdist, dist, mode, curnode + curnode->childindex(nearidx), tmin, min(tmax, nearsplit))) return true; + curnode += curnode->childindex(faridx); + tmin = max(tmin, farsplit); + continue; + } + } + else if(triintersect(m, curnode->childindex(faridx), mo, mray, maxdist, dist, mode)) return true; + } + curnode += curnode->childindex(nearidx); + tmax = min(tmax, nearsplit); + continue; + } + if(stacksize <= 0) return false; + traversestate &restore = stack[--stacksize]; + curnode = restore.node; + tmin = restore.tmin; + tmax = restore.tmax; + } +} + +inline bool BIH::traverse(const vec &o, const vec &ray, float maxdist, float &dist, int mode) +{ + vec invray(ray.x ? 1/ray.x : 1e16f, ray.y ? 1/ray.y : 1e16f, ray.z ? 1/ray.z : 1e16f); + loopi(nummeshes) + { + mesh &m = meshes[i]; + if(!(mode&RAY_SHADOW) && m.flags&MESH_NOCLIP) continue; + float t1 = (m.bbmin.x - o.x)*invray.x, + t2 = (m.bbmax.x - o.x)*invray.x, + tmin, tmax; + if(invray.x > 0) { tmin = t1; tmax = t2; } else { tmin = t2; tmax = t1; } + t1 = (m.bbmin.y - o.y)*invray.y; + t2 = (m.bbmax.y - o.y)*invray.y; + if(invray.y > 0) { tmin = max(tmin, t1); tmax = min(tmax, t2); } else { tmin = max(tmin, t2); tmax = min(tmax, t1); } + t1 = (m.bbmin.z - o.z)*invray.z; + t2 = (m.bbmax.z - o.z)*invray.z; + if(invray.z > 0) { tmin = max(tmin, t1); tmax = min(tmax, t2); } else { tmin = max(tmin, t2); tmax = min(tmax, t1); } + tmax = min(tmax, maxdist); + if(tmin < tmax && traverse(m, o, ray, invray, maxdist, dist, mode, m.nodes, tmin, tmax)) return true; + } + return false; +} + +void BIH::build(mesh &m, ushort *indices, int numindices, const ivec &vmin, const ivec &vmax) +{ + int axis = 2; + loopk(2) if(vmax[k] - vmin[k] > vmax[axis] - vmin[axis]) axis = k; + + ivec leftmin, leftmax, rightmin, rightmax; + int splitleft, splitright; + int left, right; + loopk(3) + { + leftmin = rightmin = ivec(INT_MAX, INT_MAX, INT_MAX); + leftmax = rightmax = ivec(INT_MIN, INT_MIN, INT_MIN); + int split = (vmax[axis] + vmin[axis])/2; + for(left = 0, right = numindices, splitleft = SHRT_MIN, splitright = SHRT_MAX; left < right;) + { + const tribb &tri = m.tribbs[indices[left]]; + ivec trimin = ivec(tri.center).sub(ivec(tri.radius)), + trimax = ivec(tri.center).add(ivec(tri.radius)); + int amin = trimin[axis], amax = trimax[axis]; + if(max(split - amin, 0) > max(amax - split, 0)) + { + ++left; + splitleft = max(splitleft, amax); + leftmin.min(trimin); + leftmax.max(trimax); + } + else + { + --right; + swap(indices[left], indices[right]); + splitright = min(splitright, amin); + rightmin.min(trimin); + rightmax.max(trimax); + } + } + if(left > 0 && right < numindices) break; + axis = (axis+1)%3; + } + + if(!left || right==numindices) + { + leftmin = rightmin = ivec(INT_MAX, INT_MAX, INT_MAX); + leftmax = rightmax = ivec(INT_MIN, INT_MIN, INT_MIN); + left = right = numindices/2; + splitleft = SHRT_MIN; + splitright = SHRT_MAX; + loopi(numindices) + { + const tribb &tri = m.tribbs[indices[i]]; + ivec trimin = ivec(tri.center).sub(ivec(tri.radius)), + trimax = ivec(tri.center).add(ivec(tri.radius)); + if(i < left) + { + splitleft = max(splitleft, trimax[axis]); + leftmin.min(trimin); + leftmax.max(trimax); + } + else + { + splitright = min(splitright, trimin[axis]); + rightmin.min(trimin); + rightmax.max(trimax); + } + } + } + + int offset = m.numnodes++; + node &curnode = m.nodes[offset]; + curnode.split[0] = short(splitleft); + curnode.split[1] = short(splitright); + + if(left==1) curnode.child[0] = (axis<<14) | indices[0]; + else + { + curnode.child[0] = (axis<<14) | (m.numnodes - offset); + build(m, indices, left, leftmin, leftmax); + } + + if(numindices-right==1) curnode.child[1] = (1<<15) | (left==1 ? 1<<14 : 0) | indices[right]; + else + { + curnode.child[1] = (left==1 ? 1<<14 : 0) | (m.numnodes - offset); + build(m, &indices[right], numindices-right, rightmin, rightmax); + } +} + +BIH::BIH(vector &buildmeshes) + : meshes(NULL), nummeshes(0), nodes(NULL), numnodes(0), tribbs(NULL), numtris(0), bbmin(1e16f, 1e16f, 1e16f), bbmax(-1e16f, -1e16f, -1e16f), center(0, 0, 0), radius(0), entradius(0) +{ + if(buildmeshes.empty()) return; + loopv(buildmeshes) numtris += buildmeshes[i].numtris; + if(!numtris) return; + + nummeshes = buildmeshes.length(); + meshes = new mesh[nummeshes]; + memcpy(meshes, buildmeshes.getbuf(), sizeof(mesh)*buildmeshes.length()); + tribbs = new tribb[numtris]; + tribb *dsttri = tribbs; + loopi(nummeshes) + { + mesh &m = meshes[i]; + m.scale = m.xform.a.magnitude(); + m.invscale = 1/m.scale; + m.xformnorm = matrix3(m.xform); + m.xformnorm.normalize(); + m.invxform.invert(m.xform); + m.invxformnorm = matrix3(m.invxform); + m.invxformnorm.normalize(); + m.tribbs = dsttri; + const tri *srctri = m.tris; + vec mmin(1e16f, 1e16f, 1e16f), mmax(-1e16f, -1e16f, -1e16f); + loopj(m.numtris) + { + vec s0 = m.getpos(srctri->vert[0]), s1 = m.getpos(srctri->vert[1]), s2 = m.getpos(srctri->vert[2]), + v0 = m.xform.transform(s0), v1 = m.xform.transform(s1), v2 = m.xform.transform(s2), + vmin = vec(v0).min(v1).min(v2), + vmax = vec(v0).max(v1).max(v2); + mmin.min(vmin); + mmax.max(vmax); + ivec imin = ivec::floor(vmin), imax = ivec::ceil(vmax); + dsttri->center = svec(ivec(imin).add(imax).div(2)); + dsttri->radius = svec(ivec(imax).sub(imin).add(1).div(2)); + ++srctri; + ++dsttri; + } + loopk(3) if(fabs(mmax[k] - mmin[k]) < 0.125f) + { + float mid = (mmin[k] + mmax[k]) / 2; + mmin[k] = mid - 0.0625f; + mmax[k] = mid + 0.0625f; + } + m.bbmin = mmin; + m.bbmax = mmax; + bbmin.min(mmin); + bbmax.max(mmax); + } + + center = vec(bbmin).add(bbmax).mul(0.5f); + radius = vec(bbmax).sub(bbmin).mul(0.5f).magnitude(); + entradius = max(bbmin.squaredlen(), bbmax.squaredlen()); + + nodes = new node[numtris]; + node *curnode = nodes; + ushort *indices = new ushort[numtris]; + loopi(nummeshes) + { + mesh &m = meshes[i]; + m.nodes = curnode; + loopj(m.numtris) indices[j] = j; + build(m, indices, m.numtris, ivec::floor(m.bbmin), ivec::ceil(m.bbmax)); + curnode += m.numnodes; + } + delete[] indices; + numnodes = int(curnode - nodes); +} + +BIH::~BIH() +{ + delete[] meshes; + delete[] nodes; + delete[] tribbs; +} + +bool mmintersect(const extentity &e, const vec &o, const vec &ray, float maxdist, int mode, float &dist) +{ + model *m = loadmapmodel(e.attr2); + if(!m) return false; + if(mode&RAY_SHADOW) + { + if(!m->shadow || e.flags&EF_NOSHADOW) return false; + } + else if((mode&RAY_ENTS)!=RAY_ENTS && (!m->collide || e.flags&EF_NOCOLLIDE)) return false; + if(!m->bih && (lightmapping > 1 || !m->setBIH())) return false; + vec mo = vec(o).sub(e.o), mray(ray); + float v = mo.dot(mray), inside = m->bih->entradius - mo.squaredlen(); + if((inside < 0 && v > 0) || inside + v*v < 0) return false; + int yaw = e.attr1; + if(yaw != 0) + { + const vec2 &rot = sincosmod360(-yaw); + mo.rotate_around_z(rot); + mray.rotate_around_z(rot); + } + return m->bih->traverse(mo, mray, maxdist ? maxdist : 1e16f, dist, mode); +} + diff --git a/src/engine/bih.h b/src/engine/bih.h new file mode 100644 index 0000000..0f9482f --- /dev/null +++ b/src/engine/bih.h @@ -0,0 +1,79 @@ +struct BIH +{ + struct node + { + short split[2]; + ushort child[2]; + + int axis() const { return child[0]>>14; } + int childindex(int which) const { return child[which]&0x3FFF; } + bool isleaf(int which) const { return (child[1]&(1<<(14+which)))!=0; } + }; + + struct tri + { + ushort vert[3]; + }; + + struct tribb + { + svec center, radius; + + bool outside(const ivec &bo, const ivec &br) const + { + return abs(bo.x - center.x) > br.x + radius.x || + abs(bo.y - center.y) > br.y + radius.y || + abs(bo.z - center.z) > br.z + radius.z; + } + }; + + enum { MESH_NOCLIP = 1<<0, MESH_ALPHA = 1<<1, MESH_CULLFACE = 1<<2 }; + + struct mesh + { + enum { MAXTRIS = 1<<14 }; + + matrix4x3 xform, invxform; + matrix3 xformnorm, invxformnorm; + float scale, invscale; + node *nodes; + int numnodes; + const tri *tris; + const tribb *tribbs; + int numtris; + const uchar *pos, *tc; + int posstride, tcstride; + Texture *tex; + int flags; + vec bbmin, bbmax; + + mesh() : numnodes(0), numtris(0), tex(NULL), flags(0) {} + + vec getpos(int i) const { return *(const vec *)(pos + i*posstride); } + vec2 gettc(int i) const { return *(const vec2 *)(tc + i*tcstride); } + }; + + mesh *meshes; + int nummeshes; + node *nodes; + int numnodes; + tribb *tribbs; + int numtris; + vec bbmin, bbmax, center; + float radius, entradius; + + BIH(vector &buildmeshes); + + ~BIH(); + + void build(mesh &m, ushort *indices, int numindices, const ivec &vmin, const ivec &vmax); + + bool traverse(const vec &o, const vec &ray, float maxdist, float &dist, int mode); + bool traverse(const mesh &m, const vec &o, const vec &ray, const vec &invray, float maxdist, float &dist, int mode, node *curnode, float tmin, float tmax); + bool triintersect(const mesh &m, int tidx, const vec &mo, const vec &mray, float maxdist, float &dist, int mode); + + void preload(); +}; + +extern bool mmintersect(const extentity &e, const vec &o, const vec &ray, float maxdist, int mode, float &dist); + diff --git a/src/engine/blend.cpp b/src/engine/blend.cpp new file mode 100644 index 0000000..16f21c2 --- /dev/null +++ b/src/engine/blend.cpp @@ -0,0 +1,862 @@ +#include "engine.h" + +enum +{ + BM_BRANCH = 0, + BM_SOLID, + BM_IMAGE +}; + +struct BlendMapBranch; +struct BlendMapSolid; +struct BlendMapImage; + +struct BlendMapNode +{ + union + { + BlendMapBranch *branch; + BlendMapSolid *solid; + BlendMapImage *image; + }; + + void cleanup(int type); + void splitsolid(uchar &type, uchar val); +}; + +struct BlendMapBranch +{ + uchar type[4]; + BlendMapNode children[4]; + + ~BlendMapBranch() + { + loopi(4) children[i].cleanup(type[i]); + } + + uchar shrink(BlendMapNode &child, int quadrant); +}; + +struct BlendMapSolid +{ + uchar val; + + BlendMapSolid(uchar val) : val(val) {} +}; + +#define BM_SCALE 1 +#define BM_IMAGE_SIZE 64 + +struct BlendMapImage +{ + uchar data[BM_IMAGE_SIZE*BM_IMAGE_SIZE]; +}; + +void BlendMapNode::cleanup(int type) +{ + switch(type) + { + case BM_BRANCH: delete branch; break; + case BM_IMAGE: delete image; break; + } +} + +#define DEFBMSOLIDS(n) n, n+1, n+2, n+3, n+4, n+5, n+6, n+7, n+8, n+9, n+10, n+11, n+12, n+13, n+14, n+15 + +static BlendMapSolid bmsolids[256] = +{ + DEFBMSOLIDS(0x00), DEFBMSOLIDS(0x10), DEFBMSOLIDS(0x20), DEFBMSOLIDS(0x30), + DEFBMSOLIDS(0x40), DEFBMSOLIDS(0x50), DEFBMSOLIDS(0x60), DEFBMSOLIDS(0x70), + DEFBMSOLIDS(0x80), DEFBMSOLIDS(0x90), DEFBMSOLIDS(0xA0), DEFBMSOLIDS(0xB0), + DEFBMSOLIDS(0xC0), DEFBMSOLIDS(0xD0), DEFBMSOLIDS(0xE0), DEFBMSOLIDS(0xF0), +}; + +void BlendMapNode::splitsolid(uchar &type, uchar val) +{ + cleanup(type); + type = BM_BRANCH; + branch = new BlendMapBranch; + loopi(4) + { + branch->type[i] = BM_SOLID; + branch->children[i].solid = &bmsolids[val]; + } +} + +uchar BlendMapBranch::shrink(BlendMapNode &child, int quadrant) +{ + uchar childtype = type[quadrant]; + child = children[quadrant]; + type[quadrant] = BM_SOLID; + children[quadrant].solid = &bmsolids[0]; + return childtype; +} + +struct BlendMapRoot : BlendMapNode +{ + uchar type; + + BlendMapRoot() : type(BM_SOLID) { solid = &bmsolids[0xFF]; } + BlendMapRoot(uchar type, const BlendMapNode &node) : BlendMapNode(node), type(type) {} + + void cleanup() { BlendMapNode::cleanup(type); } + + void shrink(int quadrant) + { + if(type == BM_BRANCH) + { + BlendMapRoot oldroot = *this; + type = branch->shrink(*this, quadrant); + oldroot.cleanup(); + } + } +}; + +static BlendMapRoot blendmap; + +struct BlendMapCache +{ + BlendMapRoot node; + int scale; + ivec2 origin; +}; + +BlendMapCache *newblendmapcache() { return new BlendMapCache; } + +void freeblendmapcache(BlendMapCache *&cache) { delete cache; cache = NULL; } + +bool setblendmaporigin(BlendMapCache *cache, const ivec &o, int size) +{ + if(blendmap.type!=BM_BRANCH) + { + cache->node = blendmap; + cache->scale = worldscale-BM_SCALE; + cache->origin = ivec2(0, 0); + return cache->node.solid!=&bmsolids[0xFF]; + } + + BlendMapBranch *bm = blendmap.branch; + int bmscale = worldscale-BM_SCALE, bmsize = 1<>BM_SCALE, y = o.y>>BM_SCALE, + x1 = max(x-1, 0), y1 = max(y-1, 0), + x2 = min(((o.x + size + (1<>BM_SCALE) + 1, bmsize), + y2 = min(((o.y + size + (1<>BM_SCALE) + 1, bmsize), + diff = (x1^x2)|(y1^y2); + if(diff < bmsize) while(!(diff&(1<<(bmscale-1)))) + { + bmscale--; + int n = (((y1>>bmscale)&1)<<1) | ((x1>>bmscale)&1); + if(bm->type[n]!=BM_BRANCH) + { + cache->node = BlendMapRoot(bm->type[n], bm->children[n]); + cache->scale = bmscale; + cache->origin = ivec2(x1&(~0U<node.solid!=&bmsolids[0xFF]; + } + bm = bm->children[n].branch; + } + + cache->node.type = BM_BRANCH; + cache->node.branch = bm; + cache->scale = bmscale; + cache->origin = ivec2(x1&(~0U<node.solid!=&bmsolids[0xFF]; +} + +static uchar lookupblendmap(int x, int y, BlendMapBranch *bm, int bmscale) +{ + for(;;) + { + bmscale--; + int n = (((y>>bmscale)&1)<<1) | ((x>>bmscale)&1); + switch(bm->type[n]) + { + case BM_SOLID: return bm->children[n].solid->val; + case BM_IMAGE: return bm->children[n].image->data[(y&((1<children[n].branch; + } +} + +uchar lookupblendmap(BlendMapCache *cache, const vec &pos) +{ + if(cache->node.type==BM_SOLID) return cache->node.solid->val; + + uchar vals[4], *val = vals; + float bx = pos.x/(1<origin.x, ry = iy-cache->origin.y; + loop(vy, 2) loop(vx, 2) + { + int cx = clamp(rx+vx, 0, (1<scale)-1), cy = clamp(ry+vy, 0, (1<scale)-1); + if(cache->node.type==BM_IMAGE) + *val++ = cache->node.image->data[cy*BM_IMAGE_SIZE + cx]; + else *val++ = lookupblendmap(cx, cy, cache->node.branch, cache->scale); + } + float fx = bx - ix, fy = by - iy; + return uchar((1-fy)*((1-fx)*vals[0] + fx*vals[1]) + + fy*((1-fx)*vals[2] + fx*vals[3])); +} + +static void fillblendmap(uchar &type, BlendMapNode &node, int size, uchar val, int x1, int y1, int x2, int y2) +{ + if(max(x1, y1) <= 0 && min(x2, y2) >= size) + { + node.cleanup(type); + type = BM_SOLID; + node.solid = &bmsolids[val]; + return; + } + + if(type==BM_BRANCH) + { + size /= 2; + if(y1 < size) + { + if(x1 < size) fillblendmap(node.branch->type[0], node.branch->children[0], size, val, + x1, y1, min(x2, size), min(y2, size)); + if(x2 > size) fillblendmap(node.branch->type[1], node.branch->children[1], size, val, + max(x1-size, 0), y1, x2-size, min(y2, size)); + } + if(y2 > size) + { + if(x1 < size) fillblendmap(node.branch->type[2], node.branch->children[2], size, val, + x1, max(y1-size, 0), min(x2, size), y2-size); + if(x2 > size) fillblendmap(node.branch->type[3], node.branch->children[3], size, val, + max(x1-size, 0), max(y1-size, 0), x2-size, y2-size); + } + loopi(4) if(node.branch->type[i]!=BM_SOLID || node.branch->children[i].solid->val!=val) return; + node.cleanup(type); + type = BM_SOLID; + node.solid = &bmsolids[val]; + return; + } + else if(type==BM_SOLID) + { + uchar oldval = node.solid->val; + if(oldval==val) return; + + if(size > BM_IMAGE_SIZE) + { + node.splitsolid(type, oldval); + fillblendmap(type, node, size, val, x1, y1, x2, y2); + return; + } + + type = BM_IMAGE; + node.image = new BlendMapImage; + memset(node.image->data, oldval, sizeof(node.image->data)); + } + + uchar *dst = &node.image->data[y1*BM_IMAGE_SIZE + x1]; + loopi(y2-y1) + { + memset(dst, val, x2-x1); + dst += BM_IMAGE_SIZE; + } +} + +void fillblendmap(int x, int y, int w, int h, uchar val) +{ + int bmsize = worldsize>>BM_SCALE, + x1 = clamp(x, 0, bmsize), + y1 = clamp(y, 0, bmsize), + x2 = clamp(x+w, 0, bmsize), + y2 = clamp(y+h, 0, bmsize); + if(max(x1, y1) >= bmsize || min(x2, y2) <= 0 || x1>=x2 || y1>=y2) return; + fillblendmap(blendmap.type, blendmap, bmsize, val, x1, y1, x2, y2); +} + +static void invertblendmap(uchar &type, BlendMapNode &node, int size, int x1, int y1, int x2, int y2) +{ + if(type==BM_BRANCH) + { + size /= 2; + if(y1 < size) + { + if(x1 < size) invertblendmap(node.branch->type[0], node.branch->children[0], size, + x1, y1, min(x2, size), min(y2, size)); + if(x2 > size) invertblendmap(node.branch->type[1], node.branch->children[1], size, + max(x1-size, 0), y1, x2-size, min(y2, size)); + } + if(y2 > size) + { + if(x1 < size) invertblendmap(node.branch->type[2], node.branch->children[2], size, + x1, max(y1-size, 0), min(x2, size), y2-size); + if(x2 > size) invertblendmap(node.branch->type[3], node.branch->children[3], size, + max(x1-size, 0), max(y1-size, 0), x2-size, y2-size); + } + return; + } + else if(type==BM_SOLID) + { + fillblendmap(type, node, size, 255-node.solid->val, x1, y1, x2, y2); + } + else if(type==BM_IMAGE) + { + uchar *dst = &node.image->data[y1*BM_IMAGE_SIZE + x1]; + loopi(y2-y1) + { + loopj(x2-x1) dst[j] = 255-dst[j]; + dst += BM_IMAGE_SIZE; + } + } +} + +void invertblendmap(int x, int y, int w, int h) +{ + int bmsize = worldsize>>BM_SCALE, + x1 = clamp(x, 0, bmsize), + y1 = clamp(y, 0, bmsize), + x2 = clamp(x+w, 0, bmsize), + y2 = clamp(y+h, 0, bmsize); + if(max(x1, y1) >= bmsize || min(x2, y2) <= 0 || x1>=x2 || y1>=y2) return; + invertblendmap(blendmap.type, blendmap, bmsize, x1, y1, x2, y2); +} + +static void optimizeblendmap(uchar &type, BlendMapNode &node) +{ + switch(type) + { + case BM_IMAGE: + { + uint val = node.image->data[0]; + val |= val<<8; + val |= val<<16; + for(uint *data = (uint *)node.image->data, *end = &data[sizeof(node.image->data)/sizeof(uint)]; data < end; data++) + if(*data != val) return; + node.cleanup(type); + type = BM_SOLID; + node.solid = &bmsolids[val&0xFF]; + break; + } + case BM_BRANCH: + { + loopi(4) optimizeblendmap(node.branch->type[i], node.branch->children[i]); + if(node.branch->type[3]!=BM_SOLID) return; + uint val = node.branch->children[3].solid->val; + loopi(3) if(node.branch->type[i]!=BM_SOLID || node.branch->children[i].solid->val != val) return; + node.cleanup(type); + type = BM_SOLID; + node.solid = &bmsolids[val]; + break; + } + } +} + +void optimizeblendmap() +{ + optimizeblendmap(blendmap.type, blendmap); +} + +VARF(blendpaintmode, 0, 0, 5, +{ + if(!blendpaintmode) stoppaintblendmap(); +}); + +static void blitblendmap(uchar &type, BlendMapNode &node, int bmx, int bmy, int bmsize, uchar *src, int sx, int sy, int sw, int sh, int smode) +{ + if(type==BM_BRANCH) + { + bmsize /= 2; + if(sy < bmy + bmsize) + { + if(sx < bmx + bmsize) blitblendmap(node.branch->type[0], node.branch->children[0], bmx, bmy, bmsize, src, sx, sy, sw, sh, smode); + if(sx + sw > bmx + bmsize) blitblendmap(node.branch->type[1], node.branch->children[1], bmx+bmsize, bmy, bmsize, src, sx, sy, sw, sh, smode); + } + if(sy + sh > bmy + bmsize) + { + if(sx < bmx + bmsize) blitblendmap(node.branch->type[2], node.branch->children[2], bmx, bmy+bmsize, bmsize, src, sx, sy, sw, sh, smode); + if(sx + sw > bmx + bmsize) blitblendmap(node.branch->type[3], node.branch->children[3], bmx+bmsize, bmy+bmsize, bmsize, src, sx, sy, sw, sh, smode); + } + return; + } + if(type==BM_SOLID) + { + uchar val = node.solid->val; + if(bmsize > BM_IMAGE_SIZE) + { + node.splitsolid(type, val); + blitblendmap(type, node, bmx, bmy, bmsize, src, sx, sy, sw, sh, smode); + return; + } + + type = BM_IMAGE; + node.image = new BlendMapImage; + memset(node.image->data, val, sizeof(node.image->data)); + } + + int x1 = clamp(sx - bmx, 0, bmsize), y1 = clamp(sy - bmy, 0, bmsize), + x2 = clamp(sx+sw - bmx, 0, bmsize), y2 = clamp(sy+sh - bmy, 0, bmsize); + uchar *dst = &node.image->data[y1*BM_IMAGE_SIZE + x1]; + src += max(bmy - sy, 0)*sw + max(bmx - sx, 0); + loopi(y2-y1) + { + switch(smode) + { + case 1: + memcpy(dst, src, x2 - x1); + break; + + case 2: + loopi(x2 - x1) dst[i] = min(dst[i], src[i]); + break; + + case 3: + loopi(x2 - x1) dst[i] = max(dst[i], src[i]); + break; + + case 4: + loopi(x2 - x1) dst[i] = min(dst[i], uchar(0xFF - src[i])); + break; + + case 5: + loopi(x2 - x1) dst[i] = max(dst[i], uchar(0xFF - src[i])); + break; + } + dst += BM_IMAGE_SIZE; + src += sw; + } +} + +void blitblendmap(uchar *src, int sx, int sy, int sw, int sh, int smode) +{ + int bmsize = worldsize>>BM_SCALE; + if(max(sx, sy) >= bmsize || min(sx+sw, sy+sh) <= 0 || min(sw, sh) <= 0) return; + blitblendmap(blendmap.type, blendmap, 0, 0, bmsize, src, sx, sy, sw, sh, smode); +} + +void resetblendmap() +{ + blendmap.cleanup(); + blendmap.type = BM_SOLID; + blendmap.solid = &bmsolids[0xFF]; +} + +void enlargeblendmap() +{ + if(blendmap.type == BM_SOLID) return; + BlendMapBranch *branch = new BlendMapBranch; + branch->type[0] = blendmap.type; + branch->children[0] = blendmap; + loopi(3) + { + branch->type[i+1] = BM_SOLID; + branch->children[i+1].solid = &bmsolids[0xFF]; + } + blendmap.type = BM_BRANCH; + blendmap.branch = branch; +} + +void shrinkblendmap(int octant) +{ + blendmap.shrink(octant&3); +} + +void moveblendmap(uchar type, BlendMapNode &node, int size, int x, int y, int dx, int dy) +{ + if(type == BM_BRANCH) + { + size /= 2; + moveblendmap(node.branch->type[0], node.branch->children[0], size, x, y, dx, dy); + moveblendmap(node.branch->type[1], node.branch->children[1], size, x + size, y, dx, dy); + moveblendmap(node.branch->type[2], node.branch->children[2], size, x, y + size, dx, dy); + moveblendmap(node.branch->type[3], node.branch->children[3], size, x + size, y + size, dx, dy); + return; + } + else if(type == BM_SOLID) + { + fillblendmap(x+dx, y+dy, size, size, node.solid->val); + } + else if(type == BM_IMAGE) + { + blitblendmap(node.image->data, x+dx, y+dy, size, size, 1); + } +} + +void moveblendmap(int dx, int dy) +{ + BlendMapRoot old = blendmap; + blendmap.type = BM_SOLID; + blendmap.solid = &bmsolids[0xFF]; + moveblendmap(old.type, old, worldsize>>BM_SCALE, 0, 0, dx, dy); + old.cleanup(); +} + +struct BlendBrush +{ + char *name; + int w, h; + uchar *data; + GLuint tex; + + BlendBrush(const char *name, int w, int h) : + name(newstring(name)), w(w), h(h), data(new uchar[w*h]), tex(0) + {} + + ~BlendBrush() + { + cleanup(); + delete[] name; + if(data) delete[] data; + } + + void cleanup() + { + if(tex) { glDeleteTextures(1, &tex); tex = 0; } + } + + void gentex() + { + if(!tex) glGenTextures(1, &tex); + uchar *buf = new uchar[2*w*h]; + uchar *dst = buf, *src = data; + loopi(h) + { + loopj(w) *dst++ = 255 - *src++; + } + createtexture(tex, w, h, buf, 3, 1, hasTRG ? GL_R8 : GL_LUMINANCE8); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); + GLfloat border[4] = { 0, 0, 0, 0 }; + glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border); + delete[] buf; + } + + void reorient(bool flipx, bool flipy, bool swapxy) + { + uchar *rdata = new uchar[w*h]; + int stridex = 1, stridey = 1; + if(swapxy) stridex *= h; else stridey *= w; + uchar *src = data, *dst = rdata; + if(flipx) { dst += (w-1)*stridex; stridex = -stridex; } + if(flipy) { dst += (h-1)*stridey; stridey = -stridey; } + loopi(h) + { + uchar *curdst = dst; + loopj(w) + { + *curdst = *src++; + curdst += stridex; + } + dst += stridey; + } + if(swapxy) swap(w, h); + delete[] data; + data = rdata; + if(tex) gentex(); + } +}; + +static vector brushes; +static int curbrush = -1; + +void cleanupblendmap() +{ + loopv(brushes) brushes[i]->cleanup(); +} + +void clearblendbrushes() +{ + while(brushes.length()) delete brushes.pop(); + curbrush = -1; +} + +void delblendbrush(const char *name) +{ + loopv(brushes) if(!strcmp(brushes[i]->name, name)) + { + delete brushes[i]; + brushes.remove(i--); + } + curbrush = brushes.empty() ? -1 : clamp(curbrush, 0, brushes.length()-1); +} + +void addblendbrush(const char *name, const char *imgname) +{ + delblendbrush(name); + + ImageData s; + if(!loadimage(imgname, s)) { conoutf(CON_ERROR, "could not load blend brush image %s", imgname); return; } + if(max(s.w, s.h) > (1<<12)) + { + conoutf(CON_ERROR, "blend brush image size exceeded %dx%d pixels: %s", 1<<12, 1<<12, imgname); + return; + } + + BlendBrush *brush = new BlendBrush(name, s.w, s.h); + + uchar *dst = brush->data, *srcrow = s.data; + loopi(s.h) + { + for(uchar *src = srcrow, *end = &srcrow[s.w*s.bpp]; src < end; src += s.bpp) + *dst++ = src[0]; + srcrow += s.pitch; + } + + brushes.add(brush); + if(curbrush < 0) curbrush = 0; + else if(curbrush >= brushes.length()) curbrush = brushes.length()-1; + +} + +void nextblendbrush(int *dir) +{ + curbrush += *dir < 0 ? -1 : 1; + if(brushes.empty()) curbrush = -1; + else if(!brushes.inrange(curbrush)) curbrush = *dir < 0 ? brushes.length()-1 : 0; +} + +void setblendbrush(const char *name) +{ + loopv(brushes) if(!strcmp(brushes[i]->name, name)) { curbrush = i; break; } +} + +void getblendbrushname(int *n) +{ + result(brushes.inrange(*n) ? brushes[*n]->name : ""); +} + +void curblendbrush() +{ + intret(curbrush); +} + +COMMAND(clearblendbrushes, ""); +COMMAND(delblendbrush, "s"); +COMMAND(addblendbrush, "ss"); +COMMAND(nextblendbrush, "i"); +COMMAND(setblendbrush, "s"); +COMMAND(getblendbrushname, "i"); +COMMAND(curblendbrush, ""); + +extern int nompedit; + +bool canpaintblendmap(bool brush = true, bool sel = false, bool msg = true) +{ + if(noedit(!sel, msg) || (nompedit && multiplayer())) return false; + if(!blendpaintmode) + { + if(msg) conoutf(CON_ERROR, "operation only allowed in blend paint mode"); + return false; + } + if(brush && !brushes.inrange(curbrush)) + { + if(msg) conoutf(CON_ERROR, "no blend brush selected"); + return false; + } + return true; +} + +void rotateblendbrush(int *val) +{ + if(!canpaintblendmap()) return; + BlendBrush *brush = brushes[curbrush]; + const texrotation &r = texrotations[*val < 0 ? 3 : clamp(*val, 1, 7)]; + brush->reorient(r.flipx, r.flipy, r.swapxy); +} + +COMMAND(rotateblendbrush, "i"); + +void paintblendmap(bool msg) +{ + if(!canpaintblendmap(true, false, msg)) return; + + BlendBrush *brush = brushes[curbrush]; + int x = (int)floor(clamp(worldpos.x, 0.0f, float(worldsize))/(1<w), + y = (int)floor(clamp(worldpos.y, 0.0f, float(worldsize))/(1<h); + blitblendmap(brush->data, x, y, brush->w, brush->h, blendpaintmode); + previewblends(ivec((x-1)<w+1)<h+1)<>BM_SCALE, y1 = sel.o.y>>BM_SCALE, + x2 = (sel.o.x+sel.s.x*sel.grid+(1<>BM_SCALE, + y2 = (sel.o.y+sel.s.y*sel.grid+(1<>BM_SCALE; + fillblendmap(x1, y1, x2-x1, y2-y1, 0xFF); + previewblends(ivec(x1<>BM_SCALE, y1 = sel.o.y>>BM_SCALE, + x2 = (sel.o.x+sel.s.x*sel.grid+(1<>BM_SCALE, + y2 = (sel.o.y+sel.s.y*sel.grid+(1<>BM_SCALE; + invertblendmap(x1, y1, x2-x1, y2-y1); + previewblends(ivec(x1<>BM_SCALE, worldsize>>BM_SCALE); + previewblends(ivec(0, 0, 0), ivec(worldsize, worldsize, worldsize)); +} + +COMMAND(invertblendmap, ""); + +void showblendmap() +{ + if(noedit(true) || (nompedit && multiplayer())) return; + previewblends(ivec(0, 0, 0), ivec(worldsize, worldsize, worldsize)); +} + +COMMAND(showblendmap, ""); +COMMAND(optimizeblendmap, ""); +ICOMMAND(clearblendmap, "", (), +{ + if(noedit(true) || (nompedit && multiplayer())) return; + resetblendmap(); + showblendmap(); +}); + +ICOMMAND(moveblendmap, "ii", (int *dx, int *dy), +{ + if(noedit(true) || (nompedit && multiplayer())) return; + if(*dx%(BM_IMAGE_SIZE<= worldsize || *dy <= -worldsize || *dy >= worldsize) + resetblendmap(); + else + moveblendmap(*dx>>BM_SCALE, *dy>>BM_SCALE); + showblendmap(); +}); + +void renderblendbrush() +{ + if(!blendpaintmode || !brushes.inrange(curbrush)) return; + + BlendBrush *brush = brushes[curbrush]; + int x1 = (int)floor(clamp(worldpos.x, 0.0f, float(worldsize))/(1<w) << BM_SCALE, + y1 = (int)floor(clamp(worldpos.y, 0.0f, float(worldsize))/(1<h) << BM_SCALE, + x2 = x1 + (brush->w << BM_SCALE), + y2 = y1 + (brush->h << BM_SCALE); + + if(max(x1, y1) >= worldsize || min(x2, y2) <= 0 || x1>=x2 || y1>=y2) return; + + if(!brush->tex) brush->gentex(); + renderblendbrush(brush->tex, x1, y1, x2 - x1, y2 - y1); +} + +bool loadblendmap(stream *f, uchar &type, BlendMapNode &node) +{ + type = f->getchar(); + switch(type) + { + case BM_SOLID: + { + int val = f->getchar(); + if(val<0 || val>0xFF) return false; + node.solid = &bmsolids[val]; + break; + } + + case BM_IMAGE: + node.image = new BlendMapImage; + if(f->read(node.image->data, sizeof(node.image->data)) != sizeof(node.image->data)) + return false; + break; + + case BM_BRANCH: + node.branch = new BlendMapBranch; + loopi(4) { node.branch->type[i] = BM_SOLID; node.branch->children[i].solid = &bmsolids[0xFF]; } + loopi(4) if(!loadblendmap(f, node.branch->type[i], node.branch->children[i])) + return false; + break; + + default: + type = BM_SOLID; + node.solid = &bmsolids[0xFF]; + return false; + } + return true; +} + +bool loadblendmap(stream *f, int info) +{ + resetblendmap(); + return loadblendmap(f, blendmap.type, blendmap); +} + +void saveblendmap(stream *f, uchar type, BlendMapNode &node) +{ + f->putchar(type); + switch(type) + { + case BM_SOLID: + f->putchar(node.solid->val); + break; + + case BM_IMAGE: + f->write(node.image->data, sizeof(node.image->data)); + break; + + case BM_BRANCH: + loopi(4) saveblendmap(f, node.branch->type[i], node.branch->children[i]); + break; + } +} + +void saveblendmap(stream *f) +{ + saveblendmap(f, blendmap.type, blendmap); +} + +uchar shouldsaveblendmap() +{ + return blendmap.solid!=&bmsolids[0xFF] ? 1 : 0; +} + diff --git a/src/engine/blob.cpp b/src/engine/blob.cpp new file mode 100644 index 0000000..a587971 --- /dev/null +++ b/src/engine/blob.cpp @@ -0,0 +1,727 @@ +#include "engine.h" + +extern int intel_mapbufferrange_bug; + +VARNP(blobs, showblobs, 0, 1, 1); +VARFP(blobintensity, 0, 60, 100, resetblobs()); +VARFP(blobheight, 1, 32, 128, resetblobs()); +VARFP(blobfadelow, 1, 8, 32, resetblobs()); +VARFP(blobfadehigh, 1, 8, 32, resetblobs()); +VARFP(blobmargin, 0, 1, 16, resetblobs()); + +VAR(dbgblob, 0, 0, 1); + +enum +{ + BL_DUP = 1<<0, + BL_RENDER = 1<<1 +}; + +struct blobinfo +{ + vec o; + float radius; + int millis; + ushort startindex, endindex, startvert, endvert, next, flags; +}; + +struct blobvert +{ + vec pos; + bvec4 color; + vec2 tc; +}; + +struct blobrenderer +{ + const char *texname; + Texture *tex; + ushort *cache; + int cachesize; + blobinfo *blobs; + int maxblobs, startblob, endblob; + blobvert *verts; + int maxverts, startvert, endvert, availverts; + ushort *indexes; + int maxindexes, startindex, endindex, availindexes; + GLuint ebo, vbo; + ushort *edata; + blobvert *vdata; + int numedata, numvdata; + blobinfo *startrender, *endrender; + + blobinfo *lastblob; + + vec blobmin, blobmax; + ivec bbmin, bbmax; + float blobalphalow, blobalphahigh; + uchar blobalpha; + + blobrenderer(const char *texname) + : texname(texname), tex(NULL), + cache(NULL), cachesize(0), + blobs(NULL), maxblobs(0), startblob(0), endblob(0), + verts(NULL), maxverts(0), startvert(0), endvert(0), availverts(0), + indexes(NULL), maxindexes(0), startindex(0), endindex(0), availindexes(0), + ebo(0), vbo(0), edata(NULL), vdata(NULL), numedata(0), numvdata(0), + startrender(NULL), endrender(NULL), lastblob(NULL) + {} + + ~blobrenderer() + { + DELETEA(cache); + DELETEA(blobs); + DELETEA(verts); + DELETEA(indexes); + } + + void cleanup() + { + if(ebo) { glDeleteBuffers_(1, &ebo); ebo = 0; } + if(vbo) { glDeleteBuffers_(1, &vbo); vbo = 0; } + DELETEA(edata); + DELETEA(vdata); + numedata = numvdata = 0; + startrender = endrender = NULL; + } + + void init(int tris) + { + cleanup(); + if(cache) + { + DELETEA(cache); + cachesize = 0; + } + if(blobs) + { + DELETEA(blobs); + maxblobs = startblob = endblob = 0; + } + if(verts) + { + DELETEA(verts); + maxverts = startvert = endvert = availverts = 0; + } + if(indexes) + { + DELETEA(indexes); + maxindexes = startindex = endindex = availindexes = 0; + } + if(!tris) return; + tex = textureload(texname, 3); + cachesize = tris/2; + cache = new ushort[cachesize]; + memset(cache, 0xFF, cachesize * sizeof(ushort)); + maxblobs = tris/2; + blobs = new blobinfo[maxblobs]; + memclear(blobs, maxblobs); + maxindexes = tris*3 + 3; + availindexes = maxindexes - 3; + indexes = new ushort[maxindexes]; + maxverts = min(tris*3/2 + 1, (1<<16)-1); + availverts = maxverts - 1; + verts = new blobvert[maxverts]; + } + + bool freeblob() + { + blobinfo &b = blobs[startblob]; + if(&b == lastblob) return false; + + if(b.flags & BL_RENDER) flushblobs(); + + startblob++; + if(startblob >= maxblobs) startblob = 0; + + startvert = b.endvert; + if(startvert>=maxverts) startvert = 0; + availverts += b.endvert - b.startvert; + + startindex = b.endindex; + if(startindex>=maxindexes) startindex = 0; + availindexes += b.endindex - b.startindex; + + b.millis = lastreset; + b.flags = 0; + + return true; + } + + blobinfo &newblob(const vec &o, float radius) + { + blobinfo &b = blobs[endblob]; + int next = endblob + 1; + if(next>=maxblobs) next = 0; + if(next==startblob) + { + lastblob = &b; + freeblob(); + } + endblob = next; + b.o = o; + b.radius = radius; + b.millis = totalmillis; + b.flags = 0; + b.next = 0xFFFF; + b.startindex = b.endindex = endindex; + b.startvert = b.endvert = endvert; + lastblob = &b; + return b; + } + + template + static int split(const vec *in, int numin, float below, float above, vec *out) + { + int numout = 0; + const vec *p = &in[numin-1]; + float pc = (*p)[C]; + loopi(numin) + { + const vec &v = in[i]; + float c = v[C]; + if(c < below) + { + if(pc > above) out[numout++] = vec(*p).sub(v).mul((above - c)/(pc - c)).add(v); + if(pc > below) out[numout++] = vec(*p).sub(v).mul((below - c)/(pc - c)).add(v); + } + else if(c > above) + { + if(pc < below) out[numout++] = vec(*p).sub(v).mul((below - c)/(pc - c)).add(v); + if(pc < above) out[numout++] = vec(*p).sub(v).mul((above - c)/(pc - c)).add(v); + } + else if(pc < below) + { + if(c > below) out[numout++] = vec(*p).sub(v).mul((below - c)/(pc - c)).add(v); + } + else if(pc > above && c < above) out[numout++] = vec(*p).sub(v).mul((above - c)/(pc - c)).add(v); + out[numout++] = v; + p = &v; + pc = c; + } + return numout; + } + + template + static int clip(const vec *in, int numin, float below, float above, vec *out) + { + int numout = 0; + const vec *p = &in[numin-1]; + float pc = (*p)[C]; + loopi(numin) + { + const vec &v = in[i]; + float c = v[C]; + if(c < below) + { + if(pc > above) out[numout++] = vec(*p).sub(v).mul((above - c)/(pc - c)).add(v); + if(pc > below) out[numout++] = vec(*p).sub(v).mul((below - c)/(pc - c)).add(v); + } + else if(c > above) + { + if(pc < below) out[numout++] = vec(*p).sub(v).mul((below - c)/(pc - c)).add(v); + if(pc < above) out[numout++] = vec(*p).sub(v).mul((above - c)/(pc - c)).add(v); + } + else + { + if(pc < below) + { + if(c > below) out[numout++] = vec(*p).sub(v).mul((below - c)/(pc - c)).add(v); + } + else if(pc > above && c < above) out[numout++] = vec(*p).sub(v).mul((above - c)/(pc - c)).add(v); + out[numout++] = v; + } + p = &v; + pc = c; + } + return numout; + } + + void dupblob() + { + if(lastblob->startvert >= lastblob->endvert) + { + lastblob->startindex = lastblob->endindex = endindex; + lastblob->startvert = lastblob->endvert = endvert; + return; + } + blobinfo &b = newblob(lastblob->o, lastblob->radius); + b.flags |= BL_DUP; + } + + inline int addvert(const vec &pos) + { + blobvert &v = verts[endvert]; + v.pos = pos; + v.tc = vec2((pos.x - blobmin.x) / (blobmax.x - blobmin.x), + (pos.y - blobmin.y) / (blobmax.y - blobmin.y)); + uchar alpha; + if(pos.z < blobmin.z + blobfadelow) alpha = uchar(blobalphalow * (pos.z - blobmin.z)); + else if(pos.z > blobmax.z - blobfadehigh) alpha = uchar(blobalphahigh * (blobmax.z - pos.z)); + else alpha = blobalpha; + v.color = bvec4(255, 255, 255, alpha); + return endvert++; + } + + void addtris(const vec *v, int numv) + { + if(endvert != int(lastblob->endvert) || endindex != int(lastblob->endindex)) dupblob(); + for(const vec *cur = &v[2], *end = &v[numv];;) + { + int limit = maxverts - endvert - 2; + if(limit <= 0) + { + while(availverts < limit+2) if(!freeblob()) return; + availverts -= limit+2; + lastblob->endvert = maxverts; + endvert = 0; + dupblob(); + limit = maxverts - 2; + } + limit = min(int(end - cur), min(limit, (maxindexes - endindex)/3)); + while(availverts < limit+2) if(!freeblob()) return; + while(availindexes < limit*3) if(!freeblob()) return; + + int i1 = addvert(v[0]), i2 = addvert(cur[-1]); + loopk(limit) + { + indexes[endindex++] = i1; + indexes[endindex++] = i2; + i2 = addvert(*cur++); + indexes[endindex++] = i2; + } + + availverts -= endvert - lastblob->endvert; + availindexes -= endindex - lastblob->endindex; + lastblob->endvert = endvert; + lastblob->endindex = endindex; + if(endvert >= maxverts) endvert = 0; + if(endindex >= maxindexes) endindex = 0; + + if(cur >= end) break; + dupblob(); + } + } + + void gentris(cube &cu, int orient, const ivec &o, int size, materialsurface *mat = NULL, int vismask = 0) + { + vec pos[MAXFACEVERTS+8]; + int dim = dimension(orient), numverts = 0, numplanes = 1, flat = -1; + if(mat) + { + switch(orient) + { + #define GENFACEORIENT(orient, v0, v1, v2, v3) \ + case orient: v0 v1 v2 v3 break; + #define GENFACEVERT(orient, vert, x,y,z, xv,yv,zv) \ + pos[numverts++] = vec(x xv, y yv, z zv); + GENFACEVERTS(o.x, o.x, o.y, o.y, o.z, o.z, , + mat->csize, , + mat->rsize, + 0.1f, - 0.1f); + #undef GENFACEORIENT + #undef GENFACEVERT + default: + return; + } + flat = dim; + } + else if(cu.texture[orient] == DEFAULT_SKY) return; + else if(cu.ext && (numverts = cu.ext->surfaces[orient].numverts&MAXFACEVERTS)) + { + vertinfo *verts = cu.ext->verts() + cu.ext->surfaces[orient].verts; + ivec vo = ivec(o).mask(~0xFFF).shl(3); + loopj(numverts) pos[j] = vec(verts[j].getxyz().add(vo)).mul(1/8.0f); + if(numverts >= 4 && !(cu.merged&(1<= 0) + { + float offset = pos[0][dim]; + if(offset < blobmin[dim] || offset > blobmax[dim]) return; + flat = dim; + } + + vec vmin = pos[0], vmax = pos[0]; + for(int i = 1; i < numverts; i++) { vmin.min(pos[i]); vmax.max(pos[i]); } + if(vmax.x < blobmin.x || vmin.x > blobmax.x || vmax.y < blobmin.y || vmin.y > blobmax.y || + vmax.z < blobmin.z || vmin.z > blobmax.z) + return; + + vec v1[MAXFACEVERTS+6+4], v2[MAXFACEVERTS+6+4]; + loopl(numplanes) + { + vec *v = pos; + int numv = numverts; + if(numplanes >= 2) + { + if(l) { pos[1] = pos[2]; pos[2] = pos[3]; } + numv = 3; + } + if(vec().cross(v[0], v[1], v[2]).z <= 0) continue; + #define CLIPSIDE(clip, below, above) \ + { \ + vec *in = v; \ + v = in==v1 ? v2 : v1; \ + numv = clip(in, numv, below, above, v); \ + if(numv < 3) continue; \ + } + if(flat!=0) CLIPSIDE(clip<0>, blobmin.x, blobmax.x); + if(flat!=1) CLIPSIDE(clip<1>, blobmin.y, blobmax.y); + if(flat!=2) + { + CLIPSIDE(clip<2>, blobmin.z, blobmax.z); + CLIPSIDE(split<2>, blobmin.z + blobfadelow, blobmax.z - blobfadehigh); + } + addtris(v, numv); + } + } + + void findmaterials(vtxarray *va) + { + materialsurface *matbuf = va->matbuf; + int matsurfs = va->matsurfs; + loopi(matsurfs) + { + materialsurface &m = matbuf[i]; + if(!isclipped(m.material&MATF_VOLUME) || m.orient == O_BOTTOM) { i += m.skip; continue; } + int dim = dimension(m.orient), c = C[dim], r = R[dim]; + for(;;) + { + materialsurface &m = matbuf[i]; + if(m.o[dim] >= blobmin[dim] && m.o[dim] <= blobmax[dim] && + m.o[c] + m.csize >= blobmin[c] && m.o[c] <= blobmax[c] && + m.o[r] + m.rsize >= blobmin[r] && m.o[r] <= blobmax[r]) + { + static cube dummy; + gentris(dummy, m.orient, m.o, max(m.csize, m.rsize), &m); + } + if(i+1 >= matsurfs) break; + materialsurface &n = matbuf[i+1]; + if(n.material != m.material || n.orient != m.orient) break; + i++; + } + } + } + + void findescaped(cube *cu, const ivec &o, int size, int escaped) + { + loopi(8) + { + if(escaped&(1<>1, cu[i].escaped); + else + { + int vismask = cu[i].merged; + if(vismask) loopj(6) if(vismask&(1<va && cu[i].ext->va->matsurfs) + findmaterials(cu[i].ext->va); + if(cu[i].children) gentris(cu[i].children, co, size>>1, cu[i].escaped); + else + { + int vismask = cu[i].visible; + if(vismask&0xC0) + { + if(vismask&0x80) loopj(6) gentris(cu[i], j, co, size, NULL, vismask); + else loopj(6) if(vismask&(1<>1, cu[i].escaped); + else + { + int vismask = cu[i].merged; + if(vismask) loopj(6) if(vismask&(1<>1); + return !(b.flags & BL_DUP) ? &b : NULL; + } + + static void setuprenderstate() + { + foggedshader->set(); + + enablepolygonoffset(GL_POLYGON_OFFSET_FILL); + + glDepthMask(GL_FALSE); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + if(!dbgblob) glEnable(GL_BLEND); + + gle::enablevertex(); + gle::enabletexcoord0(); + gle::enablecolor(); + } + + static void cleanuprenderstate() + { + gle::disablevertex(); + gle::disabletexcoord0(); + gle::disablecolor(); + + gle::clearvbo(); + if(glversion >= 300) gle::clearebo(); + + glDepthMask(GL_TRUE); + glDisable(GL_BLEND); + + disablepolygonoffset(GL_POLYGON_OFFSET_FILL); + } + + static int lastreset; + + static void reset() + { + lastreset = totalmillis; + } + + static blobrenderer *lastrender; + + void fadeblob(blobinfo *b, float fade) + { + float minz = b->o.z - (blobheight + blobfadelow), maxz = b->o.z + blobfadehigh, + scale = fade*blobintensity*255/100.0f, scalelow = scale / blobfadelow, scalehigh = scale / blobfadehigh; + uchar alpha = uchar(scale); + b->millis = totalmillis; + do + { + if(b->endvert - b->startvert >= 3) for(blobvert *v = &verts[b->startvert], *end = &verts[b->endvert]; v < end; v++) + { + float z = v->pos.z; + if(z < minz + blobfadelow) v->color.a = uchar(scalelow * (z - minz)); + else if(z > maxz - blobfadehigh) v->color.a = uchar(scalehigh * (maxz - z)); + else v->color.a = alpha; + } + int offset = b - &blobs[0] + 1; + if(offset >= maxblobs) offset = 0; + if(offset < endblob ? offset > startblob || startblob > endblob : offset > startblob) b = &blobs[offset]; + else break; + } while(b->flags & BL_DUP); + } + + void renderblob(const vec &o, float radius, float fade) + { + if(!blobs) initblobs(); + + if(glversion < 300 && lastrender != this) + { + if(!lastrender) setuprenderstate(); + gle::vertexpointer(sizeof(blobvert), verts->pos.v); + gle::texcoord0pointer(sizeof(blobvert), verts->tc.v); + gle::colorpointer(sizeof(blobvert), verts->color.v); + if(!lastrender || lastrender->tex != tex) glBindTexture(GL_TEXTURE_2D, tex->id); + lastrender = this; + } + + union { int i; float f; } ox, oy; + ox.f = o.x; oy.f = o.y; + uint hash = uint(ox.i^~oy.i^(INT_MAX-oy.i)^uint(radius)); + hash %= cachesize; + blobinfo *b = &blobs[cache[hash]]; + if(b >= &blobs[maxblobs] || b->millis - lastreset <= 0 || b->o!=o || b->radius!=radius) + { + b = addblob(o, radius, fade); + cache[hash] = ushort(b - blobs); + if(!b) return; + } + else if(fade < 1 && totalmillis - b->millis > 0) fadeblob(b, fade); + do + { + if(b->endvert - b->startvert >= 3) + { + if(glversion >= 300) + { + if(!startrender) { numedata = numvdata = 0; startrender = endrender = b; } + else { endrender->next = ushort(b - blobs); endrender = b; } + b->flags |= BL_RENDER; + b->next = 0xFFFF; + numedata += b->endindex - b->startindex; + numvdata += b->endvert - b->startvert; + } + else + { + glDrawRangeElements_(GL_TRIANGLES, b->startvert, b->endvert-1, b->endindex - b->startindex, GL_UNSIGNED_SHORT, &indexes[b->startindex]); + xtravertsva += b->endvert - b->startvert; + } + } + int offset = b - &blobs[0] + 1; + if(offset >= maxblobs) offset = 0; + if(offset < endblob ? offset > startblob || startblob > endblob : offset > startblob) b = &blobs[offset]; + else break; + } while(b->flags & BL_DUP); + } + + void flushblobs() + { + if(glversion < 300 || !startrender) return; + + if(lastrender != this) + { + if(!lastrender) setuprenderstate(); + lastrender = this; + } + + if(!ebo) glGenBuffers_(1, &ebo); + if(!vbo) glGenBuffers_(1, &vbo); + + gle::bindebo(ebo); + glBufferData_(GL_ELEMENT_ARRAY_BUFFER, maxindexes*sizeof(ushort), NULL, GL_STREAM_DRAW); + gle::bindvbo(vbo); + glBufferData_(GL_ARRAY_BUFFER, maxverts*sizeof(blobvert), NULL, GL_STREAM_DRAW); + + ushort *estart; + blobvert *vstart; + if(intel_mapbufferrange_bug) + { + if(!edata) edata = new ushort[maxindexes]; + if(!vdata) vdata = new blobvert[maxverts]; + estart = edata; + vstart = vdata; + } + else + { + estart = (ushort *)glMapBufferRange_(GL_ELEMENT_ARRAY_BUFFER, 0, numedata*sizeof(ushort), GL_MAP_WRITE_BIT|GL_MAP_INVALIDATE_RANGE_BIT|GL_MAP_UNSYNCHRONIZED_BIT); + vstart = (blobvert *)glMapBufferRange_(GL_ARRAY_BUFFER, 0, numvdata*sizeof(blobvert), GL_MAP_WRITE_BIT|GL_MAP_INVALIDATE_RANGE_BIT|GL_MAP_UNSYNCHRONIZED_BIT); + if(!estart || !vstart) + { + if(estart) glUnmapBuffer_(GL_ELEMENT_ARRAY_BUFFER); + if(vstart) glUnmapBuffer_(GL_ARRAY_BUFFER); + for(blobinfo *b = startrender;; b = &blobs[b->next]) + { + b->flags &= ~BL_RENDER; + if(b->next >= maxblobs) break; + } + startrender = endrender = NULL; + return; + } + } + + ushort *edst = estart; + blobvert *vdst = vstart; + for(blobinfo *b = startrender;; b = &blobs[b->next]) + { + b->flags &= ~BL_RENDER; + ushort offset = ushort(vdst - vstart) - b->startvert; + for(int i = b->startindex; i < b->endindex; ++i) + *edst++ = indexes[i] + offset; + memcpy(vdst, &verts[b->startvert], (b->endvert - b->startvert)*sizeof(blobvert)); + vdst += b->endvert - b->startvert; + if(b->next >= maxblobs) break; + } + startrender = endrender = NULL; + + if(intel_mapbufferrange_bug) + { + glBufferSubData_(GL_ELEMENT_ARRAY_BUFFER, 0, numedata*sizeof(ushort), estart); + glBufferSubData_(GL_ARRAY_BUFFER, 0, numvdata*sizeof(blobvert), vstart); + } + else + { + glUnmapBuffer_(GL_ELEMENT_ARRAY_BUFFER); + glUnmapBuffer_(GL_ARRAY_BUFFER); + } + + const blobvert *ptr = 0; + gle::vertexpointer(sizeof(blobvert), ptr->pos.v); + gle::texcoord0pointer(sizeof(blobvert), ptr->tc.v); + gle::colorpointer(sizeof(blobvert), ptr->color.v); + + glBindTexture(GL_TEXTURE_2D, tex->id); + + glDrawRangeElements_(GL_TRIANGLES, 0, numvdata-1, numedata, GL_UNSIGNED_SHORT, (ushort *)0); + } +}; + +int blobrenderer::lastreset = 0; +blobrenderer *blobrenderer::lastrender = NULL; + +VARFP(blobstattris, 128, 4096, 16384, initblobs(BLOB_STATIC)); +VARFP(blobdyntris, 128, 4096, 16384, initblobs(BLOB_DYNAMIC)); + +static blobrenderer blobs[] = +{ + blobrenderer("packages/particles/blob.png"), + blobrenderer("packages/particles/blob.png") +}; + +void initblobs(int type) +{ + if(type < 0 || (type==BLOB_STATIC && blobs[BLOB_STATIC].blobs)) blobs[BLOB_STATIC].init(showblobs ? blobstattris : 0); + if(type < 0 || (type==BLOB_DYNAMIC && blobs[BLOB_DYNAMIC].blobs)) blobs[BLOB_DYNAMIC].init(showblobs ? blobdyntris : 0); +} + +void resetblobs() +{ + blobrenderer::reset(); +} + +void renderblob(int type, const vec &o, float radius, float fade) +{ + if(!showblobs) return; + if(refracting < 0 && o.z - blobheight - blobfadelow >= reflectz) return; + blobs[type].renderblob(o, radius + blobmargin, fade); +} + +void flushblobs() +{ + loopi(sizeof(blobs)/sizeof(blobs[0])) blobs[i].flushblobs(); + if(blobrenderer::lastrender) blobrenderer::cleanuprenderstate(); + blobrenderer::lastrender = NULL; +} + +void cleanupblobs() +{ + loopi(sizeof(blobs)/sizeof(blobs[0])) blobs[i].cleanup(); +} + diff --git a/src/engine/client.cpp b/src/engine/client.cpp new file mode 100644 index 0000000..3cfef8e --- /dev/null +++ b/src/engine/client.cpp @@ -0,0 +1,274 @@ +// client.cpp, mostly network related client game code + +#include "engine.h" + +ENetHost *clienthost = NULL; +ENetPeer *curpeer = NULL, *connpeer = NULL; +int connmillis = 0, connattempts = 0, discmillis = 0; + +bool multiplayer(bool msg) +{ + bool val = curpeer || hasnonlocalclients(); + if(val && msg) conoutf(CON_ERROR, "operation not available in multiplayer"); + return val; +} + +void setrate(int rate) +{ + if(!curpeer) return; + enet_host_bandwidth_limit(clienthost, rate*1024, rate*1024); +} + +VARF(rate, 0, 0, 1024, setrate(rate)); + +void throttle(); + +VARF(throttle_interval, 0, 5, 30, throttle()); +VARF(throttle_accel, 0, 2, 32, throttle()); +VARF(throttle_decel, 0, 2, 32, throttle()); + +void throttle() +{ + if(!curpeer) return; + ASSERT(ENET_PEER_PACKET_THROTTLE_SCALE==32); + enet_peer_throttle_configure(curpeer, throttle_interval*1000, throttle_accel, throttle_decel); +} + +bool isconnected(bool attempt, bool local) +{ + return curpeer || (attempt && connpeer) || (local && haslocalclients()); +} + +ICOMMAND(isconnected, "bb", (int *attempt, int *local), intret(isconnected(*attempt > 0, *local != 0) ? 1 : 0)); + +const ENetAddress *connectedpeer() +{ + return curpeer ? &curpeer->address : NULL; +} + +ICOMMAND(connectedip, "", (), +{ + const ENetAddress *address = connectedpeer(); + string hostname; + result(address && enet_address_get_host_ip(address, hostname, sizeof(hostname)) >= 0 ? hostname : ""); +}); + +ICOMMAND(connectedport, "", (), +{ + const ENetAddress *address = connectedpeer(); + intret(address ? address->port : -1); +}); + +void abortconnect() +{ + if(!connpeer) return; + game::connectfail(); + if(connpeer->state!=ENET_PEER_STATE_DISCONNECTED) enet_peer_reset(connpeer); + connpeer = NULL; + if(curpeer) return; + enet_host_destroy(clienthost); + clienthost = NULL; +} + +SVARP(connectname, ""); +VARP(connectport, 0, 0, 0xFFFF); + +void connectserv(const char *servername, int serverport, const char *serverpassword) +{ + if(connpeer) + { + conoutf("aborting connection attempt"); + abortconnect(); + } + + if(serverport <= 0) serverport = server::serverport(); + + ENetAddress address; + address.port = serverport; + + if(servername) + { + if(strcmp(servername, connectname)) setsvar("connectname", servername); + if(serverport != connectport) setvar("connectport", serverport); + addserver(servername, serverport, serverpassword && serverpassword[0] ? serverpassword : NULL); + conoutf("attempting to connect to %s:%d", servername, serverport); + if(!resolverwait(servername, &address)) + { + conoutf(CON_ERROR, "\f3could not resolve server %s", servername); + return; + } + } + else + { + setsvar("connectname", ""); + setvar("connectport", 0); + conoutf("attempting to connect over LAN"); + address.host = ENET_HOST_BROADCAST; + } + + if(!clienthost) + { + clienthost = enet_host_create(NULL, 2, server::numchannels(), rate*1024, rate*1024); + if(!clienthost) + { + conoutf(CON_ERROR, "\f3could not connect to server"); + return; + } + clienthost->duplicatePeers = 0; + } + + connpeer = enet_host_connect(clienthost, &address, server::numchannels(), 0); + enet_host_flush(clienthost); + connmillis = totalmillis; + connattempts = 0; + + game::connectattempt(servername ? servername : "", serverpassword ? serverpassword : "", address); +} + +void reconnect(const char *serverpassword) +{ + if(!connectname[0] || connectport <= 0) + { + conoutf(CON_ERROR, "no previous connection"); + return; + } + + connectserv(connectname, connectport, serverpassword); +} + +void disconnect(bool async, bool cleanup) +{ + if(curpeer) + { + if(!discmillis) + { + enet_peer_disconnect(curpeer, DISC_NONE); + enet_host_flush(clienthost); + discmillis = totalmillis; + } + if(curpeer->state!=ENET_PEER_STATE_DISCONNECTED) + { + if(async) return; + enet_peer_reset(curpeer); + } + curpeer = NULL; + discmillis = 0; + conoutf("disconnected"); + game::gamedisconnect(cleanup); + mainmenu = 1; + } + if(!connpeer && clienthost) + { + enet_host_destroy(clienthost); + clienthost = NULL; + } +} + +void trydisconnect(bool local) +{ + if(connpeer) + { + conoutf("aborting connection attempt"); + abortconnect(); + } + else if(curpeer) + { + conoutf("attempting to disconnect..."); + disconnect(!discmillis); + } + else if(local && haslocalclients()) localdisconnect(); + else conoutf(CON_WARN, "not connected"); +} + +ICOMMAND(connect, "sis", (char *name, int *port, char *pw), connectserv(name, *port, pw)); +ICOMMAND(lanconnect, "is", (int *port, char *pw), connectserv(NULL, *port, pw)); +COMMAND(reconnect, "s"); +ICOMMAND(disconnect, "b", (int *local), trydisconnect(*local != 0)); +ICOMMAND(localconnect, "", (), { if(!isconnected()) localconnect(); }); +ICOMMAND(localdisconnect, "", (), { if(haslocalclients()) localdisconnect(); }); + +void sendclientpacket(ENetPacket *packet, int chan) +{ + if(curpeer) enet_peer_send(curpeer, chan, packet); + else localclienttoserver(chan, packet); +} + +void flushclient() +{ + if(clienthost) enet_host_flush(clienthost); +} + +void neterr(const char *s, bool disc) +{ + conoutf(CON_ERROR, "\f3illegal network message (%s)", s); + if(disc) disconnect(); +} + +void localservertoclient(int chan, ENetPacket *packet) // processes any updates from the server +{ + packetbuf p(packet); + game::parsepacketclient(chan, p); +} + +void clientkeepalive() { if(clienthost) enet_host_service(clienthost, NULL, 0); } + +void gets2c() // get updates from the server +{ + ENetEvent event; + if(!clienthost) return; + if(connpeer && totalmillis/3000 > connmillis/3000) + { + conoutf("attempting to connect..."); + connmillis = totalmillis; + ++connattempts; + if(connattempts > 3) + { + conoutf(CON_ERROR, "\f3could not connect to server"); + abortconnect(); + return; + } + } + while(clienthost && enet_host_service(clienthost, &event, 0)>0) + switch(event.type) + { + case ENET_EVENT_TYPE_CONNECT: + disconnect(false, false); + localdisconnect(false); + curpeer = connpeer; + connpeer = NULL; + conoutf("connected to server"); + throttle(); + if(rate) setrate(rate); + game::gameconnect(true); + break; + + case ENET_EVENT_TYPE_RECEIVE: + if(discmillis) conoutf("attempting to disconnect..."); + else localservertoclient(event.channelID, event.packet); + enet_packet_destroy(event.packet); + break; + + case ENET_EVENT_TYPE_DISCONNECT: + if(event.data>=DISC_NUM) event.data = DISC_NONE; + if(event.peer==connpeer) + { + conoutf(CON_ERROR, "\f3could not connect to server"); + abortconnect(); + } + else + { + if(!discmillis || event.data) + { + const char *msg = disconnectreason(event.data); + if(msg) conoutf(CON_ERROR, "\f3server network error, disconnecting (%s) ...", msg); + else conoutf(CON_ERROR, "\f3server network error, disconnecting..."); + } + disconnect(); + } + return; + + default: + break; + } +} + diff --git a/src/engine/command.cpp b/src/engine/command.cpp new file mode 100644 index 0000000..74efff2 --- /dev/null +++ b/src/engine/command.cpp @@ -0,0 +1,3484 @@ +// command.cpp: implements the parsing and execution of a tiny script language which +// is largely backwards compatible with the quake console language. + +#include "engine.h" + +hashnameset idents; // contains ALL vars/commands/aliases +vector identmap; +ident *dummyident = NULL; + +int identflags = 0; + +enum +{ + MAXARGS = 25, + MAXCOMARGS = 12 +}; + +VARN(numargs, _numargs, MAXARGS, 0, 0); + +static inline void freearg(tagval &v) +{ + switch(v.type) + { + case VAL_STR: delete[] v.s; break; + case VAL_CODE: if(v.code[-1] == CODE_START) delete[] (uchar *)&v.code[-1]; break; + } +} + +static inline void forcenull(tagval &v) +{ + switch(v.type) + { + case VAL_NULL: return; + } + freearg(v); + v.setnull(); +} + +static inline float forcefloat(tagval &v) +{ + float f = 0.0f; + switch(v.type) + { + case VAL_INT: f = v.i; break; + case VAL_STR: f = parsefloat(v.s); break; + case VAL_MACRO: f = parsefloat(v.s); break; + case VAL_FLOAT: return v.f; + } + freearg(v); + v.setfloat(f); + return f; +} + +static inline int forceint(tagval &v) +{ + int i = 0; + switch(v.type) + { + case VAL_FLOAT: i = v.f; break; + case VAL_STR: i = parseint(v.s); break; + case VAL_MACRO: i = parseint(v.s); break; + case VAL_INT: return v.i; + } + freearg(v); + v.setint(i); + return i; +} + +static inline const char *forcestr(tagval &v) +{ + const char *s = ""; + switch(v.type) + { + case VAL_FLOAT: s = floatstr(v.f); break; + case VAL_INT: s = intstr(v.i); break; + case VAL_STR: case VAL_MACRO: return v.s; + } + freearg(v); + v.setstr(newstring(s)); + return s; +} + +static inline void forcearg(tagval &v, int type) +{ + switch(type) + { + case RET_STR: if(v.type != VAL_STR) forcestr(v); break; + case RET_INT: if(v.type != VAL_INT) forceint(v); break; + case RET_FLOAT: if(v.type != VAL_FLOAT) forcefloat(v); break; + } +} + +static inline ident *forceident(tagval &v) +{ + switch(v.type) + { + case VAL_IDENT: return v.id; + case VAL_MACRO: + { + ident *id = newident(v.s, IDF_UNKNOWN); + v.setident(id); + return id; + } + case VAL_STR: + { + ident *id = newident(v.s, IDF_UNKNOWN); + delete[] v.s; + v.setident(id); + return id; + } + } + freearg(v); + v.setident(dummyident); + return dummyident; +} + +void tagval::cleanup() +{ + freearg(*this); +} + +static inline void freeargs(tagval *args, int &oldnum, int newnum) +{ + for(int i = newnum; i < oldnum; i++) freearg(args[i]); + oldnum = newnum; +} + +static inline void cleancode(ident &id) +{ + if(id.code) + { + id.code[0] -= 0x100; + if(int(id.code[0]) < 0x100) delete[] id.code; + id.code = NULL; + } +} + +struct nullval : tagval +{ + nullval() { setnull(); } +} nullval; +tagval noret = nullval, *commandret = &noret; + +void clear_command() +{ + enumerate(idents, ident, i, + { + if(i.type==ID_ALIAS) + { + DELETEA(i.name); + i.forcenull(); + DELETEA(i.code); + } + }); +} + +void clearoverride(ident &i) +{ + if(!(i.flags&IDF_OVERRIDDEN)) return; + switch(i.type) + { + case ID_ALIAS: + if(i.valtype==VAL_STR) + { + if(!i.val.s[0]) break; + delete[] i.val.s; + } + cleancode(i); + i.valtype = VAL_STR; + i.val.s = newstring(""); + break; + case ID_VAR: + *i.storage.i = i.overrideval.i; + i.changed(); + break; + case ID_FVAR: + *i.storage.f = i.overrideval.f; + i.changed(); + break; + case ID_SVAR: + delete[] *i.storage.s; + *i.storage.s = i.overrideval.s; + i.changed(); + break; + } + i.flags &= ~IDF_OVERRIDDEN; +} + +void clearoverrides() +{ + enumerate(idents, ident, i, clearoverride(i)); +} + +static bool initedidents = false; +static vector *identinits = NULL; + +static inline ident *addident(const ident &id) +{ + if(!initedidents) + { + if(!identinits) identinits = new vector; + identinits->add(id); + return NULL; + } + ident &def = idents.access(id.name, id); + def.index = identmap.length(); + return identmap.add(&def); +} + +static bool initidents() +{ + initedidents = true; + for(int i = 0; i < MAXARGS; i++) + { + defformatstring(argname, "arg%d", i+1); + newident(argname, IDF_ARG); + } + dummyident = newident("//dummy", IDF_UNKNOWN); + if(identinits) + { + loopv(*identinits) addident((*identinits)[i]); + DELETEP(identinits); + } + return true; +} +UNUSED static bool forceinitidents = initidents(); + +static const char *sourcefile = NULL, *sourcestr = NULL; + +static const char *debugline(const char *p, const char *fmt) +{ + if(!sourcestr) return fmt; + int num = 1; + const char *line = sourcestr; + for(;;) + { + const char *end = strchr(line, '\n'); + if(!end) end = line + strlen(line); + if(p >= line && p <= end) + { + static string buf; + if(sourcefile) formatstring(buf, "%s:%d: %s", sourcefile, num, fmt); + else formatstring(buf, "%d: %s", num, fmt); + return buf; + } + if(!*end) break; + line = end + 1; + num++; + } + return fmt; +} + +static struct identlink +{ + ident *id; + identlink *next; + int usedargs; + identstack *argstack; +} noalias = { NULL, NULL, (1<next) total++; + for(identlink *l = aliasstack; l != &noalias; l = l->next) + { + ident *id = l->id; + ++depth; + if(depth < dbgalias) conoutf(CON_ERROR, " %d) %s", total-depth+1, id->name); + else if(l->next == &noalias) conoutf(CON_ERROR, depth == dbgalias ? " %d) %s" : " ..%d) %s", total-depth+1, id->name); + } +} + +static int nodebug = 0; + +static void debugcode(const char *fmt, ...) PRINTFARGS(1, 2); + +static void debugcode(const char *fmt, ...) +{ + if(nodebug) return; + + va_list args; + va_start(args, fmt); + conoutfv(CON_ERROR, fmt, args); + va_end(args); + + debugalias(); +} + +static void debugcodeline(const char *p, const char *fmt, ...) PRINTFARGS(2, 3); + +static void debugcodeline(const char *p, const char *fmt, ...) +{ + if(nodebug) return; + + va_list args; + va_start(args, fmt); + conoutfv(CON_ERROR, debugline(p, fmt), args); + va_end(args); + + debugalias(); +} + +ICOMMAND(nodebug, "e", (uint *body), { nodebug++; executeret(body, *commandret); nodebug--; }); + +void addident(ident *id) +{ + addident(*id); +} + +static inline void pusharg(ident &id, const tagval &v, identstack &stack) +{ + stack.val = id.val; + stack.valtype = id.valtype; + stack.next = id.stack; + id.stack = &stack; + id.setval(v); + cleancode(id); +} + +static inline void poparg(ident &id) +{ + if(!id.stack) return; + identstack *stack = id.stack; + if(id.valtype == VAL_STR) delete[] id.val.s; + id.setval(*stack); + cleancode(id); + id.stack = stack->next; +} + +ICOMMAND(push, "rte", (ident *id, tagval *v, uint *code), +{ + if(id->type != ID_ALIAS || id->index < MAXARGS) return; + identstack stack; + pusharg(*id, *v, stack); + v->type = VAL_NULL; + id->flags &= ~IDF_UNKNOWN; + executeret(code, *commandret); + poparg(*id); +}); + +static inline void pushalias(ident &id, identstack &stack) +{ + if(id.type == ID_ALIAS && id.index >= MAXARGS) + { + pusharg(id, nullval, stack); + id.flags &= ~IDF_UNKNOWN; + } +} + +static inline void popalias(ident &id) +{ + if(id.type == ID_ALIAS && id.index >= MAXARGS) poparg(id); +} + +KEYWORD(local, ID_LOCAL); + +static inline bool checknumber(const char *s) +{ + if(isdigit(s[0])) return true; + else switch(s[0]) + { + case '+': case '-': return isdigit(s[1]) || (s[1] == '.' && isdigit(s[2])); + case '.': return isdigit(s[1]) != 0; + default: return false; + } +} + +ident *newident(const char *name, int flags) +{ + ident *id = idents.access(name); + if(!id) + { + if(checknumber(name)) + { + debugcode("number %s is not a valid identifier name", name); + return dummyident; + } + id = addident(ident(ID_ALIAS, newstring(name), flags)); + } + return id; +} + +ident *writeident(const char *name, int flags) +{ + ident *id = newident(name, flags); + if(id->index < MAXARGS && !(aliasstack->usedargs&(1<index))) + { + pusharg(*id, nullval, aliasstack->argstack[id->index]); + aliasstack->usedargs |= 1<index; + } + return id; +} + +ident *readident(const char *name) +{ + ident *id = idents.access(name); + if(id && id->index < MAXARGS && !(aliasstack->usedargs&(1<index))) + return NULL; + return id; +} + +void resetvar(char *name) +{ + ident *id = idents.access(name); + if(!id) return; + if(id->flags&IDF_READONLY) debugcode("variable %s is read-only", id->name); + else clearoverride(*id); +} + +COMMAND(resetvar, "s"); + +static inline void setarg(ident &id, tagval &v) +{ + if(aliasstack->usedargs&(1<argstack[id.index]); + aliasstack->usedargs |= 1<type == ID_ALIAS) + { + if(id->index < MAXARGS) setarg(*id, v); else setalias(*id, v); + } + else + { + debugcode("cannot redefine builtin %s with an alias", id->name); + freearg(v); + } + } + else if(checknumber(name)) + { + debugcode("cannot alias number %s", name); + freearg(v); + } + else + { + addident(ident(ID_ALIAS, newstring(name), v, identflags)); + } +} + +void alias(const char *name, const char *str) +{ + tagval v; + v.setstr(newstring(str)); + setalias(name, v); +} + +void alias(const char *name, tagval &v) +{ + setalias(name, v); +} + +ICOMMAND(alias, "st", (const char *name, tagval *v), +{ + setalias(name, *v); + v->type = VAL_NULL; +}); + +// variable's and commands are registered through globals, see cube.h + +int variable(const char *name, int min, int cur, int max, int *storage, identfun fun, int flags) +{ + addident(ident(ID_VAR, name, min, max, storage, (void *)fun, flags)); + return cur; +} + +float fvariable(const char *name, float min, float cur, float max, float *storage, identfun fun, int flags) +{ + addident(ident(ID_FVAR, name, min, max, storage, (void *)fun, flags)); + return cur; +} + +char *svariable(const char *name, const char *cur, char **storage, identfun fun, int flags) +{ + addident(ident(ID_SVAR, name, storage, (void *)fun, flags)); + return newstring(cur); +} + +#define _GETVAR(id, vartype, name, retval) \ + ident *id = idents.access(name); \ + if(!id || id->type!=vartype) return retval; +#define GETVAR(id, name, retval) _GETVAR(id, ID_VAR, name, retval) +#define OVERRIDEVAR(errorval, saveval, resetval, clearval) \ + if(identflags&IDF_OVERRIDDEN || id->flags&IDF_OVERRIDE) \ + { \ + if(id->flags&IDF_PERSIST) \ + { \ + debugcode("cannot override persistent variable %s", id->name); \ + errorval; \ + } \ + if(!(id->flags&IDF_OVERRIDDEN)) { saveval; id->flags |= IDF_OVERRIDDEN; } \ + else { clearval; } \ + } \ + else \ + { \ + if(id->flags&IDF_OVERRIDDEN) { resetval; id->flags &= ~IDF_OVERRIDDEN; } \ + clearval; \ + } + +void setvar(const char *name, int i, bool dofunc, bool doclamp) +{ + GETVAR(id, name, ); + OVERRIDEVAR(return, id->overrideval.i = *id->storage.i, , ) + if(doclamp) *id->storage.i = clamp(i, id->minval, id->maxval); + else *id->storage.i = i; + if(dofunc) id->changed(); +} +void setfvar(const char *name, float f, bool dofunc, bool doclamp) +{ + _GETVAR(id, ID_FVAR, name, ); + OVERRIDEVAR(return, id->overrideval.f = *id->storage.f, , ); + if(doclamp) *id->storage.f = clamp(f, id->minvalf, id->maxvalf); + else *id->storage.f = f; + if(dofunc) id->changed(); +} +void setsvar(const char *name, const char *str, bool dofunc) +{ + _GETVAR(id, ID_SVAR, name, ); + OVERRIDEVAR(return, id->overrideval.s = *id->storage.s, delete[] id->overrideval.s, delete[] *id->storage.s); + *id->storage.s = newstring(str); + if(dofunc) id->changed(); +} +int getvar(const char *name) +{ + GETVAR(id, name, 0); + return *id->storage.i; +} +int getvarmin(const char *name) +{ + GETVAR(id, name, 0); + return id->minval; +} +int getvarmax(const char *name) +{ + GETVAR(id, name, 0); + return id->maxval; +} +float getfvarmin(const char *name) +{ + _GETVAR(id, ID_FVAR, name, 0); + return id->minvalf; +} +float getfvarmax(const char *name) +{ + _GETVAR(id, ID_FVAR, name, 0); + return id->maxvalf; +} + +ICOMMAND(getvarmin, "s", (char *s), intret(getvarmin(s))); +ICOMMAND(getvarmax, "s", (char *s), intret(getvarmax(s))); +ICOMMAND(getfvarmin, "s", (char *s), floatret(getfvarmin(s))); +ICOMMAND(getfvarmax, "s", (char *s), floatret(getfvarmax(s))); + +bool identexists(const char *name) { return idents.access(name)!=NULL; } +ident *getident(const char *name) { return idents.access(name); } + +void touchvar(const char *name) +{ + ident *id = idents.access(name); + if(id) switch(id->type) + { + case ID_VAR: + case ID_FVAR: + case ID_SVAR: + id->changed(); + break; + } +} + +const char *getalias(const char *name) +{ + ident *i = idents.access(name); + return i && i->type==ID_ALIAS && (i->index >= MAXARGS || aliasstack->usedargs&(1<index)) ? i->getstr() : ""; +} + +ICOMMAND(getalias, "s", (char *s), result(getalias(s))); + +int clampvar(ident *id, int val, int minval, int maxval) +{ + if(val < minval) val = minval; + else if(val > maxval) val = maxval; + else return val; + debugcode(id->flags&IDF_HEX ? + (minval <= 255 ? "valid range for %s is %d..0x%X" : "valid range for %s is 0x%X..0x%X") : + "valid range for %s is %d..%d", + id->name, minval, maxval); + return val; +} + +void setvarchecked(ident *id, int val) +{ + if(id->flags&IDF_READONLY) debugcode("variable %s is read-only", id->name); +#ifndef STANDALONE + else if(!(id->flags&IDF_OVERRIDE) || identflags&IDF_OVERRIDDEN || game::allowedittoggle()) +#else + else +#endif + { + OVERRIDEVAR(return, id->overrideval.i = *id->storage.i, , ) + if(val < id->minval || val > id->maxval) val = clampvar(id, val, id->minval, id->maxval); + *id->storage.i = val; + id->changed(); // call trigger function if available +#ifndef STANDALONE + if(id->flags&IDF_OVERRIDE && !(identflags&IDF_OVERRIDDEN)) game::vartrigger(id); +#endif + } +} + +static inline void setvarchecked(ident *id, tagval *args, int numargs) +{ + int val = forceint(args[0]); + if(id->flags&IDF_HEX && numargs > 1) + { + val = (val << 16) | (forceint(args[1])<<8); + if(numargs > 2) val |= forceint(args[2]); + } + setvarchecked(id, val); +} + +float clampfvar(ident *id, float val, float minval, float maxval) +{ + if(val < minval) val = minval; + else if(val > maxval) val = maxval; + else return val; + debugcode("valid range for %s is %s..%s", id->name, floatstr(minval), floatstr(maxval)); + return val; +} + +void setfvarchecked(ident *id, float val) +{ + if(id->flags&IDF_READONLY) debugcode("variable %s is read-only", id->name); +#ifndef STANDALONE + else if(!(id->flags&IDF_OVERRIDE) || identflags&IDF_OVERRIDDEN || game::allowedittoggle()) +#else + else +#endif + { + OVERRIDEVAR(return, id->overrideval.f = *id->storage.f, , ); + if(val < id->minvalf || val > id->maxvalf) val = clampfvar(id, val, id->minvalf, id->maxvalf); + *id->storage.f = val; + id->changed(); +#ifndef STANDALONE + if(id->flags&IDF_OVERRIDE && !(identflags&IDF_OVERRIDDEN)) game::vartrigger(id); +#endif + } +} + +void setsvarchecked(ident *id, const char *val) +{ + if(id->flags&IDF_READONLY) debugcode("variable %s is read-only", id->name); +#ifndef STANDALONE + else if(!(id->flags&IDF_OVERRIDE) || identflags&IDF_OVERRIDDEN || game::allowedittoggle()) +#else + else +#endif + { + OVERRIDEVAR(return, id->overrideval.s = *id->storage.s, delete[] id->overrideval.s, delete[] *id->storage.s); + *id->storage.s = newstring(val); + id->changed(); +#ifndef STANDALONE + if(id->flags&IDF_OVERRIDE && !(identflags&IDF_OVERRIDDEN)) game::vartrigger(id); +#endif + } +} + +ICOMMAND(set, "rt", (ident *id, tagval *v), +{ + switch(id->type) + { + case ID_ALIAS: + if(id->index < MAXARGS) setarg(*id, *v); else setalias(*id, *v); + v->type = VAL_NULL; + break; + case ID_VAR: + setvarchecked(id, forceint(*v)); + break; + case ID_FVAR: + setfvarchecked(id, forcefloat(*v)); + break; + case ID_SVAR: + setsvarchecked(id, forcestr(*v)); + break; + case ID_COMMAND: + if(id->flags&IDF_EMUVAR) + { + execute(id, v, 1); + v->type = VAL_NULL; + break; + } + // fall through + default: + debugcode("cannot redefine builtin %s with an alias", id->name); + break; + } +}); + +bool addcommand(const char *name, identfun fun, const char *args) +{ + uint argmask = 0; + int numargs = 0, flags = 0; + bool limit = true; + for(const char *fmt = args; *fmt; fmt++) switch(*fmt) + { + case 'i': case 'b': case 'f': case 't': case 'N': case 'D': if(numargs < MAXARGS) numargs++; break; + case '$': flags |= IDF_EMUVAR; // fall through + case 's': case 'e': case 'r': if(numargs < MAXARGS) { argmask |= 1< MAXCOMARGS) fatal("builtin %s declared with too many args: %d", name, numargs); + addident(ident(ID_COMMAND, name, args, argmask, numargs, (void *)fun, flags)); + return false; +} + +bool addkeyword(int type, const char *name) +{ + addident(ident(type, name, "", 0, 0, NULL)); + return true; +} + +const char *parsestring(const char *p) +{ + for(; *p; p++) switch(*p) + { + case '\r': + case '\n': + case '\"': + return p; + case '^': + if(*++p) break; + return p; + } + return p; +} + +int unescapestring(char *dst, const char *src, const char *end) +{ + char *start = dst; + while(src < end) + { + int c = *src++; + if(c == '^') + { + if(src >= end) break; + int e = *src++; + switch(e) + { + case 'n': *dst++ = '\n'; break; + case 't': *dst++ = '\t'; break; + case 'f': *dst++ = '\f'; break; + default: *dst++ = e; break; + } + } + else *dst++ = c; + } + return dst - start; +} + +static char *conc(vector &buf, tagval *v, int n, bool space, const char *prefix = NULL, int prefixlen = 0) +{ + if(prefix) + { + buf.put(prefix, prefixlen); + if(space && n) buf.add(' '); + } + loopi(n) + { + const char *s = ""; + int len = 0; + switch(v[i].type) + { + case VAL_INT: s = intstr(v[i].i); break; + case VAL_FLOAT: s = floatstr(v[i].f); break; + case VAL_STR: s = v[i].s; break; + case VAL_MACRO: s = v[i].s; len = v[i].code[-1]>>8; goto haslen; + } + len = int(strlen(s)); + haslen: + buf.put(s, len); + if(i == n-1) break; + if(space) buf.add(' '); + } + buf.add('\0'); + return buf.getbuf(); +} + +static char *conc(tagval *v, int n, bool space, const char *prefix, int prefixlen) +{ + static int vlen[MAXARGS]; + static char numbuf[3*MAXSTRLEN]; + int len = prefixlen, numlen = 0, i = 0; + for(; i < n; i++) switch(v[i].type) + { + case VAL_MACRO: len += (vlen[i] = v[i].code[-1]>>8); break; + case VAL_STR: len += (vlen[i] = int(strlen(v[i].s))); break; + case VAL_INT: + if(numlen + MAXSTRLEN > int(sizeof(numbuf))) goto overflow; + intformat(&numbuf[numlen], v[i].i); + numlen += (vlen[i] = strlen(&numbuf[numlen])); + break; + case VAL_FLOAT: + if(numlen + MAXSTRLEN > int(sizeof(numbuf))) goto overflow; + floatformat(&numbuf[numlen], v[i].f); + numlen += (vlen[i] = strlen(&numbuf[numlen])); + break; + default: vlen[i] = 0; break; + } +overflow: + if(space) len += max(prefix ? i : i-1, 0); + char *buf = newstring(len + numlen); + int offset = 0, numoffset = 0; + if(prefix) + { + memcpy(buf, prefix, prefixlen); + offset += prefixlen; + if(space && i) buf[offset++] = ' '; + } + loopj(i) + { + if(v[j].type == VAL_INT || v[j].type == VAL_FLOAT) + { + memcpy(&buf[offset], &numbuf[numoffset], vlen[j]); + numoffset += vlen[j]; + } + else if(vlen[j]) memcpy(&buf[offset], v[j].s, vlen[j]); + offset += vlen[j]; + if(j==i-1) break; + if(space) buf[offset++] = ' '; + } + buf[offset] = '\0'; + if(i < n) + { + char *morebuf = conc(&v[i], n-i, space, buf, offset); + delete[] buf; + return morebuf; + } + return buf; +} + +static inline char *conc(tagval *v, int n, bool space) +{ + return conc(v, n, space, NULL, 0); +} + +static inline char *conc(tagval *v, int n, bool space, const char *prefix) +{ + return conc(v, n, space, prefix, strlen(prefix)); +} + +static inline void skipcomments(const char *&p) +{ + for(;;) + { + p += strspn(p, " \t\r"); + if(p[0]!='/' || p[1]!='/') break; + p += strcspn(p, "\n\0"); + } +} + +static inline char *cutstring(const char *&p, int &len) +{ + p++; + const char *end = parsestring(p); + char *s = newstring(end - p); + len = unescapestring(s, p, end); + s[len] = '\0'; + p = end; + if(*p=='\"') p++; + return s; +} + +static inline const char *parseword(const char *p) +{ + const int maxbrak = 100; + static char brakstack[maxbrak]; + int brakdepth = 0; + for(;; p++) + { + p += strcspn(p, "\"/;()[] \t\r\n\0"); + switch(p[0]) + { + case '"': case ';': case ' ': case '\t': case '\r': case '\n': case '\0': return p; + case '/': if(p[1] == '/') return p; break; + case '[': case '(': if(brakdepth >= maxbrak) return p; brakstack[brakdepth++] = p[0]; break; + case ']': if(brakdepth <= 0 || brakstack[--brakdepth] != '[') return p; break; + case ')': if(brakdepth <= 0 || brakstack[--brakdepth] != '(') return p; break; + } + } + return p; +} + +static inline char *cutword(const char *&p, int &len) +{ + const char *word = p; + p = parseword(p); + len = p-word; + if(!len) return NULL; + return newstring(word, len); +} + +static inline void compilestr(vector &code, const char *word, int len, bool macro = false) +{ + if(len <= 3 && !macro) + { + uint op = CODE_VALI|RET_STR; + for(int i = 0; i < len; i++) op |= uint(uchar(word[i]))<<((i+1)*8); + code.add(op); + return; + } + code.add((macro ? CODE_MACRO : CODE_VAL|RET_STR)|(len<<8)); + code.put((const uint *)word, len/sizeof(uint)); + size_t endlen = len%sizeof(uint); + union + { + char c[sizeof(uint)]; + uint u; + } end; + end.u = 0; + memcpy(end.c, word + len - endlen, endlen); + code.add(end.u); +} + +static inline void compilestr(vector &code, const char *word = NULL) +{ + if(!word) { code.add(CODE_VALI|RET_STR); return; } + compilestr(code, word, int(strlen(word))); +} + +static inline void compileint(vector &code, int i) +{ + if(i >= -0x800000 && i <= 0x7FFFFF) + code.add(CODE_VALI|RET_INT|(i<<8)); + else + { + code.add(CODE_VAL|RET_INT); + code.add(i); + } +} + +static inline void compilenull(vector &code) +{ + code.add(CODE_VALI|RET_NULL); +} + +static inline void compileblock(vector &code) +{ + int start = code.length(); + code.add(CODE_BLOCK); + code.add(CODE_OFFSET|((start+2)<<8)); + code.add(CODE_EXIT); + code[start] |= uint(code.length() - (start + 1))<<8; +} + +static inline void compileident(vector &code, ident *id) +{ + code.add((id->index < MAXARGS ? CODE_IDENTARG : CODE_IDENT)|(id->index<<8)); +} + +static inline void compileident(vector &code, const char *word = NULL) +{ + compileident(code, word ? newident(word, IDF_UNKNOWN) : dummyident); +} + +static inline void compileint(vector &code, const char *word = NULL) +{ + return compileint(code, word ? parseint(word) : 0); +} + +static inline void compilefloat(vector &code, float f) +{ + if(int(f) == f && f >= -0x800000 && f <= 0x7FFFFF) + code.add(CODE_VALI|RET_FLOAT|(int(f)<<8)); + else + { + union { float f; uint u; } conv; + conv.f = f; + code.add(CODE_VAL|RET_FLOAT); + code.add(conv.u); + } +} + +static inline void compilefloat(vector &code, const char *word = NULL) +{ + return compilefloat(code, word ? parsefloat(word) : 0.0f); +} + +static bool compilearg(vector &code, const char *&p, int wordtype); +static void compilestatements(vector &code, const char *&p, int rettype, int brak = '\0'); + +static inline void compileval(vector &code, int wordtype, char *word, int wordlen) +{ + switch(wordtype) + { + case VAL_STR: compilestr(code, word, wordlen, true); break; + case VAL_ANY: compilestr(code, word, wordlen); break; + case VAL_FLOAT: compilefloat(code, word); break; + case VAL_INT: compileint(code, word); break; + case VAL_CODE: + { + int start = code.length(); + code.add(CODE_BLOCK); + code.add(CODE_OFFSET|((start+2)<<8)); + const char *p = word; + if(p) compilestatements(code, p, VAL_ANY); + code.add(CODE_EXIT|RET_STR); + code[start] |= uint(code.length() - (start + 1))<<8; + break; + } + case VAL_IDENT: compileident(code, word); break; + default: + break; + } +} + +static bool compileword(vector &code, const char *&p, int wordtype, char *&word, int &wordlen); + +static void compilelookup(vector &code, const char *&p, int ltype) +{ + char *lookup = NULL; + int lookuplen = 0; + switch(*++p) + { + case '(': + case '[': + if(!compileword(code, p, VAL_STR, lookup, lookuplen)) goto invalid; + break; + case '$': + compilelookup(code, p, VAL_STR); + break; + case '\"': + lookup = cutstring(p, lookuplen); + goto lookupid; + default: + { + lookup = cutword(p, lookuplen); + if(!lookup) goto invalid; + lookupid: + ident *id = newident(lookup, IDF_UNKNOWN); + if(id) switch(id->type) + { + case ID_VAR: code.add(CODE_IVAR|((ltype >= VAL_ANY ? VAL_INT : ltype)<index<<8)); goto done; + case ID_FVAR: code.add(CODE_FVAR|((ltype >= VAL_ANY ? VAL_FLOAT : ltype)<index<<8)); goto done; + case ID_SVAR: code.add(CODE_SVAR|((ltype >= VAL_ANY ? VAL_STR : ltype)<index<<8)); goto done; + case ID_ALIAS: code.add((id->index < MAXARGS ? CODE_LOOKUPARG : CODE_LOOKUP)|((ltype >= VAL_ANY ? VAL_STR : ltype)<index<<8)); goto done; + case ID_COMMAND: + { + int comtype = CODE_COM, numargs = 0; + code.add(CODE_ENTER); + for(const char *fmt = id->args; *fmt; fmt++) switch(*fmt) + { + case 's': compilestr(code, NULL, 0, true); numargs++; break; + case 'i': compileint(code); numargs++; break; + case 'b': compileint(code, INT_MIN); numargs++; break; + case 'f': compilefloat(code); numargs++; break; + case 't': compilenull(code); numargs++; break; + case 'e': compileblock(code); numargs++; break; + case 'r': compileident(code); numargs++; break; + case '$': compileident(code, id); numargs++; break; + case 'N': compileint(code, -1); numargs++; break; +#ifndef STANDALONE + case 'D': comtype = CODE_COMD; numargs++; break; +#endif + case 'C': comtype = CODE_COMC; numargs = 1; goto endfmt; + case 'V': comtype = CODE_COMV; numargs = 2; goto endfmt; + case '1': case '2': case '3': case '4': break; + } + endfmt: + code.add(comtype|(ltype < VAL_ANY ? ltype<index<<8)); + code.add(CODE_EXIT|(ltype < VAL_ANY ? ltype< &code, const char *str, const char *end, bool macro) +{ + int start = code.length(); + code.add(macro ? CODE_MACRO : CODE_VAL|RET_STR); + char *buf = (char *)code.reserve((end-str)/sizeof(uint)+1).buf; + int len = 0; + while(str < end) + { + int n = strcspn(str, "\r/\"@]\0"); + memcpy(&buf[len], str, n); + len += n; + str += n; + switch(*str) + { + case '\r': str++; break; + case '\"': + { + const char *start = str; + str = parsestring(str+1); + if(*str=='\"') str++; + memcpy(&buf[len], start, str-start); + len += str-start; + break; + } + case '/': + if(str[1] == '/') + { + size_t comment = strcspn(str, "\n\0"); + if (iscubepunct(str[2])) + { + memcpy(&buf[len], str, comment); + len += comment; + } + str += comment; + } + else buf[len++] = *str++; + break; + case '@': + case ']': + if(str < end) { buf[len++] = *str++; break; } + case '\0': goto done; + } + } +done: + memset(&buf[len], '\0', sizeof(uint)-len%sizeof(uint)); + code.advance(len/sizeof(uint)+1); + code[start] |= len<<8; + return true; +} + +static bool compileblocksub(vector &code, const char *&p) +{ + char *lookup = NULL; + int lookuplen = 0; + switch(*p) + { + case '(': + if(!compilearg(code, p, VAL_STR)) return false; + break; + case '[': + if(!compilearg(code, p, VAL_STR)) return false; + code.add(CODE_LOOKUPU|RET_STR); + break; + case '\"': + lookup = cutstring(p, lookuplen); + goto lookupid; + default: + { + { + const char *start = p; + while(iscubealnum(*p) || *p=='_') p++; + lookuplen = p-start; + if(!lookuplen) return false; + lookup = newstring(start, lookuplen); + } + lookupid: + ident *id = newident(lookup, IDF_UNKNOWN); + if(id) switch(id->type) + { + case ID_VAR: code.add(CODE_IVAR|RET_STR|(id->index<<8)); goto done; + case ID_FVAR: code.add(CODE_FVAR|RET_STR|(id->index<<8)); goto done; + case ID_SVAR: code.add(CODE_SVAR|RET_STR|(id->index<<8)); goto done; + case ID_ALIAS: code.add((id->index < MAXARGS ? CODE_LOOKUPARG : CODE_LOOKUP)|RET_STR|(id->index<<8)); goto done; + } + compilestr(code, lookup, lookuplen, true); + code.add(CODE_LOOKUPU|RET_STR); + done: + delete[] lookup; + break; + } + } + return true; +} + +static void compileblock(vector &code, const char *&p, int wordtype) +{ + const char *line = p, *start = p; + int concs = 0; + for(int brak = 1; brak;) + { + p += strcspn(p, "@\"/[]\0"); + int c = *p++; + switch(c) + { + case '\0': + debugcodeline(line, "missing \"]\""); + p--; + goto done; + case '\"': + p = parsestring(p); + if(*p=='\"') p++; + break; + case '/': + if(*p=='/') p += strcspn(p, "\n\0"); + break; + case '[': brak++; break; + case ']': brak--; break; + case '@': + { + const char *esc = p; + while(*p == '@') p++; + int level = p - (esc - 1); + if(brak > level) continue; + else if(brak < level) debugcodeline(line, "too many @s"); + if(!concs) code.add(CODE_ENTER); + if(concs + 2 > MAXARGS) + { + code.add(CODE_CONCW|RET_STR|(concs<<8)); + concs = 1; + } + if(compileblockstr(code, start, esc-1, true)) concs++; + if(compileblocksub(code, p)) concs++; + if(!concs) code.pop(); + else start = p; + break; + } + } + } +done: + if(p-1 > start) + { + if(!concs) switch(wordtype) + { + case VAL_CODE: + { + p = start; + int inst = code.length(); + code.add(CODE_BLOCK); + code.add(CODE_OFFSET|((inst+2)<<8)); + compilestatements(code, p, VAL_ANY, ']'); + code.add(CODE_EXIT); + code[inst] |= uint(code.length() - (inst + 1))<<8; + return; + } + case VAL_IDENT: + { + char *name = newstring(start, p-1-start); + compileident(code, name); + delete[] name; + return; + } + } + compileblockstr(code, start, p-1, concs > 0); + if(concs > 1) concs++; + } + if(concs) + { + code.add(CODE_CONCM|(wordtype < VAL_ANY ? wordtype< &code, const char *&p, int wordtype, char *&word, int &wordlen) +{ + skipcomments(p); + switch(*p) + { + case '\"': word = cutstring(p, wordlen); break; + case '$': compilelookup(code, p, wordtype); return true; + case '(': + p++; + code.add(CODE_ENTER); + compilestatements(code, p, VAL_ANY, ')'); + code.add(CODE_EXIT|(wordtype < VAL_ANY ? wordtype< &code, const char *&p, int wordtype) +{ + char *word = NULL; + int wordlen = 0; + bool more = compileword(code, p, wordtype, word, wordlen); + if(!more) return false; + if(word) + { + compileval(code, wordtype, word, wordlen); + delete[] word; + } + return true; +} + +static void compilestatements(vector &code, const char *&p, int rettype, int brak) +{ + const char *line = p; + char *idname = NULL; + int idlen = 0; + ident *id = NULL; + int numargs = 0; + for(;;) + { + skipcomments(p); + idname = NULL; + bool more = compileword(code, p, VAL_ANY, idname, idlen); + if(!more) goto endstatement; + skipcomments(p); + if(p[0] == '=') switch(p[1]) + { + case '/': + if(p[2] != '/') break; + case ';': case ' ': case '\t': case '\r': case '\n': case '\0': + p++; + if(idname) + { + id = newident(idname, IDF_UNKNOWN); + if(id) switch(id->type) + { + case ID_ALIAS: + if(!(more = compilearg(code, p, VAL_ANY))) compilestr(code); + code.add((id->index < MAXARGS ? CODE_ALIASARG : CODE_ALIAS)|(id->index<<8)); + goto endcommand; + case ID_VAR: + if(!(more = compilearg(code, p, VAL_INT))) compileint(code); + code.add(CODE_IVAR1|(id->index<<8)); + goto endcommand; + case ID_FVAR: + if(!(more = compilearg(code, p, VAL_FLOAT))) compilefloat(code); + code.add(CODE_FVAR1|(id->index<<8)); + goto endcommand; + case ID_SVAR: + if(!(more = compilearg(code, p, VAL_STR))) compilestr(code); + code.add(CODE_SVAR1|(id->index<<8)); + goto endcommand; + case ID_COMMAND: + if(id->flags&IDF_EMUVAR) goto compilecommand; + break; + } + compilestr(code, idname, idlen, true); + delete[] idname; + } + if(!(more = compilearg(code, p, VAL_ANY))) compilestr(code); + code.add(CODE_ALIASU); + goto endstatement; + } + compilecommand: + numargs = 0; + if(!idname) + { + noid: + while(numargs < MAXARGS && (more = compilearg(code, p, VAL_ANY))) numargs++; + code.add(CODE_CALLU); + } + else + { + id = idents.access(idname); + if(!id) + { + if(!checknumber(idname)) { compilestr(code, idname, idlen); delete[] idname; goto noid; } + char *end = idname; + int val = int(strtoul(idname, &end, 0)); + if(*end) compilestr(code, idname, idlen); + else compileint(code, val); + code.add(CODE_RESULT); + } + else switch(id->type) + { + case ID_ALIAS: + while(numargs < MAXARGS && (more = compilearg(code, p, VAL_ANY))) numargs++; + code.add((id->index < MAXARGS ? CODE_CALLARG : CODE_CALL)|(id->index<<8)); + break; + case ID_COMMAND: + { + int comtype = CODE_COM, fakeargs = 0; + bool rep = false; + for(const char *fmt = id->args; *fmt; fmt++) switch(*fmt) + { + case 's': + if(more) more = compilearg(code, p, VAL_STR); + if(!more) + { + if(rep) break; + compilestr(code, NULL, 0, true); + fakeargs++; + } + else if(!fmt[1]) + { + int numconc = 0; + while(numargs + numconc < MAXARGS && (more = compilearg(code, p, VAL_STR))) numconc++; + if(numconc > 0) code.add(CODE_CONC|RET_STR|((numconc+1)<<8)); + } + numargs++; + break; + case 'i': if(more) more = compilearg(code, p, VAL_INT); if(!more) { if(rep) break; compileint(code); fakeargs++; } numargs++; break; + case 'b': if(more) more = compilearg(code, p, VAL_INT); if(!more) { if(rep) break; compileint(code, INT_MIN); fakeargs++; } numargs++; break; + case 'f': if(more) more = compilearg(code, p, VAL_FLOAT); if(!more) { if(rep) break; compilefloat(code); fakeargs++; } numargs++; break; + case 't': if(more) more = compilearg(code, p, VAL_ANY); if(!more) { if(rep) break; compilenull(code); fakeargs++; } numargs++; break; + case 'e': if(more) more = compilearg(code, p, VAL_CODE); if(!more) { if(rep) break; compileblock(code); fakeargs++; } numargs++; break; + case 'r': if(more) more = compilearg(code, p, VAL_IDENT); if(!more) { if(rep) break; compileident(code); fakeargs++; } numargs++; break; + case '$': compileident(code, id); numargs++; break; + case 'N': compileint(code, numargs-fakeargs); numargs++; break; +#ifndef STANDALONE + case 'D': comtype = CODE_COMD; numargs++; break; +#endif + case 'C': comtype = CODE_COMC; if(more) while(numargs < MAXARGS && (more = compilearg(code, p, VAL_ANY))) numargs++; numargs = 1; goto endfmt; + case 'V': comtype = CODE_COMV; if(more) while(numargs < MAXARGS && (more = compilearg(code, p, VAL_ANY))) numargs++; numargs = 2; goto endfmt; + case '1': case '2': case '3': case '4': + if(more && numargs < MAXARGS) + { + int numrep = *fmt-'0'+1; + fmt -= numrep; + rep = true; + } + else for(; numargs > MAXARGS; numargs--) code.add(CODE_POP); + break; + } + endfmt: + code.add(comtype|(rettype < VAL_ANY ? rettype<index<<8)); + break; + } + case ID_LOCAL: + if(more) while(numargs < MAXARGS && (more = compilearg(code, p, VAL_IDENT))) numargs++; + if(more) while((more = compilearg(code, p, VAL_ANY))) code.add(CODE_POP); + code.add(CODE_LOCAL); + break; + case ID_VAR: + if(!(more = compilearg(code, p, VAL_INT))) code.add(CODE_PRINT|(id->index<<8)); + else if(!(id->flags&IDF_HEX) || !(more = compilearg(code, p, VAL_INT))) code.add(CODE_IVAR1|(id->index<<8)); + else if(!(more = compilearg(code, p, VAL_INT))) code.add(CODE_IVAR2|(id->index<<8)); + else code.add(CODE_IVAR3|(id->index<<8)); + break; + case ID_FVAR: + if(!(more = compilearg(code, p, VAL_FLOAT))) code.add(CODE_PRINT|(id->index<<8)); + else code.add(CODE_FVAR1|(id->index<<8)); + break; + case ID_SVAR: + if(!(more = compilearg(code, p, VAL_STR))) code.add(CODE_PRINT|(id->index<<8)); + else + { + int numconc = 0; + while(numconc+1 < MAXARGS && (more = compilearg(code, p, VAL_ANY))) numconc++; + if(numconc > 0) code.add(CODE_CONC|RET_STR|((numconc+1)<<8)); + code.add(CODE_SVAR1|(id->index<<8)); + } + break; + } + endcommand: + delete[] idname; + } + endstatement: + if(more) while(compilearg(code, p, VAL_ANY)) code.add(CODE_POP); + p += strcspn(p, ")];/\n\0"); + int c = *p++; + switch(c) + { + case '\0': + if(c != brak) debugcodeline(line, "missing \"%c\"", brak); + p--; + return; + + case ')': + case ']': + if(c == brak) return; + debugcodeline(line, "unexpected \"%c\"", c); + break; + + case '/': + if(*p == '/') p += strcspn(p, "\n\0"); + goto endstatement; + } + } +} + +static void compilemain(vector &code, const char *p, int rettype = VAL_ANY) +{ + code.add(CODE_START); + compilestatements(code, p, VAL_ANY); + code.add(CODE_EXIT|(rettype < VAL_ANY ? rettype< buf; + buf.reserve(64); + compilemain(buf, p); + uint *code = new uint[buf.length()]; + memcpy(code, buf.getbuf(), buf.length()*sizeof(uint)); + code[0] += 0x100; + return code; +} + +void keepcode(uint *code) +{ + if(!code) return; + switch(*code&CODE_OP_MASK) + { + case CODE_START: + *code += 0x100; + return; + } + switch(code[-1]&CODE_OP_MASK) + { + case CODE_START: + code[-1] += 0x100; + break; + case CODE_OFFSET: + code -= int(code[-1]>>8); + *code += 0x100; + break; + } +} + +void freecode(uint *code) +{ + if(!code) return; + switch(*code&CODE_OP_MASK) + { + case CODE_START: + *code -= 0x100; + if(int(*code) < 0x100) delete[] code; + return; + } + switch(code[-1]&CODE_OP_MASK) + { + case CODE_START: + code[-1] -= 0x100; + if(int(code[-1]) < 0x100) delete[] &code[-1]; + break; + case CODE_OFFSET: + code -= int(code[-1]>>8); + *code -= 0x100; + if(int(*code) < 0x100) delete[] code; + break; + } +} + +void printvar(ident *id, int i) +{ + if(i < 0) conoutf(CON_INFO, id->index, "%s = %d", id->name, i); + else if(id->flags&IDF_HEX && id->maxval==0xFFFFFF) + conoutf(CON_INFO, id->index, "%s = 0x%.6X (%d, %d, %d)", id->name, i, (i>>16)&0xFF, (i>>8)&0xFF, i&0xFF); + else + conoutf(CON_INFO, id->index, id->flags&IDF_HEX ? "%s = 0x%X" : "%s = %d", id->name, i); +} + +void printfvar(ident *id, float f) +{ + conoutf(CON_INFO, id->index, "%s = %s", id->name, floatstr(f)); +} + +void printsvar(ident *id, const char *s) +{ + conoutf(CON_INFO, id->index, strchr(s, '"') ? "%s = [%s]" : "%s = \"%s\"", id->name, s); +} + +template +static void printvar(ident *id, int type, V &val) +{ + switch(type) + { + case VAL_INT: printvar(id, val.getint()); break; + case VAL_FLOAT: printfvar(id, val.getfloat()); break; + default: printsvar(id, val.getstr()); break; + } +} + +void printvar(ident *id) +{ + switch(id->type) + { + case ID_VAR: printvar(id, *id->storage.i); break; + case ID_FVAR: printfvar(id, *id->storage.f); break; + case ID_SVAR: printsvar(id, *id->storage.s); break; + case ID_ALIAS: printvar(id, id->valtype, *id); break; + case ID_COMMAND: + if(id->flags&IDF_EMUVAR) + { + tagval result; + executeret(id, NULL, 0, true, result); + printvar(id, result.type, result); + freearg(result); + } + break; + } +} +ICOMMAND(printvar, "r", (ident *id), printvar(id)); + +typedef void (__cdecl *comfun)(); +typedef void (__cdecl *comfun1)(void *); +typedef void (__cdecl *comfun2)(void *, void *); +typedef void (__cdecl *comfun3)(void *, void *, void *); +typedef void (__cdecl *comfun4)(void *, void *, void *, void *); +typedef void (__cdecl *comfun5)(void *, void *, void *, void *, void *); +typedef void (__cdecl *comfun6)(void *, void *, void *, void *, void *, void *); +typedef void (__cdecl *comfun7)(void *, void *, void *, void *, void *, void *, void *); +typedef void (__cdecl *comfun8)(void *, void *, void *, void *, void *, void *, void *, void *); +typedef void (__cdecl *comfun9)(void *, void *, void *, void *, void *, void *, void *, void *, void *); +typedef void (__cdecl *comfun10)(void *, void *, void *, void *, void *, void *, void *, void *, void *, void *); +typedef void (__cdecl *comfun11)(void *, void *, void *, void *, void *, void *, void *, void *, void *, void *, void *); +typedef void (__cdecl *comfun12)(void *, void *, void *, void *, void *, void *, void *, void *, void *, void *, void *, void *); +typedef void (__cdecl *comfunv)(tagval *, int); + +static const uint *skipcode(const uint *code, tagval &result) +{ + int depth = 0; + for(;;) + { + uint op = *code++; + switch(op&0xFF) + { + case CODE_MACRO: + case CODE_VAL|RET_STR: + { + uint len = op>>8; + code += len/sizeof(uint) + 1; + continue; + } + case CODE_BLOCK: + { + uint len = op>>8; + code += len; + continue; + } + case CODE_ENTER: + ++depth; + continue; + case CODE_EXIT|RET_NULL: case CODE_EXIT|RET_STR: case CODE_EXIT|RET_INT: case CODE_EXIT|RET_FLOAT: + if(depth <= 0) + { + forcearg(result, op&CODE_RET_MASK); + return code; + } + --depth; + continue; + } + } +} + +static inline void callcommand(ident *id, tagval *args, int numargs, bool lookup = false) +{ + int i = -1, fakeargs = 0; + bool rep = false; + for(const char *fmt = id->args; *fmt; fmt++) switch(*fmt) + { + case 'i': if(++i >= numargs) { if(rep) break; args[i].setint(0); fakeargs++; } else forceint(args[i]); break; + case 'b': if(++i >= numargs) { if(rep) break; args[i].setint(INT_MIN); fakeargs++; } else forceint(args[i]); break; + case 'f': if(++i >= numargs) { if(rep) break; args[i].setfloat(0.0f); fakeargs++; } else forcefloat(args[i]); break; + case 's': if(++i >= numargs) { if(rep) break; args[i].setstr(newstring("")); fakeargs++; } else forcestr(args[i]); break; + case 't': if(++i >= numargs) { if(rep) break; args[i].setnull(); fakeargs++; } break; + case 'e': + if(++i >= numargs) + { + if(rep) break; + static uint buf[2] = { CODE_START + 0x100, CODE_EXIT }; + args[i].setcode(buf); + fakeargs++; + } + else + { + vector buf; + buf.reserve(64); + compilemain(buf, numargs <= i ? "" : args[i].getstr()); + freearg(args[i]); + args[i].setcode(buf.getbuf()+1); + buf.disown(); + } + break; + case 'r': if(++i >= numargs) { if(rep) break; args[i].setident(dummyident); fakeargs++; } else forceident(args[i]); break; + case '$': if(++i < numargs) freearg(args[i]); args[i].setident(id); break; + case 'N': if(++i < numargs) freearg(args[i]); args[i].setint(lookup ? -1 : i-fakeargs); break; +#ifndef STANDALONE + case 'D': if(++i < numargs) freearg(args[i]); args[i].setint(addreleaseaction(conc(args, i, true, id->name)) ? 1 : 0); fakeargs++; break; +#endif + case 'C': { i = max(i+1, numargs); vector buf; ((comfun1)id->fun)(conc(buf, args, i, true)); goto cleanup; } + case 'V': i = max(i+1, numargs); ((comfunv)id->fun)(args, i); goto cleanup; + case '1': case '2': case '3': case '4': if(i+1 < numargs) { fmt -= *fmt-'0'+1; rep = true; } break; + } + #define ARG(n) (id->argmask&(1<fun)(); break; \ + case 1: ((comfun1)id->fun)(ARG(0)); break; \ + case 2: ((comfun2)id->fun)(ARG(0), ARG(1)); break; \ + case 3: ((comfun3)id->fun)(ARG(0), ARG(1), ARG(2)); break; \ + case 4: ((comfun4)id->fun)(ARG(0), ARG(1), ARG(2), ARG(3)); break; \ + case 5: ((comfun5)id->fun)(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4)); break; \ + case 6: ((comfun6)id->fun)(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4), ARG(5)); break; \ + case 7: ((comfun7)id->fun)(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4), ARG(5), ARG(6)); break; \ + case 8: ((comfun8)id->fun)(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4), ARG(5), ARG(6), ARG(7)); break; \ + case 9: ((comfun9)id->fun)(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4), ARG(5), ARG(6), ARG(7), ARG(8)); break; \ + case 10: ((comfun10)id->fun)(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4), ARG(5), ARG(6), ARG(7), ARG(8), ARG(9)); break; \ + case 11: ((comfun11)id->fun)(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4), ARG(5), ARG(6), ARG(7), ARG(8), ARG(9), ARG(10)); break; \ + case 12: ((comfun12)id->fun)(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4), ARG(5), ARG(6), ARG(7), ARG(8), ARG(9), ARG(10), ARG(11)); break; \ + } + ++i; + CALLCOM(i) +cleanup: + loopk(i) freearg(args[k]); + for(; i < numargs; i++) freearg(args[i]); +} + +#define MAXRUNDEPTH 255 +static int rundepth = 0; + +static const uint *runcode(const uint *code, tagval &result) +{ + result.setnull(); + if(rundepth >= MAXRUNDEPTH) + { + debugcode("exceeded recursion limit"); + return skipcode(code, result); + } + ++rundepth; + ident *id = NULL; + int numargs = 0; + tagval args[MAXARGS+1], *prevret = commandret; + commandret = &result; + for(;;) + { + uint op = *code++; + switch(op&0xFF) + { + case CODE_START: case CODE_OFFSET: continue; + + case CODE_POP: + freearg(args[--numargs]); + continue; + case CODE_ENTER: + code = runcode(code, args[numargs++]); + continue; + case CODE_EXIT|RET_NULL: case CODE_EXIT|RET_STR: case CODE_EXIT|RET_INT: case CODE_EXIT|RET_FLOAT: + forcearg(result, op&CODE_RET_MASK); + goto exit; + case CODE_PRINT: + printvar(identmap[op>>8]); + continue; + case CODE_LOCAL: + { + identstack locals[MAXARGS]; + freearg(result); + loopi(numargs) pushalias(*args[i].id, locals[i]); + code = runcode(code, result); + loopi(numargs) popalias(*args[i].id); + goto exit; + } + + case CODE_MACRO: + { + uint len = op>>8; + args[numargs++].setmacro(code); + code += len/sizeof(uint) + 1; + continue; + } + + case CODE_VAL|RET_STR: + { + uint len = op>>8; + args[numargs++].setstr(newstring((const char *)code, len)); + code += len/sizeof(uint) + 1; + continue; + } + case CODE_VALI|RET_STR: + { + char s[4] = { char((op>>8)&0xFF), char((op>>16)&0xFF), char((op>>24)&0xFF), '\0' }; + args[numargs++].setstr(newstring(s)); + continue; + } + case CODE_VAL|RET_NULL: + case CODE_VALI|RET_NULL: args[numargs++].setnull(); continue; + case CODE_VAL|RET_INT: args[numargs++].setint(int(*code++)); continue; + case CODE_VALI|RET_INT: args[numargs++].setint(int(op)>>8); continue; + case CODE_VAL|RET_FLOAT: args[numargs++].setfloat(*(const float *)code++); continue; + case CODE_VALI|RET_FLOAT: args[numargs++].setfloat(float(int(op)>>8)); continue; + + case CODE_FORCE|RET_STR: forcestr(args[numargs-1]); continue; + case CODE_FORCE|RET_INT: forceint(args[numargs-1]); continue; + case CODE_FORCE|RET_FLOAT: forcefloat(args[numargs-1]); continue; + + case CODE_RESULT|RET_NULL: case CODE_RESULT|RET_STR: case CODE_RESULT|RET_INT: case CODE_RESULT|RET_FLOAT: + litval: + freearg(result); + result = args[0]; + forcearg(result, op&CODE_RET_MASK); + args[0].setnull(); + freeargs(args, numargs, 0); + continue; + + case CODE_BLOCK: + { + uint len = op>>8; + args[numargs++].setcode(code+1); + code += len; + continue; + } + case CODE_COMPILE: + { + tagval &arg = args[numargs-1]; + vector buf; + switch(arg.type) + { + case VAL_INT: buf.reserve(8); buf.add(CODE_START); compileint(buf, arg.i); buf.add(CODE_RESULT); buf.add(CODE_EXIT); break; + case VAL_FLOAT: buf.reserve(8); buf.add(CODE_START); compilefloat(buf, arg.f); buf.add(CODE_RESULT); buf.add(CODE_EXIT); break; + case VAL_STR: case VAL_MACRO: buf.reserve(64); compilemain(buf, arg.s); freearg(arg); break; + default: buf.reserve(8); buf.add(CODE_START); compilenull(buf); buf.add(CODE_RESULT); buf.add(CODE_EXIT); break; + } + arg.setcode(buf.getbuf()+1); + buf.disown(); + continue; + } + + case CODE_IDENT: + args[numargs++].setident(identmap[op>>8]); + continue; + case CODE_IDENTARG: + { + ident *id = identmap[op>>8]; + if(!(aliasstack->usedargs&(1<index))) + { + pusharg(*id, nullval, aliasstack->argstack[id->index]); + aliasstack->usedargs |= 1<index; + } + args[numargs++].setident(id); + continue; + } + case CODE_IDENTU: + { + tagval &arg = args[numargs-1]; + ident *id = arg.type == VAL_STR || arg.type == VAL_MACRO ? newident(arg.s, IDF_UNKNOWN) : dummyident; + if(id->index < MAXARGS && !(aliasstack->usedargs&(1<index))) + { + pusharg(*id, nullval, aliasstack->argstack[id->index]); + aliasstack->usedargs |= 1<index; + } + freearg(arg); + arg.setident(id); + continue; + } + + case CODE_LOOKUPU|RET_STR: + #define LOOKUPU(aval, sval, ival, fval, nval) { \ + tagval &arg = args[numargs-1]; \ + if(arg.type != VAL_STR && arg.type != VAL_MACRO) continue; \ + id = idents.access(arg.s); \ + if(id) switch(id->type) \ + { \ + case ID_ALIAS: \ + if(id->flags&IDF_UNKNOWN) break; \ + freearg(arg); \ + if(id->index < MAXARGS && !(aliasstack->usedargs&(1<index))) { nval; continue; } \ + aval; \ + continue; \ + case ID_SVAR: freearg(arg); sval; continue; \ + case ID_VAR: freearg(arg); ival; continue; \ + case ID_FVAR: freearg(arg); fval; continue; \ + case ID_COMMAND: \ + { \ + freearg(arg); \ + arg.setnull(); \ + commandret = &arg; \ + tagval buf[MAXARGS]; \ + callcommand(id, buf, 0, true); \ + forcearg(arg, op&CODE_RET_MASK); \ + commandret = &result; \ + continue; \ + } \ + default: freearg(arg); nval; continue; \ + } \ + debugcode("unknown alias lookup: %s", arg.s); \ + freearg(arg); \ + nval; \ + continue; \ + } + LOOKUPU(arg.setstr(newstring(id->getstr())), + arg.setstr(newstring(*id->storage.s)), + arg.setstr(newstring(intstr(*id->storage.i))), + arg.setstr(newstring(floatstr(*id->storage.f))), + arg.setstr(newstring(""))); + case CODE_LOOKUP|RET_STR: + #define LOOKUP(aval) { \ + id = identmap[op>>8]; \ + if(id->flags&IDF_UNKNOWN) debugcode("unknown alias lookup: %s", id->name); \ + aval; \ + continue; \ + } + LOOKUP(args[numargs++].setstr(newstring(id->getstr()))); + case CODE_LOOKUPARG|RET_STR: + #define LOOKUPARG(aval, nval) { \ + id = identmap[op>>8]; \ + if(!(aliasstack->usedargs&(1<index))) { nval; continue; } \ + aval; \ + continue; \ + } + LOOKUPARG(args[numargs++].setstr(newstring(id->getstr())), args[numargs++].setstr(newstring(""))); + case CODE_LOOKUPU|RET_INT: + LOOKUPU(arg.setint(id->getint()), + arg.setint(parseint(*id->storage.s)), + arg.setint(*id->storage.i), + arg.setint(int(*id->storage.f)), + arg.setint(0)); + case CODE_LOOKUP|RET_INT: + LOOKUP(args[numargs++].setint(id->getint())); + case CODE_LOOKUPARG|RET_INT: + LOOKUPARG(args[numargs++].setint(id->getint()), args[numargs++].setint(0)); + case CODE_LOOKUPU|RET_FLOAT: + LOOKUPU(arg.setfloat(id->getfloat()), + arg.setfloat(parsefloat(*id->storage.s)), + arg.setfloat(float(*id->storage.i)), + arg.setfloat(*id->storage.f), + arg.setfloat(0.0f)); + case CODE_LOOKUP|RET_FLOAT: + LOOKUP(args[numargs++].setfloat(id->getfloat())); + case CODE_LOOKUPARG|RET_FLOAT: + LOOKUPARG(args[numargs++].setfloat(id->getfloat()), args[numargs++].setfloat(0.0f)); + case CODE_LOOKUPU|RET_NULL: + LOOKUPU(id->getval(arg), + arg.setstr(newstring(*id->storage.s)), + arg.setint(*id->storage.i), + arg.setfloat(*id->storage.f), + arg.setnull()); + case CODE_LOOKUP|RET_NULL: + LOOKUP(id->getval(args[numargs++])); + case CODE_LOOKUPARG|RET_NULL: + LOOKUPARG(id->getval(args[numargs++]), args[numargs++].setnull()); + + case CODE_SVAR|RET_STR: case CODE_SVAR|RET_NULL: args[numargs++].setstr(newstring(*identmap[op>>8]->storage.s)); continue; + case CODE_SVAR|RET_INT: args[numargs++].setint(parseint(*identmap[op>>8]->storage.s)); continue; + case CODE_SVAR|RET_FLOAT: args[numargs++].setfloat(parsefloat(*identmap[op>>8]->storage.s)); continue; + case CODE_SVAR1: setsvarchecked(identmap[op>>8], args[0].s); freeargs(args, numargs, 0); continue; + + case CODE_IVAR|RET_INT: case CODE_IVAR|RET_NULL: args[numargs++].setint(*identmap[op>>8]->storage.i); continue; + case CODE_IVAR|RET_STR: args[numargs++].setstr(newstring(intstr(*identmap[op>>8]->storage.i))); continue; + case CODE_IVAR|RET_FLOAT: args[numargs++].setfloat(float(*identmap[op>>8]->storage.i)); continue; + case CODE_IVAR1: setvarchecked(identmap[op>>8], args[0].i); numargs = 0; continue; + case CODE_IVAR2: setvarchecked(identmap[op>>8], (args[0].i<<16)|(args[1].i<<8)); numargs = 0; continue; + case CODE_IVAR3: setvarchecked(identmap[op>>8], (args[0].i<<16)|(args[1].i<<8)|args[2].i); numargs = 0; continue; + + case CODE_FVAR|RET_FLOAT: case CODE_FVAR|RET_NULL: args[numargs++].setfloat(*identmap[op>>8]->storage.f); continue; + case CODE_FVAR|RET_STR: args[numargs++].setstr(newstring(floatstr(*identmap[op>>8]->storage.f))); continue; + case CODE_FVAR|RET_INT: args[numargs++].setint(int(*identmap[op>>8]->storage.f)); continue; + case CODE_FVAR1: setfvarchecked(identmap[op>>8], args[0].f); numargs = 0; continue; + + case CODE_COM|RET_NULL: case CODE_COM|RET_STR: case CODE_COM|RET_FLOAT: case CODE_COM|RET_INT: + id = identmap[op>>8]; +#ifndef STANDALONE + callcom: +#endif + forcenull(result); + CALLCOM(numargs) + forceresult: + freeargs(args, numargs, 0); + forcearg(result, op&CODE_RET_MASK); + continue; +#ifndef STANDALONE + case CODE_COMD|RET_NULL: case CODE_COMD|RET_STR: case CODE_COMD|RET_FLOAT: case CODE_COMD|RET_INT: + id = identmap[op>>8]; + args[numargs].setint(addreleaseaction(conc(args, numargs, true, id->name)) ? 1 : 0); + numargs++; + goto callcom; +#endif + case CODE_COMV|RET_NULL: case CODE_COMV|RET_STR: case CODE_COMV|RET_FLOAT: case CODE_COMV|RET_INT: + id = identmap[op>>8]; + forcenull(result); + ((comfunv)id->fun)(args, numargs); + goto forceresult; + case CODE_COMC|RET_NULL: case CODE_COMC|RET_STR: case CODE_COMC|RET_FLOAT: case CODE_COMC|RET_INT: + id = identmap[op>>8]; + forcenull(result); + { + vector buf; + buf.reserve(MAXSTRLEN); + ((comfun1)id->fun)(conc(buf, args, numargs, true)); + } + goto forceresult; + + case CODE_CONC|RET_NULL: case CODE_CONC|RET_STR: case CODE_CONC|RET_FLOAT: case CODE_CONC|RET_INT: + case CODE_CONCW|RET_NULL: case CODE_CONCW|RET_STR: case CODE_CONCW|RET_FLOAT: case CODE_CONCW|RET_INT: + { + int numconc = op>>8; + char *s = conc(&args[numargs-numconc], numconc, (op&CODE_OP_MASK)==CODE_CONC); + freeargs(args, numargs, numargs-numconc); + args[numargs++].setstr(s); + forcearg(args[numargs-1], op&CODE_RET_MASK); + continue; + } + + case CODE_CONCM|RET_NULL: case CODE_CONCM|RET_STR: case CODE_CONCM|RET_FLOAT: case CODE_CONCM|RET_INT: + { + int numconc = op>>8; + char *s = conc(&args[numargs-numconc], numconc, false); + freeargs(args, numargs, numargs-numconc); + result.setstr(s); + forcearg(result, op&CODE_RET_MASK); + continue; + } + + case CODE_ALIAS: + setalias(*identmap[op>>8], args[--numargs]); + freeargs(args, numargs, 0); + continue; + case CODE_ALIASARG: + setarg(*identmap[op>>8], args[--numargs]); + freeargs(args, numargs, 0); + continue; + case CODE_ALIASU: + forcestr(args[0]); + setalias(args[0].s, args[--numargs]); + freeargs(args, numargs, 0); + continue; + + case CODE_CALL|RET_NULL: case CODE_CALL|RET_STR: case CODE_CALL|RET_FLOAT: case CODE_CALL|RET_INT: + #define CALLALIAS(offset) { \ + identstack argstack[MAXARGS]; \ + for(int i = 0; i < numargs-offset; i++) \ + pusharg(*identmap[i], args[i+offset], argstack[i]); \ + int oldargs = _numargs, newargs = numargs-offset; \ + _numargs = newargs; \ + int oldflags = identflags; \ + identflags |= id->flags&IDF_OVERRIDDEN; \ + identlink aliaslink = { id, aliasstack, (1<code) id->code = compilecode(id->getstr()); \ + uint *code = id->code; \ + code[0] += 0x100; \ + runcode(code+1, result); \ + code[0] -= 0x100; \ + if(int(code[0]) < 0x100) delete[] code; \ + aliasstack = aliaslink.next; \ + identflags = oldflags; \ + for(int i = 0; i < newargs; i++) \ + poparg(*identmap[i]); \ + for(int argmask = aliaslink.usedargs&(~0U<>8]; + if(id->flags&IDF_UNKNOWN) + { + debugcode("unknown command: %s", id->name); + goto forceresult; + } + CALLALIAS(0); + continue; + case CODE_CALLARG|RET_NULL: case CODE_CALLARG|RET_STR: case CODE_CALLARG|RET_FLOAT: case CODE_CALLARG|RET_INT: + forcenull(result); + id = identmap[op>>8]; + if(!(aliasstack->usedargs&(1<index))) goto forceresult; + CALLALIAS(0); + continue; + + case CODE_CALLU|RET_NULL: case CODE_CALLU|RET_STR: case CODE_CALLU|RET_FLOAT: case CODE_CALLU|RET_INT: + if(args[0].type != VAL_STR) goto litval; + id = idents.access(args[0].s); + if(!id) + { + noid: + if(checknumber(args[0].s)) goto litval; + debugcode("unknown command: %s", args[0].s); + forcenull(result); + goto forceresult; + } + forcenull(result); + switch(id->type) + { + case ID_COMMAND: + freearg(args[0]); + callcommand(id, args+1, numargs-1); + forcearg(result, op&CODE_RET_MASK); + numargs = 0; + continue; + case ID_LOCAL: + { + identstack locals[MAXARGS]; + freearg(args[0]); + loopj(numargs-1) pushalias(*forceident(args[j+1]), locals[j]); + code = runcode(code, result); + loopj(numargs-1) popalias(*args[j+1].id); + goto exit; + } + case ID_VAR: + if(numargs <= 1) printvar(id); else setvarchecked(id, &args[1], numargs-1); + goto forceresult; + case ID_FVAR: + if(numargs <= 1) printvar(id); else setfvarchecked(id, forcefloat(args[1])); + goto forceresult; + case ID_SVAR: + if(numargs <= 1) printvar(id); else setsvarchecked(id, forcestr(args[1])); + goto forceresult; + case ID_ALIAS: + if(id->index < MAXARGS && !(aliasstack->usedargs&(1<index))) goto forceresult; + if(id->valtype==VAL_NULL) goto noid; + freearg(args[0]); + CALLALIAS(1); + continue; + default: + goto forceresult; + } + } + } +exit: + commandret = prevret; + --rundepth; + return code; +} + +void executeret(const uint *code, tagval &result) +{ + runcode(code, result); +} + +void executeret(const char *p, tagval &result) +{ + vector code; + code.reserve(64); + compilemain(code, p, VAL_ANY); + runcode(code.getbuf()+1, result); + if(int(code[0]) >= 0x100) code.disown(); +} + +void executeret(ident *id, tagval *args, int numargs, bool lookup, tagval &result) +{ + result.setnull(); + ++rundepth; + tagval *prevret = commandret; + commandret = &result; + if(rundepth > MAXRUNDEPTH) debugcode("exceeded recursion limit"); + else if(id) switch(id->type) + { + default: + if(!id->fun) break; + // fall-through + case ID_COMMAND: + if(numargs < id->numargs) + { + tagval buf[MAXARGS]; + memcpy(buf, args, numargs*sizeof(tagval)); + callcommand(id, buf, numargs, lookup); + } + else callcommand(id, args, numargs, lookup); + numargs = 0; + break; + case ID_VAR: + if(numargs <= 0) printvar(id); else setvarchecked(id, args, numargs); + break; + case ID_FVAR: + if(numargs <= 0) printvar(id); else setfvarchecked(id, forcefloat(args[0])); + break; + case ID_SVAR: + if(numargs <= 0) printvar(id); else setsvarchecked(id, forcestr(args[0])); + break; + case ID_ALIAS: + if(id->index < MAXARGS && !(aliasstack->usedargs&(1<index))) break; + if(id->valtype==VAL_NULL) break; + #define op RET_NULL + CALLALIAS(0); + #undef op + break; + } + freeargs(args, numargs, 0); + commandret = prevret; + --rundepth; +} + +char *executestr(const uint *code) +{ + tagval result; + runcode(code, result); + if(result.type == VAL_NULL) return NULL; + forcestr(result); + return result.s; +} + +char *executestr(const char *p) +{ + tagval result; + executeret(p, result); + if(result.type == VAL_NULL) return NULL; + forcestr(result); + return result.s; +} + +char *executestr(ident *id, tagval *args, int numargs, bool lookup) +{ + tagval result; + executeret(id, args, numargs, lookup, result); + if(result.type == VAL_NULL) return NULL; + forcestr(result); + return result.s; +} + +char *execidentstr(const char *name, bool lookup) +{ + ident *id = idents.access(name); + return id ? executestr(id, NULL, 0, lookup) : NULL; +} + +int execute(const uint *code) +{ + tagval result; + runcode(code, result); + int i = result.getint(); + freearg(result); + return i; +} + +int execute(const char *p) +{ + vector code; + code.reserve(64); + compilemain(code, p, VAL_INT); + tagval result; + runcode(code.getbuf()+1, result); + if(int(code[0]) >= 0x100) code.disown(); + int i = result.getint(); + freearg(result); + return i; +} + +int execute(ident *id, tagval *args, int numargs, bool lookup) +{ + tagval result; + executeret(id, args, numargs, lookup, result); + int i = result.getint(); + freearg(result); + return i; +} + +int execident(const char *name, int noid, bool lookup) +{ + ident *id = idents.access(name); + return id ? execute(id, NULL, 0, lookup) : noid; +} + +static inline bool getbool(const char *s) +{ + switch(s[0]) + { + case '+': case '-': + switch(s[1]) + { + case '0': break; + case '.': return !isdigit(s[2]) || parsefloat(s) != 0; + default: return true; + } + // fall through + case '0': + { + char *end; + int val = int(strtoul((char *)s, &end, 0)); + if(val) return true; + switch(*end) + { + case 'e': case '.': return parsefloat(s) != 0; + default: return false; + } + } + case '.': return !isdigit(s[1]) || parsefloat(s) != 0; + case '\0': return false; + default: return true; + } +} + +static inline bool getbool(const tagval &v) +{ + switch(v.type) + { + case VAL_FLOAT: return v.f!=0; + case VAL_INT: return v.i!=0; + case VAL_STR: case VAL_MACRO: return getbool(v.s); + default: return false; + } +} + +bool executebool(const uint *code) +{ + tagval result; + runcode(code, result); + bool b = getbool(result); + freearg(result); + return b; +} + +bool executebool(const char *p) +{ + tagval result; + executeret(p, result); + bool b = getbool(result); + freearg(result); + return b; +} + +bool executebool(ident *id, tagval *args, int numargs, bool lookup) +{ + tagval result; + executeret(id, args, numargs, lookup, result); + bool b = getbool(result); + freearg(result); + return b; +} + +bool execidentbool(const char *name, bool noid, bool lookup) +{ + ident *id = idents.access(name); + return id ? executebool(id, NULL, 0, lookup) : noid; +} + +bool execfile(const char *cfgfile, bool msg) +{ + string s; + copystring(s, cfgfile); + char *buf = loadfile(path(s), NULL); + if(!buf) + { + if(msg) conoutf(CON_ERROR, "could not read \"%s\"", cfgfile); + return false; + } + const char *oldsourcefile = sourcefile, *oldsourcestr = sourcestr; + sourcefile = cfgfile; + sourcestr = buf; + execute(buf); + sourcefile = oldsourcefile; + sourcestr = oldsourcestr; + delete[] buf; + return true; +} +ICOMMAND(exec, "sb", (char *file, int *msg), intret(execfile(file, *msg != 0) ? 1 : 0)); + +const char *escapestring(const char *s) +{ + static vector strbuf[3]; + static int stridx = 0; + stridx = (stridx + 1)%3; + vector &buf = strbuf[stridx]; + buf.setsize(0); + buf.add('"'); + for(; *s; s++) switch(*s) + { + case '\n': buf.put("^n", 2); break; + case '\t': buf.put("^t", 2); break; + case '\f': buf.put("^f", 2); break; + case '"': buf.put("^\"", 2); break; + case '^': buf.put("^^", 2); break; + default: buf.add(*s); break; + } + buf.put("\"\0", 2); + return buf.getbuf(); +} + +ICOMMAND(escape, "s", (char *s), result(escapestring(s))); +ICOMMAND(unescape, "s", (char *s), +{ + int len = strlen(s); + char *d = newstring(len); + d[unescapestring(d, s, &s[len])] = '\0'; + stringret(d); +}); + +const char *escapeid(const char *s) +{ + const char *end = s + strcspn(s, "\"/;()[]@ \f\t\r\n\0"); + return *end ? escapestring(s) : s; +} + +bool validateblock(const char *s) +{ + const int maxbrak = 100; + static char brakstack[maxbrak]; + int brakdepth = 0; + for(; *s; s++) switch(*s) + { + case '[': case '(': if(brakdepth >= maxbrak) return false; brakstack[brakdepth++] = *s; break; + case ']': if(brakdepth <= 0 || brakstack[--brakdepth] != '[') return false; break; + case ')': if(brakdepth <= 0 || brakstack[--brakdepth] != '(') return false; break; + case '"': s = parsestring(s + 1); if(*s != '"') return false; break; + case '/': if(s[1] == '/') return false; break; + case '@': case '\f': return false; + } + return brakdepth == 0; +} + +#ifndef STANDALONE +void writecfg(const char *name) +{ + stream *f = openutf8file(path(name && name[0] ? name : game::savedconfig(), true), "w"); + if(!f) return; + f->printf("// automatically written on exit, DO NOT MODIFY\n// delete this file to have %s overwrite these settings\n// modify settings in game, or put settings in %s to override anything\n\n", game::defaultconfig(), game::autoexec()); + game::writeclientinfo(f); + f->printf("\n"); + writecrosshairs(f); + vector ids; + enumerate(idents, ident, id, ids.add(&id)); + ids.sortname(); + loopv(ids) + { + ident &id = *ids[i]; + if(id.flags&IDF_PERSIST) switch(id.type) + { + case ID_VAR: f->printf("%s %d\n", escapeid(id), *id.storage.i); break; + case ID_FVAR: f->printf("%s %s\n", escapeid(id), floatstr(*id.storage.f)); break; + case ID_SVAR: f->printf("%s %s\n", escapeid(id), escapestring(*id.storage.s)); break; + } + } + f->printf("\n"); + writebinds(f); + f->printf("\n"); + loopv(ids) + { + ident &id = *ids[i]; + if(id.type==ID_ALIAS && id.flags&IDF_PERSIST && !(id.flags&IDF_OVERRIDDEN)) switch(id.valtype) + { + case VAL_STR: + if(!id.val.s[0]) break; + if(!validateblock(id.val.s)) { f->printf("%s = %s\n", escapeid(id), escapestring(id.val.s)); break; } + case VAL_FLOAT: + case VAL_INT: + f->printf("%s = [%s]\n", escapeid(id), id.getstr()); break; + } + } + f->printf("\n"); + writecompletions(f); + delete f; +} + +COMMAND(writecfg, "s"); +#endif + +void changedvars() +{ + vector ids; + enumerate(idents, ident, id, if(id.flags&IDF_OVERRIDDEN) ids.add(&id)); + ids.sortname(); + loopv(ids) printvar(ids[i]); +} +COMMAND(changedvars, ""); + +// below the commands that implement a small imperative language. thanks to the semantics of +// () and [] expressions, any control construct can be defined trivially. + +static string retbuf[4]; +static int retidx = 0; + +const char *intstr(int v) +{ + retidx = (retidx + 1)%4; + intformat(retbuf[retidx], v); + return retbuf[retidx]; +} + +void intret(int v) +{ + commandret->setint(v); +} + +const char *floatstr(float v) +{ + retidx = (retidx + 1)%4; + floatformat(retbuf[retidx], v); + return retbuf[retidx]; +} + +void floatret(float v) +{ + commandret->setfloat(v); +} + +#undef ICOMMANDNAME +#define ICOMMANDNAME(name) _stdcmd + +ICOMMAND(do, "e", (uint *body), executeret(body, *commandret)); +ICOMMAND(if, "tee", (tagval *cond, uint *t, uint *f), executeret(getbool(*cond) ? t : f, *commandret)); +ICOMMAND(?, "ttt", (tagval *cond, tagval *t, tagval *f), result(*(getbool(*cond) ? t : f))); + +ICOMMAND(pushif, "rte", (ident *id, tagval *v, uint *code), +{ + if(id->type != ID_ALIAS || id->index < MAXARGS) return; + if(getbool(*v)) + { + identstack stack; + pusharg(*id, *v, stack); + v->type = VAL_NULL; + id->flags &= ~IDF_UNKNOWN; + executeret(code, *commandret); + poparg(*id); + } +}); + +void loopiter(ident *id, identstack &stack, const tagval &v) +{ + if(id->stack != &stack) + { + pusharg(*id, v, stack); + id->flags &= ~IDF_UNKNOWN; + } + else + { + if(id->valtype == VAL_STR) delete[] id->val.s; + cleancode(*id); + id->setval(v); + } +} + +void loopend(ident *id, identstack &stack) +{ + if(id->stack == &stack) poparg(*id); +} + +static inline void setiter(ident &id, int i, identstack &stack) +{ + if(id.stack == &stack) + { + if(id.valtype != VAL_INT) + { + if(id.valtype == VAL_STR) delete[] id.val.s; + cleancode(id); + id.valtype = VAL_INT; + } + id.val.i = i; + } + else + { + tagval t; + t.setint(i); + pusharg(id, t, stack); + id.flags &= ~IDF_UNKNOWN; + } +} +ICOMMAND(loop, "rie", (ident *id, int *n, uint *body), +{ + if(*n <= 0 || id->type!=ID_ALIAS) return; + identstack stack; + loopi(*n) + { + setiter(*id, i, stack); + execute(body); + } + poparg(*id); +}); +ICOMMAND(loopwhile, "riee", (ident *id, int *n, uint *cond, uint *body), +{ + if(*n <= 0 || id->type!=ID_ALIAS) return; + identstack stack; + loopi(*n) + { + setiter(*id, i, stack); + if(!executebool(cond)) break; + execute(body); + } + poparg(*id); +}); +ICOMMAND(while, "ee", (uint *cond, uint *body), while(executebool(cond)) execute(body)); + +char *loopconc(ident *id, int n, uint *body, bool space) +{ + identstack stack; + vector s; + loopi(n) + { + setiter(*id, i, stack); + tagval v; + executeret(body, v); + const char *vstr = v.getstr(); + int len = strlen(vstr); + if(space && i) s.add(' '); + s.put(vstr, len); + freearg(v); + } + if(n > 0) poparg(*id); + s.add('\0'); + return newstring(s.getbuf(), s.length()-1); +} + +ICOMMAND(loopconcat, "rie", (ident *id, int *n, uint *body), +{ + if(*n > 0 && id->type==ID_ALIAS) commandret->setstr(loopconc(id, *n, body, true)); +}); + +ICOMMAND(loopconcatword, "rie", (ident *id, int *n, uint *body), +{ + if(*n > 0 && id->type==ID_ALIAS) commandret->setstr(loopconc(id, *n, body, false)); +}); + +void concat(tagval *v, int n) +{ + commandret->setstr(conc(v, n, true)); +} +COMMAND(concat, "V"); + +void concatword(tagval *v, int n) +{ + commandret->setstr(conc(v, n, false)); +} +COMMAND(concatword, "V"); + +void append(ident *id, tagval *v, bool space) +{ + if(id->type != ID_ALIAS || v->type == VAL_NULL) return; + if(id->valtype == VAL_NULL) + { + noprefix: + if(id->index < MAXARGS) setarg(*id, *v); else setalias(*id, *v); + v->type = VAL_NULL; + } + else + { + const char *prefix = id->getstr(); + if(!prefix[0]) goto noprefix; + tagval r; + r.setstr(conc(v, 1, space, prefix)); + if(id->index < MAXARGS) setarg(*id, r); else setalias(*id, r); + } +} +ICOMMAND(append, "rt", (ident *id, tagval *v), append(id, v, true)); +ICOMMAND(appendword, "rt", (ident *id, tagval *v), append(id, v, false)); + +void result(tagval &v) +{ + *commandret = v; + v.type = VAL_NULL; +} + +void stringret(char *s) +{ + commandret->setstr(s); +} + +void result(const char *s) +{ + commandret->setstr(newstring(s)); +} + +ICOMMAND(result, "t", (tagval *v), +{ + *commandret = *v; + v->type = VAL_NULL; +}); + +void format(tagval *args, int numargs) +{ + vector s; + const char *f = args[0].getstr(); + while(*f) + { + int c = *f++; + if(c == '%') + { + int i = *f++; + if(i >= '1' && i <= '9') + { + i -= '0'; + const char *sub = i < numargs ? args[i].getstr() : ""; + while(*sub) s.add(*sub++); + } + else s.add(i); + } + else s.add(c); + } + s.add('\0'); + result(s.getbuf()); +} +COMMAND(format, "V"); + +static const char *liststart = NULL, *listend = NULL, *listquotestart = NULL, *listquoteend = NULL; + +static inline void skiplist(const char *&p) +{ + for(;;) + { + p += strspn(p, " \t\r\n"); + if(p[0]!='/' || p[1]!='/') break; + p += strcspn(p, "\n\0"); + } +} + +static bool parselist(const char *&s, const char *&start = liststart, const char *&end = listend, const char *"estart = listquotestart, const char *"eend = listquoteend) +{ + skiplist(s); + switch(*s) + { + case '"': quotestart = s++; start = s; s = parsestring(s); end = s; if(*s == '"') s++; quoteend = s; break; + case '(': case '[': + quotestart = s; + start = s+1; + for(int braktype = *s++, brak = 1;;) + { + s += strcspn(s, "\"/;()[]\0"); + int c = *s++; + switch(c) + { + case '\0': s--; quoteend = end = s; return true; + case '"': s = parsestring(s); if(*s == '"') s++; break; + case '/': if(*s == '/') s += strcspn(s, "\n\0"); break; + case '(': case '[': if(c == braktype) brak++; break; + case ')': if(braktype == '(' && --brak <= 0) goto endblock; break; + case ']': if(braktype == '[' && --brak <= 0) goto endblock; break; + } + } + endblock: + end = s-1; + quoteend = s; + break; + case '\0': case ')': case ']': return false; + default: quotestart = start = s; s = parseword(s); quoteend = end = s; break; + } + skiplist(s); + if(*s == ';') s++; + return true; +} + +void explodelist(const char *s, vector &elems, int limit) +{ + const char *start, *end; + while((limit < 0 || elems.length() < limit) && parselist(s, start, end)) + elems.add(newstring(start, end-start)); +} + +char *indexlist(const char *s, int pos) +{ + loopi(pos) if(!parselist(s)) return newstring(""); + const char *start, *end; + return parselist(s, start, end) ? newstring(start, end-start) : newstring(""); +} + +int listlen(const char *s) +{ + int n = 0; + while(parselist(s)) n++; + return n; +} +ICOMMAND(listlen, "s", (char *s), intret(listlen(s))); + +void at(tagval *args, int numargs) +{ + if(!numargs) return; + const char *start = args[0].getstr(), *end = start + strlen(start); + for(int i = 1; i < numargs; i++) + { + const char *list = start; + int pos = args[i].getint(); + for(; pos > 0; pos--) if(!parselist(list)) break; + if(pos > 0 || !parselist(list, start, end)) start = end = ""; + } + commandret->setstr(newstring(start, end-start)); +} +COMMAND(at, "si1V"); + +void substr(char *s, int *start, int *count, int *numargs) +{ + int len = strlen(s), offset = clamp(*start, 0, len); + commandret->setstr(newstring(&s[offset], *numargs >= 3 ? clamp(*count, 0, len - offset) : len - offset)); +} +COMMAND(substr, "siiN"); + +void chopstr(char *s, int *lim, char *ellipsis) +{ + int len = strlen(s), maxlen = abs(*lim); + if(len > maxlen) + { + int elen = strlen(ellipsis); + maxlen = max(maxlen, elen); + char *chopped = newstring(maxlen); + if(*lim < 0) + { + memcpy(chopped, ellipsis, elen); + memcpy(&chopped[elen], &s[len - (maxlen - elen)], maxlen - elen); + } + else + { + memcpy(chopped, s, maxlen - elen); + memcpy(&chopped[maxlen - elen], ellipsis, elen); + } + chopped[maxlen] = '\0'; + commandret->setstr(chopped); + } + else result(s); +} +COMMAND(chopstr, "sis"); + +void sublist(const char *s, int *skip, int *count, int *numargs) +{ + int offset = max(*skip, 0), len = *numargs >= 3 ? max(*count, 0) : -1; + loopi(offset) if(!parselist(s)) break; + if(len < 0) { if(offset > 0) skiplist(s); commandret->setstr(newstring(s)); return; } + const char *list = s, *start, *end, *qstart, *qend = s; + if(len > 0 && parselist(s, start, end, list, qend)) while(--len > 0 && parselist(s, start, end, qstart, qend)); + commandret->setstr(newstring(list, qend - list)); +} +COMMAND(sublist, "siiN"); + +ICOMMAND(stripcolors, "s", (char *s), +{ + int len = strlen(s); + char *d = newstring(len); + filtertext(d, s, true, false, len); + stringret(d); +}); + +static inline void setiter(ident &id, char *val, identstack &stack) +{ + if(id.stack == &stack) + { + if(id.valtype == VAL_STR) delete[] id.val.s; + else id.valtype = VAL_STR; + cleancode(id); + id.val.s = val; + } + else + { + tagval t; + t.setstr(val); + pusharg(id, t, stack); + id.flags &= ~IDF_UNKNOWN; + } +} + +void listfind(ident *id, const char *list, const uint *body) +{ + if(id->type!=ID_ALIAS) { intret(-1); return; } + identstack stack; + int n = -1; + for(const char *s = list, *start, *end; parselist(s, start, end);) + { + ++n; + char *val = newstring(start, end-start); + setiter(*id, val, stack); + if(executebool(body)) { intret(n); goto found; } + } + intret(-1); +found: + if(n >= 0) poparg(*id); +} +COMMAND(listfind, "rse"); + +void looplist(ident *id, const char *list, const uint *body) +{ + if(id->type!=ID_ALIAS) return; + identstack stack; + int n = 0; + for(const char *s = list, *start, *end; parselist(s, start, end); n++) + { + char *val = newstring(start, end-start); + setiter(*id, val, stack); + execute(body); + } + if(n) poparg(*id); +} +COMMAND(looplist, "rse"); + +void loopsublist(ident *id, const char *list, int *skip, int *count, const uint *body) +{ + if(id->type!=ID_ALIAS) return; + identstack stack; + int n = 0, offset = max(*skip, 0), len = *count < 0 ? INT_MAX : offset + *count; + for(const char *s = list, *start, *end; parselist(s, start, end) && n < len; n++) if(n >= offset) + { + char *val = newstring(start, end-start); + setiter(*id, val, stack); + execute(body); + } + if(n) poparg(*id); +} +COMMAND(loopsublist, "rsiie"); + +void looplistconc(ident *id, const char *list, const uint *body, bool space) +{ + if(id->type!=ID_ALIAS) return; + identstack stack; + vector r; + int n = 0; + for(const char *s = list, *start, *end; parselist(s, start, end); n++) + { + char *val = newstring(start, end-start); + setiter(*id, val, stack); + + if(n && space) r.add(' '); + + tagval v; + executeret(body, v); + const char *vstr = v.getstr(); + int len = strlen(vstr); + r.put(vstr, len); + freearg(v); + } + if(n) poparg(*id); + r.add('\0'); + commandret->setstr(newstring(r.getbuf(), r.length()-1)); +} +ICOMMAND(looplistconcat, "rse", (ident *id, char *list, uint *body), looplistconc(id, list, body, true)); +ICOMMAND(looplistconcatword, "rse", (ident *id, char *list, uint *body), looplistconc(id, list, body, false)); + +void listfilter(ident *id, const char *list, const uint *body) +{ + if(id->type!=ID_ALIAS) return; + identstack stack; + vector r; + int n = 0; + for(const char *s = list, *start, *end, *quotestart, *quoteend; parselist(s, start, end, quotestart, quoteend); n++) + { + char *val = newstring(start, end-start); + setiter(*id, val, stack); + + if(executebool(body)) + { + if(r.length()) r.add(' '); + r.put(quotestart, quoteend-quotestart); + } + } + if(n) poparg(*id); + r.add('\0'); + commandret->setstr(newstring(r.getbuf(), r.length()-1)); +} +COMMAND(listfilter, "rse"); + +void prettylist(const char *s, const char *conj) +{ + vector p; + const char *start, *end; + for(int len = listlen(s), n = 0; parselist(s, start, end); n++) + { + p.put(start, end - start); + if(n+1 < len) + { + if(len > 2 || !conj[0]) p.add(','); + if(n+2 == len && conj[0]) + { + p.add(' '); + p.put(conj, strlen(conj)); + } + p.add(' '); + } + } + p.add('\0'); + result(p.getbuf()); +} +COMMAND(prettylist, "ss"); + +int listincludes(const char *list, const char *needle, int needlelen) +{ + int offset = 0; + for(const char *s = list, *start, *end; parselist(s, start, end);) + { + int len = end - start; + if(needlelen == len && !strncmp(needle, start, len)) return offset; + offset++; + } + return -1; +} +ICOMMAND(indexof, "ss", (char *list, char *elem), intret(listincludes(list, elem, strlen(elem)))); + +char *listdel(const char *s, const char *del) +{ + vector p; + for(const char *start, *end, *qstart, *qend; parselist(s, start, end, qstart, qend);) + { + if(listincludes(del, start, end-start) < 0) + { + if(!p.empty()) p.add(' '); + p.put(qstart, qend-qstart); + } + } + p.add('\0'); + return newstring(p.getbuf(), p.length()-1); +} +ICOMMAND(listdel, "ss", (char *list, char *del), commandret->setstr(listdel(list, del))); + +void listsplice(const char *s, const char *vals, int *skip, int *count) +{ + int offset = max(*skip, 0), len = max(*count, 0); + const char *list = s, *start, *end, *qstart, *qend = s; + loopi(offset) if(!parselist(s, start, end, qstart, qend)) break; + vector p; + if(qend > list) p.put(list, qend-list); + if(*vals) + { + if(!p.empty()) p.add(' '); + p.put(vals, strlen(vals)); + } + loopi(len) if(!parselist(s)) break; + skiplist(s); + switch(*s) + { + case '\0': case ')': case ']': break; + default: + if(!p.empty()) p.add(' '); + p.put(s, strlen(s)); + break; + } + p.add('\0'); + commandret->setstr(newstring(p.getbuf(), p.length()-1)); +} +COMMAND(listsplice, "ssii"); + +ICOMMAND(loopfiles, "rsse", (ident *id, char *dir, char *ext, uint *body), +{ + if(id->type!=ID_ALIAS) return; + identstack stack; + vector files; + listfiles(dir, ext[0] ? ext : NULL, files); + loopvrev(files) + { + char *file = files[i]; + bool redundant = false; + loopj(i) if(!strcmp(files[j], file)) { redundant = true; break; } + if(redundant) delete[] files.removeunordered(i); + } + loopv(files) + { + char *file = files[i]; + if(i) + { + if(id->valtype == VAL_STR) delete[] id->val.s; + else id->valtype = VAL_STR; + id->val.s = file; + } + else + { + tagval t; + t.setstr(file); + pusharg(*id, t, stack); + id->flags &= ~IDF_UNKNOWN; + } + execute(body); + } + if(files.length()) poparg(*id); +}); + +void findfile_(char *name) +{ + string fname; + copystring(fname, name); + path(fname); + intret( +#ifndef STANDALONE + findzipfile(fname) || +#endif + fileexists(fname, "e") || findfile(fname, "e") ? 1 : 0 + ); +} +COMMANDN(findfile, findfile_, "s"); + +struct sortitem +{ + const char *str, *quotestart, *quoteend; +}; + +struct sortfun +{ + ident *x, *y; + uint *body; + + bool operator()(const sortitem &xval, const sortitem &yval) + { + if(x->valtype != VAL_MACRO) x->valtype = VAL_MACRO; + cleancode(*x); + x->val.code = (const uint *)xval.str; + if(y->valtype != VAL_MACRO) y->valtype = VAL_MACRO; + cleancode(*y); + y->val.code = (const uint *)yval.str; + return executebool(body); + } +}; + +void sortlist(char *list, ident *x, ident *y, uint *body) +{ + if(x == y || x->type != ID_ALIAS || y->type != ID_ALIAS) return; + + vector items; + int macrolen = strlen(list), total = 0; + char *macros = newstring(list, macrolen); + const char *curlist = list, *start, *end, *quotestart, *quoteend; + while(parselist(curlist, start, end, quotestart, quoteend)) + { + macros[end - list] = '\0'; + sortitem item = { ¯os[start - list], quotestart, quoteend }; + items.add(item); + total += int(quoteend - quotestart); + } + + identstack xstack, ystack; + pusharg(*x, nullval, xstack); x->flags &= ~IDF_UNKNOWN; + pusharg(*y, nullval, ystack); y->flags &= ~IDF_UNKNOWN; + + sortfun f = { x, y, body }; + items.sort(f); + + poparg(*x); + poparg(*y); + + char *sorted = macros; + int sortedlen = total + max(items.length() - 1, 0); + if(macrolen < sortedlen) + { + delete[] macros; + sorted = newstring(sortedlen); + } + + int offset = 0; + loopv(items) + { + sortitem &item = items[i]; + int len = int(item.quoteend - item.quotestart); + if(i) sorted[offset++] = ' '; + memcpy(&sorted[offset], item.quotestart, len); + offset += len; + } + sorted[offset] = '\0'; + + commandret->setstr(sorted); +} +COMMAND(sortlist, "srre"); + +ICOMMAND(+, "ii", (int *a, int *b), intret(*a + *b)); +ICOMMAND(*, "ii", (int *a, int *b), intret(*a * *b)); +ICOMMAND(-, "ii", (int *a, int *b), intret(*a - *b)); +ICOMMAND(+f, "ff", (float *a, float *b), floatret(*a + *b)); +ICOMMAND(*f, "ff", (float *a, float *b), floatret(*a * *b)); +ICOMMAND(-f, "ff", (float *a, float *b), floatret(*a - *b)); +ICOMMAND(=, "ii", (int *a, int *b), intret((int)(*a == *b))); +ICOMMAND(!=, "ii", (int *a, int *b), intret((int)(*a != *b))); +ICOMMAND(<, "ii", (int *a, int *b), intret((int)(*a < *b))); +ICOMMAND(>, "ii", (int *a, int *b), intret((int)(*a > *b))); +ICOMMAND(<=, "ii", (int *a, int *b), intret((int)(*a <= *b))); +ICOMMAND(>=, "ii", (int *a, int *b), intret((int)(*a >= *b))); +ICOMMAND(=f, "ff", (float *a, float *b), intret((int)(*a == *b))); +ICOMMAND(!=f, "ff", (float *a, float *b), intret((int)(*a != *b))); +ICOMMAND(f, "ff", (float *a, float *b), intret((int)(*a > *b))); +ICOMMAND(<=f, "ff", (float *a, float *b), intret((int)(*a <= *b))); +ICOMMAND(>=f, "ff", (float *a, float *b), intret((int)(*a >= *b))); +ICOMMAND(^, "ii", (int *a, int *b), intret(*a ^ *b)); +ICOMMAND(!, "t", (tagval *a), intret(!getbool(*a))); +ICOMMAND(&, "ii", (int *a, int *b), intret(*a & *b)); +ICOMMAND(|, "ii", (int *a, int *b), intret(*a | *b)); +ICOMMAND(~, "i", (int *a), intret(~*a)); +ICOMMAND(^~, "ii", (int *a, int *b), intret(*a ^ ~*b)); +ICOMMAND(&~, "ii", (int *a, int *b), intret(*a & ~*b)); +ICOMMAND(|~, "ii", (int *a, int *b), intret(*a | ~*b)); +ICOMMAND(<<, "ii", (int *a, int *b), intret(*b < 32 ? *a << max(*b, 0) : 0)); +ICOMMAND(>>, "ii", (int *a, int *b), intret(*a >> clamp(*b, 0, 31))); +ICOMMAND(&&, "e1V", (tagval *args, int numargs), +{ + if(!numargs) intret(1); + else loopi(numargs) + { + if(i) freearg(*commandret); + executeret(args[i].code, *commandret); + if(!getbool(*commandret)) break; + } +}); +ICOMMAND(||, "e1V", (tagval *args, int numargs), +{ + if(!numargs) intret(0); + else loopi(numargs) + { + if(i) freearg(*commandret); + executeret(args[i].code, *commandret); + if(getbool(*commandret)) break; + } +}); + +ICOMMAND(div, "ii", (int *a, int *b), intret(*b ? *a / *b : 0)); +ICOMMAND(mod, "ii", (int *a, int *b), intret(*b ? *a % *b : 0)); +ICOMMAND(divf, "ff", (float *a, float *b), floatret(*b ? *a / *b : 0)); +ICOMMAND(modf, "ff", (float *a, float *b), floatret(*b ? fmod(*a, *b) : 0)); +ICOMMAND(sin, "f", (float *a), floatret(sin(*a*RAD))); +ICOMMAND(cos, "f", (float *a), floatret(cos(*a*RAD))); +ICOMMAND(tan, "f", (float *a), floatret(tan(*a*RAD))); +ICOMMAND(asin, "f", (float *a), floatret(asin(*a)/RAD)); +ICOMMAND(acos, "f", (float *a), floatret(acos(*a)/RAD)); +ICOMMAND(atan, "f", (float *a), floatret(atan(*a)/RAD)); +ICOMMAND(atan2, "ff", (float *y, float *x), floatret(atan2(*y, *x)/RAD)); +ICOMMAND(sqrt, "f", (float *a), floatret(sqrt(*a))); +ICOMMAND(pow, "ff", (float *a, float *b), floatret(pow(*a, *b))); +ICOMMAND(loge, "f", (float *a), floatret(log(*a))); +ICOMMAND(log2, "f", (float *a), floatret(log(*a)/M_LN2)); +ICOMMAND(log10, "f", (float *a), floatret(log10(*a))); +ICOMMAND(exp, "f", (float *a), floatret(exp(*a))); +ICOMMAND(min, "V", (tagval *args, int numargs), +{ + int val = numargs > 0 ? args[numargs - 1].getint() : 0; + loopi(numargs - 1) val = min(val, args[i].getint()); + intret(val); +}); +ICOMMAND(max, "V", (tagval *args, int numargs), +{ + int val = numargs > 0 ? args[numargs - 1].getint() : 0; + loopi(numargs - 1) val = max(val, args[i].getint()); + intret(val); +}); +ICOMMAND(minf, "V", (tagval *args, int numargs), +{ + float val = numargs > 0 ? args[numargs - 1].getfloat() : 0.0f; + loopi(numargs - 1) val = min(val, args[i].getfloat()); + floatret(val); +}); +ICOMMAND(maxf, "V", (tagval *args, int numargs), +{ + float val = numargs > 0 ? args[numargs - 1].getfloat() : 0.0f; + loopi(numargs - 1) val = max(val, args[i].getfloat()); + floatret(val); +}); +ICOMMAND(abs, "i", (int *n), intret(abs(*n))); +ICOMMAND(absf, "f", (float *n), floatret(fabs(*n))); + +ICOMMAND(floor, "f", (float *n), floatret(floor(*n))); +ICOMMAND(ceil, "f", (float *n), floatret(ceil(*n))); +ICOMMAND(round, "ff", (float *n, float *k), +{ + double step = *k; + double r = *n; + if(step > 0) + { + r += step * (r < 0 ? -0.5 : 0.5); + r -= fmod(r, step); + } + else r = r < 0 ? ceil(r - 0.5) : floor(r + 0.5); + floatret(float(r)); +}); + +ICOMMAND(cond, "ee2V", (tagval *args, int numargs), +{ + for(int i = 0; i < numargs; i += 2) + { + if(i+1 < numargs) + { + if(executebool(args[i].code)) + { + executeret(args[i+1].code, *commandret); + break; + } + } + else + { + executeret(args[i].code, *commandret); + break; + } + } +}); +#define CASECOMMAND(name, fmt, type, acc, compare) \ + ICOMMAND(name, fmt "te2V", (tagval *args, int numargs), \ + { \ + type val = acc; \ + int i; \ + for(i = 1; i+1 < numargs; i += 2) \ + { \ + if(compare) \ + { \ + executeret(args[i+1].code, *commandret); \ + return; \ + } \ + } \ + }) +CASECOMMAND(case, "i", int, args[0].getint(), args[i].type == VAL_NULL || args[i].getint() == val); +CASECOMMAND(casef, "f", float, args[0].getfloat(), args[i].type == VAL_NULL || args[i].getfloat() == val); +CASECOMMAND(cases, "s", const char *, args[0].getstr(), args[i].type == VAL_NULL || !strcmp(args[i].getstr(), val)); + +ICOMMAND(rnd, "ii", (int *a, int *b), intret(*a - *b > 0 ? rnd(*a - *b) + *b : *b)); +ICOMMAND(rndstr, "i", (int *len), +{ + int n = clamp(*len, 0, 10000); + char *s = newstring(n); + for(int i = 0; i < n;) + { + uint r = randomMT(); + for(int j = min(i + 4, n); i < j; i++) + { + s[i] = (r%255) + 1; + r /= 255; + } + } + s[n] = '\0'; + stringret(s); +}); + +ICOMMAND(strcmp, "ss", (char *a, char *b), intret(strcmp(a,b)==0)); +ICOMMAND(=s, "ss", (char *a, char *b), intret(strcmp(a,b)==0)); +ICOMMAND(!=s, "ss", (char *a, char *b), intret(strcmp(a,b)!=0)); +ICOMMAND(s, "ss", (char *a, char *b), intret(strcmp(a,b)>0)); +ICOMMAND(<=s, "ss", (char *a, char *b), intret(strcmp(a,b)<=0)); +ICOMMAND(>=s, "ss", (char *a, char *b), intret(strcmp(a,b)>=0)); +ICOMMAND(echo, "C", (char *s), conoutf(CON_ECHO, "\f1%s", s)); +ICOMMAND(error, "C", (char *s), conoutf(CON_ERROR, "%s", s)); +ICOMMAND(strstr, "ss", (char *a, char *b), { char *s = strstr(a, b); intret(s ? s-a : -1); }); +ICOMMAND(strlen, "s", (char *s), intret(strlen(s))); +ICOMMAND(strcode, "si", (char *s, int *i), intret(*i > 0 ? (memchr(s, 0, *i) ? 0 : uchar(s[*i])) : uchar(s[0]))); +ICOMMAND(codestr, "i", (int *i), { char *s = newstring(1); s[0] = char(*i); s[1] = '\0'; stringret(s); }); +ICOMMAND(struni, "si", (char *s, int *i), intret(*i > 0 ? (memchr(s, 0, *i) ? 0 : cube2uni(s[*i])) : cube2uni(s[0]))); +ICOMMAND(unistr, "i", (int *i), { char *s = newstring(1); s[0] = uni2cube(*i); s[1] = '\0'; stringret(s); }); + +int naturalsort(const char *a, const char *b) +{ + for(;;) + { + int ac = *a, bc = *b; + if(!ac) return bc ? -1 : 0; + else if(!bc) return 1; + else if(isdigit(ac) && isdigit(bc)) + { + while(*a == '0') a++; + while(*b == '0') b++; + const char *a0 = a, *b0 = b; + while(isdigit(*a)) a++; + while(isdigit(*b)) b++; + int alen = a - a0, blen = b - b0; + if(alen != blen) return alen - blen; + int n = memcmp(a0, b0, alen); + if(n < 0) return -1; + else if(n > 0) return 1; + } + else if(ac != bc) return ac - bc; + else { ++a; ++b; } + } +} +ICOMMAND(naturalsort, "ss", (char *a, char *b), intret(naturalsort(a,b)<=0)); + +#define STRMAPCOMMAND(name, map) \ + ICOMMAND(name, "s", (char *s), \ + { \ + int len = strlen(s); \ + char *m = newstring(len); \ + loopi(len) m[i] = map(s[i]); \ + m[len] = '\0'; \ + stringret(m); \ + }) + +STRMAPCOMMAND(strlower, cubelower); +STRMAPCOMMAND(strupper, cubeupper); + +char *strreplace(const char *s, const char *oldval, const char *newval) +{ + vector buf; + + int oldlen = strlen(oldval); + if(!oldlen) return newstring(s); + for(;;) + { + const char *found = strstr(s, oldval); + if(found) + { + while(s < found) buf.add(*s++); + for(const char *n = newval; *n; n++) buf.add(*n); + s = found + oldlen; + } + else + { + while(*s) buf.add(*s++); + buf.add('\0'); + return newstring(buf.getbuf(), buf.length()); + } + } +} + +ICOMMAND(strreplace, "sss", (char *s, char *o, char *n), commandret->setstr(strreplace(s, o, n))); + +void strsplice(const char *s, const char *vals, int *skip, int *count) +{ + int slen = strlen(s), vlen = strlen(vals), + offset = clamp(*skip, 0, slen), + len = clamp(*count, 0, slen - offset); + char *p = newstring(slen - len + vlen); + if(offset) memcpy(p, s, offset); + if(vlen) memcpy(&p[offset], vals, vlen); + if(offset + len < slen) memcpy(&p[offset + vlen], &s[offset + len], slen - (offset + len)); + p[slen - len + vlen] = '\0'; + commandret->setstr(p); +} +COMMAND(strsplice, "ssii"); + +#ifndef STANDALONE +ICOMMAND(getmillis, "i", (int *total), intret(*total ? totalmillis : lastmillis)); + +struct sleepcmd +{ + int delay, millis, flags; + char *command; +}; +vector sleepcmds; + +void addsleep(int *msec, char *cmd) +{ + sleepcmd &s = sleepcmds.add(); + s.delay = max(*msec, 1); + s.millis = lastmillis; + s.command = newstring(cmd); + s.flags = identflags; +} + +COMMANDN(sleep, addsleep, "is"); + +void checksleep(int millis) +{ + loopv(sleepcmds) + { + sleepcmd &s = sleepcmds[i]; + if(millis - s.millis >= s.delay) + { + char *cmd = s.command; // execute might create more sleep commands + s.command = NULL; + int oldflags = identflags; + identflags = s.flags; + execute(cmd); + identflags = oldflags; + delete[] cmd; + if(sleepcmds.inrange(i) && !sleepcmds[i].command) sleepcmds.remove(i--); + } + } +} + +void clearsleep(bool clearoverrides) +{ + int len = 0; + loopv(sleepcmds) if(sleepcmds[i].command) + { + if(clearoverrides && !(sleepcmds[i].flags&IDF_OVERRIDDEN)) sleepcmds[len++] = sleepcmds[i]; + else delete[] sleepcmds[i].command; + } + sleepcmds.shrink(len); +} + +void clearsleep_(int *clearoverrides) +{ + clearsleep(*clearoverrides!=0 || identflags&IDF_OVERRIDDEN); +} + +COMMANDN(clearsleep, clearsleep_, "i"); +#endif + diff --git a/src/engine/console.cpp b/src/engine/console.cpp new file mode 100644 index 0000000..d6e83a1 --- /dev/null +++ b/src/engine/console.cpp @@ -0,0 +1,785 @@ +// console.cpp: the console buffer, its display, and command line control + +#include "engine.h" + +#define MAXCONLINES 1000 +struct cline { char *line; int type, outtime; }; +reversequeue conlines; + +int commandmillis = -1; +string commandbuf; +char *commandaction = NULL, *commandprompt = NULL; +enum { CF_COMPLETE = 1<<0, CF_EXECUTE = 1<<1 }; +int commandflags = 0, commandpos = -1; + +VARFP(maxcon, 10, 200, MAXCONLINES, { while(conlines.length() > maxcon) delete[] conlines.pop().line; }); + +#define CONSTRLEN 512 + +VARP(contags, 0, 3, 3); + +void conline(int type, const char *sf) // add a line to the console buffer +{ + char *buf = NULL; + if(type&CON_TAG_MASK) for(int i = conlines.length()-1; i >= max(conlines.length()-contags, 0); i--) + { + int prev = conlines.removing(i).type; + if(!(prev&CON_TAG_MASK)) break; + if(type == prev) + { + buf = conlines.remove(i).line; + break; + } + } + if(!buf) buf = conlines.length() >= maxcon ? conlines.remove().line : newstring("", CONSTRLEN-1); + cline &cl = conlines.add(); + cl.line = buf; + cl.type = type; + cl.outtime = totalmillis; // for how long to keep line on screen + copystring(cl.line, sf, CONSTRLEN); +} + +void conoutfv(int type, const char *fmt, va_list args) +{ + static char buf[CONSTRLEN]; + vformatstring(buf, fmt, args, sizeof(buf)); + conline(type, buf); + logoutf("%s", buf); +} + +VAR(fullconsole, 0, 0, 1); +ICOMMAND(toggleconsole, "", (), { fullconsole ^= 1; }); + +int rendercommand(int x, int y, int w) +{ + if(commandmillis < 0) return 0; + + defformatstring(s, "%s %s", commandprompt ? commandprompt : ">", commandbuf); + int width, height; + text_bounds(s, width, height, w); + y -= height; + draw_text(s, x, y, 0xFF, 0xFF, 0xFF, 0xFF, (commandpos>=0) ? (commandpos+1+(commandprompt?strlen(commandprompt):1)) : strlen(s), w); + return height; +} + +VARP(consize, 0, 5, 100); +VARP(miniconsize, 0, 5, 100); +VARP(miniconwidth, 0, 40, 100); +VARP(confade, 0, 30, 60); +VARP(miniconfade, 0, 30, 60); +VARP(fullconsize, 0, 75, 100); +HVARP(confilter, 0, 0x7FFFFFF, 0x7FFFFFF); +HVARP(fullconfilter, 0, 0x7FFFFFF, 0x7FFFFFF); +HVARP(miniconfilter, 0, 0, 0x7FFFFFF); + +int conskip = 0, miniconskip = 0; + +void setconskip(int &skip, int filter, int n) +{ + filter &= CON_FLAGS; + int offset = abs(n), dir = n < 0 ? -1 : 1; + skip = clamp(skip, 0, conlines.length()-1); + while(offset) + { + skip += dir; + if(!conlines.inrange(skip)) + { + skip = clamp(skip, 0, conlines.length()-1); + return; + } + if(conlines[skip].type&filter) --offset; + } +} + +ICOMMAND(conskip, "i", (int *n), setconskip(conskip, fullconsole ? fullconfilter : confilter, *n)); +ICOMMAND(miniconskip, "i", (int *n), setconskip(miniconskip, miniconfilter, *n)); + +ICOMMAND(clearconsole, "", (), { while(conlines.length()) delete[] conlines.pop().line; }); + +int drawconlines(int conskip, int confade, int conwidth, int conheight, int conoff, int filter, int y = 0, int dir = 1) +{ + filter &= CON_FLAGS; + int numl = conlines.length(), offset = min(conskip, numl); + + if(confade) + { + if(!conskip) + { + numl = 0; + loopvrev(conlines) if(totalmillis-conlines[i].outtime < confade*1000) { numl = i+1; break; } + } + else offset--; + } + + int totalheight = 0; + loopi(numl) //determine visible height + { + // shuffle backwards to fill if necessary + int idx = offset+i < numl ? offset+i : --offset; + if(!(conlines[idx].type&filter)) continue; + char *line = conlines[idx].line; + int width, height; + text_bounds(line, width, height, conwidth); + if(totalheight + height > conheight) { numl = i; if(offset == idx) ++offset; break; } + totalheight += height; + } + if(dir > 0) y = conoff; + loopi(numl) + { + int idx = offset + (dir > 0 ? numl-i-1 : i); + if(!(conlines[idx].type&filter)) continue; + char *line = conlines[idx].line; + int width, height; + text_bounds(line, width, height, conwidth); + if(dir <= 0) y -= height; + draw_text(line, conoff, y, 0xFF, 0xFF, 0xFF, 0xFF, -1, conwidth); + if(dir > 0) y += height; + } + return y+conoff; +} + +int renderconsole(int w, int h, int abovehud) // render buffer taking into account time & scrolling +{ + int conpad = fullconsole ? 0 : FONTH/4, + conoff = fullconsole ? FONTH : FONTH/3, + conheight = min(fullconsole ? ((h*fullconsize/100)/FONTH)*FONTH : FONTH*consize, h - 2*(conpad + conoff)), + conwidth = w - 2*(conpad + conoff) - (fullconsole ? 0 : game::clipconsole(w, h)); + + extern void consolebox(int x1, int y1, int x2, int y2); + if(fullconsole) consolebox(conpad, conpad, conwidth+conpad+2*conoff, conheight+conpad+2*conoff); + + int y = drawconlines(conskip, fullconsole ? 0 : confade, conwidth, conheight, conpad+conoff, fullconsole ? fullconfilter : confilter); + if(!fullconsole && (miniconsize && miniconwidth)) + drawconlines(miniconskip, miniconfade, (miniconwidth*(w - 2*(conpad + conoff)))/100, min(FONTH*miniconsize, abovehud - y), conpad+conoff, miniconfilter, abovehud, -1); + return fullconsole ? conheight + 2*(conpad + conoff) : y; +} + +// keymap is defined externally in keymap.cfg + +struct keym +{ + enum + { + ACTION_DEFAULT = 0, + ACTION_SPECTATOR, + ACTION_EDITING, + NUMACTIONS + }; + + int code; + char *name; + char *actions[NUMACTIONS]; + bool pressed; + + keym() : code(-1), name(NULL), pressed(false) { loopi(NUMACTIONS) actions[i] = newstring(""); } + ~keym() { DELETEA(name); loopi(NUMACTIONS) DELETEA(actions[i]); } +}; + +hashtable keyms(128); + +void keymap(int *code, char *key) +{ + if(identflags&IDF_OVERRIDDEN) { conoutf(CON_ERROR, "cannot override keymap %d", *code); return; } + keym &km = keyms[*code]; + km.code = *code; + DELETEA(km.name); + km.name = newstring(key); +} + +COMMAND(keymap, "is"); + +keym *keypressed = NULL; +char *keyaction = NULL; + +const char *getkeyname(int code) +{ + keym *km = keyms.access(code); + return km ? km->name : NULL; +} + +void searchbinds(char *action, int type) +{ + vector names; + enumerate(keyms, keym, km, + { + if(!strcmp(km.actions[type], action)) + { + if(names.length()) names.add(' '); + names.put(km.name, strlen(km.name)); + } + }); + names.add('\0'); + result(names.getbuf()); +} + +keym *findbind(char *key) +{ + enumerate(keyms, keym, km, + { + if(!strcasecmp(km.name, key)) return &km; + }); + return NULL; +} + +void getbind(char *key, int type) +{ + keym *km = findbind(key); + result(km ? km->actions[type] : ""); +} + +void bindkey(char *key, char *action, int state, const char *cmd) +{ + if(identflags&IDF_OVERRIDDEN) { conoutf(CON_ERROR, "cannot override %s \"%s\"", cmd, key); return; } + keym *km = findbind(key); + if(!km) { conoutf(CON_ERROR, "unknown key \"%s\"", key); return; } + char *&binding = km->actions[state]; + if(!keypressed || keyaction!=binding) delete[] binding; + // trim white-space to make searchbinds more reliable + while(iscubespace(*action)) action++; + int len = strlen(action); + while(len>0 && iscubespace(action[len-1])) len--; + binding = newstring(action, len); +} + +ICOMMAND(bind, "ss", (char *key, char *action), bindkey(key, action, keym::ACTION_DEFAULT, "bind")); +ICOMMAND(specbind, "ss", (char *key, char *action), bindkey(key, action, keym::ACTION_SPECTATOR, "specbind")); +ICOMMAND(editbind, "ss", (char *key, char *action), bindkey(key, action, keym::ACTION_EDITING, "editbind")); +ICOMMAND(getbind, "s", (char *key), getbind(key, keym::ACTION_DEFAULT)); +ICOMMAND(getspecbind, "s", (char *key), getbind(key, keym::ACTION_SPECTATOR)); +ICOMMAND(geteditbind, "s", (char *key), getbind(key, keym::ACTION_EDITING)); +ICOMMAND(searchbinds, "s", (char *action), searchbinds(action, keym::ACTION_DEFAULT)); +ICOMMAND(searchspecbinds, "s", (char *action), searchbinds(action, keym::ACTION_SPECTATOR)); +ICOMMAND(searcheditbinds, "s", (char *action), searchbinds(action, keym::ACTION_EDITING)); + +void inputcommand(char *init, char *action = NULL, char *prompt = NULL, char *flags = NULL) // turns input to the command line on or off +{ + commandmillis = init ? totalmillis : -1; + textinput(commandmillis >= 0, TI_CONSOLE); + keyrepeat(commandmillis >= 0, KR_CONSOLE); + copystring(commandbuf, init ? init : ""); + DELETEA(commandaction); + DELETEA(commandprompt); + commandpos = -1; + if(action && action[0]) commandaction = newstring(action); + if(prompt && prompt[0]) commandprompt = newstring(prompt); + commandflags = 0; + if(flags) while(*flags) switch(*flags++) + { + case 'c': commandflags |= CF_COMPLETE; break; + case 'x': commandflags |= CF_EXECUTE; break; + case 's': commandflags |= CF_COMPLETE|CF_EXECUTE; break; + } + else if(init) commandflags |= CF_COMPLETE|CF_EXECUTE; +} + +ICOMMAND(saycommand, "C", (char *init), inputcommand(init)); +COMMAND(inputcommand, "ssss"); + +void pasteconsole() +{ + if(!SDL_HasClipboardText()) return; + char *cb = SDL_GetClipboardText(); + if(!cb) return; + size_t cblen = strlen(cb), + commandlen = strlen(commandbuf), + decoded = decodeutf8((uchar *)&commandbuf[commandlen], sizeof(commandbuf)-1-commandlen, (const uchar *)cb, cblen); + commandbuf[commandlen + decoded] = '\0'; + SDL_free(cb); +} + +struct hline +{ + char *buf, *action, *prompt; + int flags; + + hline() : buf(NULL), action(NULL), prompt(NULL), flags(0) {} + ~hline() + { + DELETEA(buf); + DELETEA(action); + DELETEA(prompt); + } + + void restore() + { + copystring(commandbuf, buf); + if(commandpos >= (int)strlen(commandbuf)) commandpos = -1; + DELETEA(commandaction); + DELETEA(commandprompt); + if(action) commandaction = newstring(action); + if(prompt) commandprompt = newstring(prompt); + commandflags = flags; + } + + bool shouldsave() + { + return strcmp(commandbuf, buf) || + (commandaction ? !action || strcmp(commandaction, action) : action!=NULL) || + (commandprompt ? !prompt || strcmp(commandprompt, prompt) : prompt!=NULL) || + commandflags != flags; + } + + void save() + { + buf = newstring(commandbuf); + if(commandaction) action = newstring(commandaction); + if(commandprompt) prompt = newstring(commandprompt); + flags = commandflags; + } + + void run() + { + if(flags&CF_EXECUTE && buf[0]=='/') execute(buf+1); + else if(action) + { + alias("commandbuf", buf); + execute(action); + } + else game::toserver(buf); + } +}; +vector history; +int histpos = 0; + +VARP(maxhistory, 0, 1000, 10000); + +void history_(int *n) +{ + static bool inhistory = false; + if(!inhistory && history.inrange(*n)) + { + inhistory = true; + history[history.length()-*n-1]->run(); + inhistory = false; + } +} + +COMMANDN(history, history_, "i"); + +struct releaseaction +{ + keym *key; + char *action; +}; +vector releaseactions; + +const char *addreleaseaction(char *s) +{ + if(!keypressed) { delete[] s; return NULL; } + releaseaction &ra = releaseactions.add(); + ra.key = keypressed; + ra.action = s; + return keypressed->name; +} + +void onrelease(const char *s) +{ + addreleaseaction(newstring(s)); +} + +COMMAND(onrelease, "s"); + +void execbind(keym &k, bool isdown) +{ + loopv(releaseactions) + { + releaseaction &ra = releaseactions[i]; + if(ra.key==&k) + { + if(!isdown) execute(ra.action); + delete[] ra.action; + releaseactions.remove(i--); + } + } + if(isdown) + { + int state = keym::ACTION_DEFAULT; + if(!mainmenu) + { + if(editmode) state = keym::ACTION_EDITING; + else if(player->state==CS_SPECTATOR) state = keym::ACTION_SPECTATOR; + } + char *&action = k.actions[state][0] ? k.actions[state] : k.actions[keym::ACTION_DEFAULT]; + keyaction = action; + keypressed = &k; + execute(keyaction); + keypressed = NULL; + if(keyaction!=action) delete[] keyaction; + } + k.pressed = isdown; +} + +bool consoleinput(const char *str, int len) +{ + if(commandmillis < 0) return false; + + resetcomplete(); + int cmdlen = (int)strlen(commandbuf), cmdspace = int(sizeof(commandbuf)) - (cmdlen+1); + len = min(len, cmdspace); + if(commandpos<0) + { + memcpy(&commandbuf[cmdlen], str, len); + } + else + { + memmove(&commandbuf[commandpos+len], &commandbuf[commandpos], cmdlen - commandpos); + memcpy(&commandbuf[commandpos], str, len); + commandpos += len; + } + commandbuf[cmdlen + len] = '\0'; + + return true; +} + +bool consolekey(int code, bool isdown) +{ + if(commandmillis < 0) return false; + + #ifdef __APPLE__ + #define MOD_KEYS (KMOD_LGUI|KMOD_RGUI) + #else + #define MOD_KEYS (KMOD_LCTRL|KMOD_RCTRL) + #endif + + if(isdown) + { + switch(code) + { + case SDLK_RETURN: + case SDLK_KP_ENTER: + break; + + case SDLK_HOME: + if(strlen(commandbuf)) commandpos = 0; + break; + + case SDLK_END: + commandpos = -1; + break; + + case SDLK_DELETE: + { + int len = (int)strlen(commandbuf); + if(commandpos<0) break; + memmove(&commandbuf[commandpos], &commandbuf[commandpos+1], len - commandpos); + resetcomplete(); + if(commandpos>=len-1) commandpos = -1; + break; + } + + case SDLK_BACKSPACE: + { + int len = (int)strlen(commandbuf), i = commandpos>=0 ? commandpos : len; + if(i<1) break; + memmove(&commandbuf[i-1], &commandbuf[i], len - i + 1); + resetcomplete(); + if(commandpos>0) commandpos--; + else if(!commandpos && len<=1) commandpos = -1; + break; + } + + case SDLK_LEFT: + if(commandpos>0) commandpos--; + else if(commandpos<0) commandpos = (int)strlen(commandbuf)-1; + break; + + case SDLK_RIGHT: + if(commandpos>=0 && ++commandpos>=(int)strlen(commandbuf)) commandpos = -1; + break; + + case SDLK_UP: + if(histpos > history.length()) histpos = history.length(); + if(histpos > 0) history[--histpos]->restore(); + break; + + case SDLK_DOWN: + if(histpos + 1 < history.length()) history[++histpos]->restore(); + break; + + case SDLK_TAB: + if(commandflags&CF_COMPLETE) + { + complete(commandbuf, sizeof(commandbuf), commandflags&CF_EXECUTE ? "/" : NULL); + if(commandpos>=0 && commandpos>=(int)strlen(commandbuf)) commandpos = -1; + } + break; + + case SDLK_v: + if(SDL_GetModState()&MOD_KEYS) pasteconsole(); + break; + } + } + else + { + if(code==SDLK_RETURN || code==SDLK_KP_ENTER) + { + hline *h = NULL; + if(commandbuf[0]) + { + if(history.empty() || history.last()->shouldsave()) + { + if(maxhistory && history.length() >= maxhistory) + { + loopi(history.length()-maxhistory+1) delete history[i]; + history.remove(0, history.length()-maxhistory+1); + } + history.add(h = new hline)->save(); + } + else h = history.last(); + } + histpos = history.length(); + inputcommand(NULL); + if(h) h->run(); + } + else if(code==SDLK_ESCAPE) + { + histpos = history.length(); + inputcommand(NULL); + } + } + + return true; +} + +void processtextinput(const char *str, int len) +{ + if(!g3d_input(str, len)) + consoleinput(str, len); +} + +void processkey(int code, bool isdown, int modstate) +{ + switch(code) + { + case SDLK_LGUI: case SDLK_RGUI: + return; + } + keym *haskey = keyms.access(code); + if(haskey && haskey->pressed) execbind(*haskey, isdown); // allow pressed keys to release + else if(!g3d_key(code, isdown)) // 3D GUI mouse button intercept + { + if(!consolekey(code, isdown)) + { + if(modstate&KMOD_GUI) return; + if(haskey) execbind(*haskey, isdown); + } + } +} + +void clear_console() +{ + keyms.clear(); +} + +void writebinds(stream *f) +{ + static const char * const cmds[3] = { "bind", "specbind", "editbind" }; + vector binds; + enumerate(keyms, keym, km, binds.add(&km)); + binds.sortname(); + loopj(3) + { + loopv(binds) + { + keym &km = *binds[i]; + if(*km.actions[j]) + { + if(validateblock(km.actions[j])) f->printf("%s %s [%s]\n", cmds[j], escapestring(km.name), km.actions[j]); + else f->printf("%s %s %s\n", cmds[j], escapestring(km.name), escapestring(km.actions[j])); + } + } + } +} + +// tab-completion of all idents and base maps + +enum { FILES_DIR = 0, FILES_VAR, FILES_LIST }; + +struct fileskey +{ + int type; + const char *dir, *ext; + + fileskey() {} + fileskey(int type, const char *dir, const char *ext) : type(type), dir(dir), ext(ext) {} +}; + +static void cleanfilesdir(char *dir) +{ + int dirlen = (int)strlen(dir); + while(dirlen > 0 && (dir[dirlen-1] == '/' || dir[dirlen-1] == '\\')) + dir[--dirlen] = '\0'; +} + +struct filesval +{ + int type; + char *dir, *ext; + vector files; + int millis; + + filesval(int type, const char *dir, const char *ext) : type(type), dir(newstring(dir)), ext(ext && ext[0] ? newstring(ext) : NULL), millis(-1) {} + ~filesval() { DELETEA(dir); DELETEA(ext); files.deletearrays(); } + + void update() + { + if((type!=FILES_DIR && type!=FILES_VAR) || millis >= commandmillis) return; + files.deletearrays(); + if(type==FILES_VAR) + { + string buf; + buf[0] = '\0'; + if(ident *id = readident(dir)) switch(id->type) + { + case ID_SVAR: copystring(buf, *id->storage.s); break; + case ID_ALIAS: copystring(buf, id->getstr()); break; + } + if(!buf[0]) copystring(buf, "."); + cleanfilesdir(buf); + listfiles(buf, ext, files); + } + else listfiles(dir, ext, files); + files.sort(); + loopv(files) if(i && !strcmp(files[i], files[i-1])) delete[] files.remove(i--); + millis = totalmillis; + } +}; + +static inline bool htcmp(const fileskey &x, const fileskey &y) +{ + return x.type==y.type && !strcmp(x.dir, y.dir) && (x.ext == y.ext || (x.ext && y.ext && !strcmp(x.ext, y.ext))); +} + +static inline uint hthash(const fileskey &k) +{ + return hthash(k.dir); +} + +static hashtable completefiles; +static hashtable completions; + +int completesize = 0; +char *lastcomplete = NULL; + +void resetcomplete() { completesize = 0; } + +void addcomplete(char *command, int type, char *dir, char *ext) +{ + if(identflags&IDF_OVERRIDDEN) + { + conoutf(CON_ERROR, "cannot override complete %s", command); + return; + } + if(!dir[0]) + { + filesval **hasfiles = completions.access(command); + if(hasfiles) *hasfiles = NULL; + return; + } + if(type==FILES_DIR) cleanfilesdir(dir); + if(ext) + { + if(strchr(ext, '*')) ext[0] = '\0'; + if(!ext[0]) ext = NULL; + } + fileskey key(type, dir, ext); + filesval **val = completefiles.access(key); + if(!val) + { + filesval *f = new filesval(type, dir, ext); + if(type==FILES_LIST) explodelist(dir, f->files); + val = &completefiles[fileskey(type, f->dir, f->ext)]; + *val = f; + } + filesval **hasfiles = completions.access(command); + if(hasfiles) *hasfiles = *val; + else completions[newstring(command)] = *val; +} + +void addfilecomplete(char *command, char *dir, char *ext) +{ + addcomplete(command, FILES_DIR, dir, ext); +} + +void addvarcomplete(char *command, char *var, char *ext) +{ + addcomplete(command, FILES_VAR, var, ext); +} + +void addlistcomplete(char *command, char *list) +{ + addcomplete(command, FILES_LIST, list, NULL); +} + +COMMANDN(complete, addfilecomplete, "sss"); +COMMANDN(varcomplete, addvarcomplete, "sss"); +COMMANDN(listcomplete, addlistcomplete, "ss"); + +void complete(char *s, int maxlen, const char *cmdprefix) +{ + int cmdlen = 0; + if(cmdprefix) + { + cmdlen = strlen(cmdprefix); + if(strncmp(s, cmdprefix, cmdlen)) prependstring(s, cmdprefix, maxlen); + } + if(!s[cmdlen]) return; + if(!completesize) { completesize = (int)strlen(&s[cmdlen]); DELETEA(lastcomplete); } + + filesval *f = NULL; + if(completesize) + { + char *end = strchr(&s[cmdlen], ' '); + if(end) f = completions.find(stringslice(&s[cmdlen], end), NULL); + } + + const char *nextcomplete = NULL; + if(f) // complete using filenames + { + int commandsize = strchr(&s[cmdlen], ' ')+1-s; + f->update(); + loopv(f->files) + { + if(strncmp(f->files[i], &s[commandsize], completesize+cmdlen-commandsize)==0 && + (!lastcomplete || strcmp(f->files[i], lastcomplete) > 0) && (!nextcomplete || strcmp(f->files[i], nextcomplete) < 0)) + nextcomplete = f->files[i]; + } + cmdprefix = s; + cmdlen = commandsize; + } + else // complete using command names + { + enumerate(idents, ident, id, + if(strncmp(id.name, &s[cmdlen], completesize)==0 && + (!lastcomplete || strcmp(id.name, lastcomplete) > 0) && (!nextcomplete || strcmp(id.name, nextcomplete) < 0)) + nextcomplete = id.name; + ); + } + DELETEA(lastcomplete); + if(nextcomplete) + { + cmdlen = min(cmdlen, maxlen-1); + if(cmdlen) memmove(s, cmdprefix, cmdlen); + copystring(&s[cmdlen], nextcomplete, maxlen-cmdlen); + lastcomplete = newstring(nextcomplete); + } +} + +void writecompletions(stream *f) +{ + vector cmds; + enumeratekt(completions, char *, k, filesval *, v, { if(v) cmds.add(k); }); + cmds.sort(); + loopv(cmds) + { + char *k = cmds[i]; + filesval *v = completions[k]; + if(v->type==FILES_LIST) + { + if(validateblock(v->dir)) f->printf("listcomplete %s [%s]\n", escapeid(k), v->dir); + else f->printf("listcomplete %s %s\n", escapeid(k), escapestring(v->dir)); + } + else f->printf("%s %s %s %s\n", v->type==FILES_VAR ? "varcomplete" : "complete", escapeid(k), escapestring(v->dir), escapestring(v->ext ? v->ext : "*")); + } +} + diff --git a/src/engine/decal.cpp b/src/engine/decal.cpp new file mode 100644 index 0000000..98787dd --- /dev/null +++ b/src/engine/decal.cpp @@ -0,0 +1,642 @@ +#include "engine.h" + +struct decalvert +{ + vec pos; + bvec4 color; + vec2 tc; +}; + +struct decalinfo +{ + int millis; + bvec color; + ushort startvert, endvert; +}; + +enum +{ + DF_RND4 = 1<<0, + DF_ROTATE = 1<<1, + DF_INVMOD = 1<<2, + DF_OVERBRIGHT = 1<<3, + DF_ADD = 1<<4, + DF_SATURATE = 1<<5 +}; + +VARFP(maxdecaltris, 1, 1024, 16384, initdecals()); +VARP(decalfade, 1000, 10000, 60000); +VAR(dbgdec, 0, 0, 1); + +struct decalrenderer +{ + const char *texname; + int flags, fadeintime, fadeouttime, timetolive; + Texture *tex; + decalinfo *decals; + int maxdecals, startdecal, enddecal; + decalvert *verts; + int maxverts, startvert, endvert, lastvert, availverts; + GLuint vbo; + bool dirty; + + decalrenderer(const char *texname, int flags = 0, int fadeintime = 0, int fadeouttime = 1000, int timetolive = -1) + : texname(texname), flags(flags), + fadeintime(fadeintime), fadeouttime(fadeouttime), timetolive(timetolive), + tex(NULL), + decals(NULL), maxdecals(0), startdecal(0), enddecal(0), + verts(NULL), maxverts(0), startvert(0), endvert(0), lastvert(0), availverts(0), + vbo(0), dirty(false), + decalu(0), decalv(0) + { + } + + ~decalrenderer() + { + DELETEA(decals); + DELETEA(verts); + } + + void init(int tris) + { + if(decals) + { + DELETEA(decals); + maxdecals = startdecal = enddecal = 0; + } + if(verts) + { + DELETEA(verts); + maxverts = startvert = endvert = lastvert = availverts = 0; + } + decals = new decalinfo[tris]; + maxdecals = tris; + tex = textureload(texname, 3); + maxverts = tris*3 + 3; + availverts = maxverts - 3; + verts = new decalvert[maxverts]; + } + + int hasdecals() + { + return enddecal < startdecal ? maxdecals - (startdecal - enddecal) : enddecal - startdecal; + } + + void cleanup() + { + if(vbo) { glDeleteBuffers_(1, &vbo); vbo = 0; } + } + + void cleardecals() + { + startdecal = enddecal = 0; + startvert = endvert = lastvert = 0; + availverts = maxverts - 3; + dirty = true; + } + + int freedecal() + { + if(startdecal==enddecal) return 0; + + decalinfo &d = decals[startdecal]; + startdecal++; + if(startdecal >= maxdecals) startdecal = 0; + + int removed = d.endvert < d.startvert ? maxverts - (d.startvert - d.endvert) : d.endvert - d.startvert; + startvert = d.endvert; + if(startvert==endvert) startvert = endvert = lastvert = 0; + availverts += removed; + + return removed; + } + + void fadedecal(decalinfo &d, uchar alpha) + { + bvec rgb; + if(flags&DF_OVERBRIGHT) rgb = bvec(128, 128, 128); + else + { + rgb = d.color; + if(flags&(DF_ADD|DF_INVMOD)) rgb.scale(alpha, 255); + } + bvec4 color(rgb, alpha); + + decalvert *vert = &verts[d.startvert], + *end = &verts[d.endvert < d.startvert ? maxverts : d.endvert]; + while(vert < end) + { + vert->color = color; + vert++; + } + if(d.endvert < d.startvert) + { + vert = verts; + end = &verts[d.endvert]; + while(vert < end) + { + vert->color = color; + vert++; + } + } + dirty = true; + } + + void clearfadeddecals() + { + int threshold = lastmillis - (timetolive>=0 ? timetolive : decalfade) - fadeouttime; + decalinfo *d = &decals[startdecal], + *end = &decals[enddecal < startdecal ? maxdecals : enddecal]; + while(d < end && d->millis <= threshold) d++; + if(d >= end && enddecal < startdecal) + { + d = decals; + end = &decals[enddecal]; + while(d < end && d->millis <= threshold) d++; + } + int prevstart = startdecal; + startdecal = d - decals; + if(prevstart == startdecal) return; + if(startdecal!=enddecal) startvert = decals[startdecal].startvert; + else startvert = endvert = lastvert = 0; + availverts = endvert < startvert ? startvert - endvert - 3 : maxverts - 3 - (endvert - startvert); + dirty = true; + } + + void fadeindecals() + { + if(!fadeintime) return; + decalinfo *d = &decals[enddecal], + *end = &decals[enddecal < startdecal ? 0 : startdecal]; + while(d > end) + { + d--; + int fade = lastmillis - d->millis; + if(fade >= fadeintime) return; + fadedecal(*d, (fade<<8)/fadeintime); + } + if(enddecal < startdecal) + { + d = &decals[maxdecals]; + end = &decals[startdecal]; + while(d > end) + { + d--; + int fade = lastmillis - d->millis; + if(fade >= fadeintime) return; + fadedecal(*d, (fade<<8)/fadeintime); + } + } + } + + void fadeoutdecals() + { + decalinfo *d = &decals[startdecal], + *end = &decals[enddecal < startdecal ? maxdecals : enddecal]; + int offset = (timetolive>=0 ? timetolive : decalfade) + fadeouttime - lastmillis; + while(d < end) + { + int fade = d->millis + offset; + if(fade >= fadeouttime) return; + fadedecal(*d, (fade<<8)/fadeouttime); + d++; + } + if(enddecal < startdecal) + { + d = decals; + end = &decals[enddecal]; + while(d < end) + { + int fade = d->millis + offset; + if(fade >= fadeouttime) return; + fadedecal(*d, (fade<<8)/fadeouttime); + d++; + } + } + } + + static void setuprenderstate() + { + enablepolygonoffset(GL_POLYGON_OFFSET_FILL); + + glDepthMask(GL_FALSE); + glEnable(GL_BLEND); + + gle::enablevertex(); + gle::enabletexcoord0(); + gle::enablecolor(); + } + + static void cleanuprenderstate() + { + gle::clearvbo(); + + gle::disablevertex(); + gle::disabletexcoord0(); + gle::disablecolor(); + + glDepthMask(GL_TRUE); + glDisable(GL_BLEND); + + disablepolygonoffset(GL_POLYGON_OFFSET_FILL); + } + + void render() + { + if(startvert==endvert) return; + + if(flags&DF_OVERBRIGHT) + { + glBlendFunc(GL_DST_COLOR, GL_SRC_COLOR); + SETSHADER(overbrightdecal); + } + else + { + if(flags&DF_INVMOD) { glBlendFunc(GL_ZERO, GL_ONE_MINUS_SRC_COLOR); zerofogcolor(); } + else if(flags&DF_ADD) { glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_COLOR); zerofogcolor(); } + else glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + if(flags&DF_SATURATE) SETSHADER(saturatedecal); + else foggedshader->set(); + } + + glBindTexture(GL_TEXTURE_2D, tex->id); + + if(!vbo) { glGenBuffers_(1, &vbo); dirty = true; } + gle::bindvbo(vbo); + + int count = endvert < startvert ? maxverts - startvert : endvert - startvert; + if(dirty) + { + glBufferData_(GL_ARRAY_BUFFER, maxverts*sizeof(decalvert), NULL, GL_STREAM_DRAW); + glBufferSubData_(GL_ARRAY_BUFFER, 0, count*sizeof(decalvert), &verts[startvert]); + if(endvert < startvert) + { + glBufferSubData_(GL_ARRAY_BUFFER, count*sizeof(decalvert), endvert*sizeof(decalvert), verts); + count += endvert; + } + dirty = false; + } + else if(endvert < startvert) count += endvert; + + const decalvert *ptr = 0; + gle::vertexpointer(sizeof(decalvert), ptr->pos.v); + gle::texcoord0pointer(sizeof(decalvert), ptr->tc.v); + gle::colorpointer(sizeof(decalvert), ptr->color.v); + + glDrawArrays(GL_TRIANGLES, 0, count); + xtravertsva += count; + + if(flags&(DF_ADD|DF_INVMOD)) resetfogcolor(); + + extern int intel_vertexarray_bug; + if(intel_vertexarray_bug) glFlush(); + } + + decalinfo &newdecal() + { + decalinfo &d = decals[enddecal]; + int next = enddecal + 1; + if(next>=maxdecals) next = 0; + if(next==startdecal) freedecal(); + enddecal = next; + dirty = true; + return d; + } + + ivec bbmin, bbmax; + vec decalcenter, decalnormal, decaltangent, decalbitangent; + float decalradius, decalu, decalv; + bvec4 decalcolor; + + void adddecal(const vec ¢er, const vec &dir, float radius, const bvec &color, int info) + { + if(dir.iszero()) return; + + int bbradius = int(ceil(radius)); + bbmin = ivec(center).sub(bbradius); + bbmax = ivec(center).add(bbradius); + + decalcolor = bvec4(color, 255); + decalcenter = center; + decalradius = radius; + decalnormal = dir; +#if 0 + decaltangent.orthogonal(dir); +#else + decaltangent = vec(dir.z, -dir.x, dir.y); + decaltangent.sub(vec(dir).mul(decaltangent.dot(dir))); +#endif + if(flags&DF_ROTATE) decaltangent.rotate(rnd(360)*RAD, dir); + decaltangent.normalize(); + decalbitangent.cross(decaltangent, dir); + if(flags&DF_RND4) + { + decalu = 0.5f*(info&1); + decalv = 0.5f*((info>>1)&1); + } + + lastvert = endvert; + gentris(worldroot, ivec(0, 0, 0), worldsize>>1); + if(dbgdec) + { + int nverts = endvert < lastvert ? endvert + maxverts - lastvert : endvert - lastvert; + conoutf(CON_DEBUG, "tris = %d, verts = %d, total tris = %d", nverts/3, nverts, (maxverts - 3 - availverts)/3); + } + if(endvert==lastvert) return; + + decalinfo &d = newdecal(); + d.color = color; + d.millis = lastmillis; + d.startvert = lastvert; + d.endvert = endvert; + } + + static int clip(const vec *in, int numin, const vec &dir, float below, float above, vec *out) + { + int numout = 0; + const vec *p = &in[numin-1]; + float pc = dir.dot(*p); + loopi(numin) + { + const vec &v = in[i]; + float c = dir.dot(v); + if(c < below) + { + if(pc > above) out[numout++] = vec(*p).sub(v).mul((above - c)/(pc - c)).add(v); + if(pc > below) out[numout++] = vec(*p).sub(v).mul((below - c)/(pc - c)).add(v); + } + else if(c > above) + { + if(pc < below) out[numout++] = vec(*p).sub(v).mul((below - c)/(pc - c)).add(v); + if(pc < above) out[numout++] = vec(*p).sub(v).mul((above - c)/(pc - c)).add(v); + } + else + { + if(pc < below) + { + if(c > below) out[numout++] = vec(*p).sub(v).mul((below - c)/(pc - c)).add(v); + } + else if(pc > above && c < above) out[numout++] = vec(*p).sub(v).mul((above - c)/(pc - c)).add(v); + out[numout++] = v; + } + p = &v; + pc = c; + } + return numout; + } + + void gentris(cube &cu, int orient, const ivec &o, int size, materialsurface *mat = NULL, int vismask = 0) + { + vec pos[MAXFACEVERTS+4]; + int numverts = 0, numplanes = 1; + vec planes[2]; + if(mat) + { + planes[0] = vec(0, 0, 0); + switch(orient) + { + #define GENFACEORIENT(orient, v0, v1, v2, v3) \ + case orient: \ + planes[0][dimension(orient)] = dimcoord(orient) ? 1 : -1; \ + v0 v1 v2 v3 \ + break; + #define GENFACEVERT(orient, vert, x,y,z, xv,yv,zv) \ + pos[numverts++] = vec(x xv, y yv, z zv); + GENFACEVERTS(o.x, o.x, o.y, o.y, o.z, o.z, , + mat->csize, , + mat->rsize, + 0.1f, - 0.1f); + #undef GENFACEORIENT + #undef GENFACEVERT + } + } + else if(cu.texture[orient] == DEFAULT_SKY) return; + else if(cu.ext && (numverts = cu.ext->surfaces[orient].numverts&MAXFACEVERTS)) + { + vertinfo *verts = cu.ext->verts() + cu.ext->surfaces[orient].verts; + ivec vo = ivec(o).mask(~0xFFF).shl(3); + loopj(numverts) pos[j] = vec(verts[j].getxyz().add(vo)).mul(1/8.0f); + planes[0].cross(pos[0], pos[1], pos[2]).normalize(); + if(numverts >= 4 && !(cu.merged&(1< decalradius) continue; + vec pcenter = vec(decalnormal).mul(dist).add(decalcenter); +#else + // travel back along plane normal from the decal center + float dist = n.dot(p); + if(fabs(dist) > decalradius) continue; + vec pcenter = vec(n).mul(dist).add(decalcenter); +#endif + vec ft, fb; + ft.orthogonal(n); + ft.normalize(); + fb.cross(ft, n); + vec pt = vec(ft).mul(ft.dot(decaltangent)).add(vec(fb).mul(fb.dot(decaltangent))).normalize(), + pb = vec(ft).mul(ft.dot(decalbitangent)).add(vec(fb).mul(fb.dot(decalbitangent))).normalize(); + // orthonormalize projected bitangent to prevent streaking + pb.sub(vec(pt).mul(pt.dot(pb))).normalize(); + vec v1[MAXFACEVERTS+4], v2[MAXFACEVERTS+4]; + float ptc = pt.dot(pcenter), pbc = pb.dot(pcenter); + int numv; + if(numplanes >= 2) + { + if(l) { pos[1] = pos[2]; pos[2] = pos[3]; } + numv = clip(pos, 3, pt, ptc - decalradius, ptc + decalradius, v1); + if(numv<3) continue; + } + else + { + numv = clip(pos, numverts, pt, ptc - decalradius, ptc + decalradius, v1); + if(numv<3) continue; + } + numv = clip(v1, numv, pb, pbc - decalradius, pbc + decalradius, v2); + if(numv<3) continue; + float tsz = flags&DF_RND4 ? 0.5f : 1.0f, scale = tsz*0.5f/decalradius, + tu = decalu + tsz*0.5f - ptc*scale, tv = decalv + tsz*0.5f - pbc*scale; + pt.mul(scale); pb.mul(scale); + decalvert dv1 = { v2[0], decalcolor, vec2(pt.dot(v2[0]) + tu, pb.dot(v2[0]) + tv) }, + dv2 = { v2[1], decalcolor, vec2(pt.dot(v2[1]) + tu, pb.dot(v2[1]) + tv) }; + int totalverts = 3*(numv-2); + if(totalverts > maxverts-3) return; + while(availverts < totalverts) + { + if(!freedecal()) return; + } + availverts -= totalverts; + loopk(numv-2) + { + verts[endvert++] = dv1; + verts[endvert++] = dv2; + dv2.pos = v2[k+2]; + dv2.tc = vec2(pt.dot(v2[k+2]) + tu, pb.dot(v2[k+2]) + tv); + verts[endvert++] = dv2; + if(endvert>=maxverts) endvert = 0; + } + } + } + + void findmaterials(vtxarray *va) + { + materialsurface *matbuf = va->matbuf; + int matsurfs = va->matsurfs; + loopi(matsurfs) + { + materialsurface &m = matbuf[i]; + if(!isclipped(m.material&MATF_VOLUME)) { i += m.skip; continue; } + int dim = dimension(m.orient), dc = dimcoord(m.orient); + if(dc ? decalnormal[dim] <= 0 : decalnormal[dim] >= 0) { i += m.skip; continue; } + int c = C[dim], r = R[dim]; + for(;;) + { + materialsurface &m = matbuf[i]; + if(m.o[dim] >= bbmin[dim] && m.o[dim] <= bbmax[dim] && + m.o[c] + m.csize >= bbmin[c] && m.o[c] <= bbmax[c] && + m.o[r] + m.rsize >= bbmin[r] && m.o[r] <= bbmax[r]) + { + static cube dummy; + gentris(dummy, m.orient, m.o, max(m.csize, m.rsize), &m); + } + if(i+1 >= matsurfs) break; + materialsurface &n = matbuf[i+1]; + if(n.material != m.material || n.orient != m.orient) break; + i++; + } + } + } + + void findescaped(cube *cu, const ivec &o, int size, int escaped) + { + loopi(8) + { + if(escaped&(1<>1, cu[i].escaped); + else + { + int vismask = cu[i].merged; + if(vismask) loopj(6) if(vismask&(1<va && cu[i].ext->va->matsurfs) + findmaterials(cu[i].ext->va); + if(cu[i].children) gentris(cu[i].children, co, size>>1, cu[i].escaped); + else + { + int vismask = cu[i].visible; + if(vismask&0xC0) + { + if(vismask&0x80) loopj(6) gentris(cu[i], j, co, size, NULL, vismask); + else loopj(6) if(vismask&(1<>1, cu[i].escaped); + else + { + int vismask = cu[i].merged; + if(vismask) loopj(6) if(vismask&(1<packages/particles/scorch.png", DF_ROTATE, 500), + decalrenderer("packages/particles/blood.png", DF_RND4|DF_ROTATE|DF_INVMOD), + decalrenderer("packages/particles/bullet.png", DF_OVERBRIGHT) +}; + +void initdecals() +{ + loopi(sizeof(decals)/sizeof(decals[0])) decals[i].init(maxdecaltris); +} + +void cleardecals() +{ + loopi(sizeof(decals)/sizeof(decals[0])) decals[i].cleardecals(); +} + +void cleanupdecals() +{ + loopi(sizeof(decals)/sizeof(decals[0])) decals[i].cleanup(); +} + +VARNP(decals, showdecals, 0, 1, 1); + +void renderdecals(bool mainpass) +{ + bool rendered = false; + loopi(sizeof(decals)/sizeof(decals[0])) + { + decalrenderer &d = decals[i]; + if(mainpass) + { + d.clearfadeddecals(); + d.fadeindecals(); + d.fadeoutdecals(); + } + if(!showdecals || !d.hasdecals()) continue; + if(!rendered) + { + rendered = true; + decalrenderer::setuprenderstate(); + } + d.render(); + } + if(!rendered) return; + decalrenderer::cleanuprenderstate(); +} + +VARP(maxdecaldistance, 1, 512, 10000); + +void adddecal(int type, const vec ¢er, const vec &surface, float radius, const bvec &color, int info) +{ + if(!showdecals || type<0 || (size_t)type>=sizeof(decals)/sizeof(decals[0]) || center.dist(camera1->o) - radius > maxdecaldistance) return; + decalrenderer &d = decals[type]; + d.adddecal(center, surface, radius, color, info); +} + diff --git a/src/engine/depthfx.h b/src/engine/depthfx.h new file mode 100644 index 0000000..a9c0fdf --- /dev/null +++ b/src/engine/depthfx.h @@ -0,0 +1,193 @@ +// eye space depth texture for soft particles, done at low res then blurred to prevent ugly jaggies +VARP(depthfxfpscale, 1, 1<<12, 1<<16); +VARP(depthfxscale, 1, 1<<6, 1<<8); +VARP(depthfxblend, 1, 16, 64); +VARP(depthfxpartblend, 1, 8, 64); +VAR(depthfxmargin, 0, 16, 64); +VAR(depthfxbias, 0, 1, 64); + +extern void cleanupdepthfx(); +VARFP(fpdepthfx, 0, 0, 1, cleanupdepthfx()); +VARP(depthfxemuprecision, 0, 1, 1); +VARFP(depthfxsize, 6, 7, 12, cleanupdepthfx()); +VARP(depthfx, 0, 1, 1); +VARP(depthfxparts, 0, 1, 1); +VARP(blurdepthfx, 0, 1, 7); +VARP(blurdepthfxsigma, 1, 50, 200); +VAR(depthfxscissor, 0, 2, 2); +VAR(debugdepthfx, 0, 0, 1); + +#define MAXDFXRANGES 4 + +void *depthfxowners[MAXDFXRANGES]; +float depthfxranges[MAXDFXRANGES]; +int numdepthfxranges = 0; +vec depthfxmin(1e16f, 1e16f, 1e16f), depthfxmax(1e16f, 1e16f, 1e16f); + +static struct depthfxtexture : rendertarget +{ + const GLenum *colorformats() const + { + static const GLenum colorfmts[] = { GL_RG16F, GL_RGB16F, GL_RGBA, GL_RGBA8, GL_RGB, GL_RGB8, GL_FALSE }; + return &colorfmts[fpdepthfx && hasTF ? (hasTRG ? 0 : 1) : 2]; + } + + float eyedepth(const vec &p) const + { + return max(-cammatrix.transform(p).z, 0.0f); + } + + void addscissorvert(const vec &v, float &sx1, float &sy1, float &sx2, float &sy2) + { + vec p = camprojmatrix.perspectivetransform(v); + sx1 = min(sx1, p.x); + sy1 = min(sy1, p.y); + sx2 = max(sx2, p.x); + sy2 = max(sy2, p.y); + } + + bool addscissorbox(const vec ¢er, float size) + { + float sx1, sy1, sx2, sy2; + calcspherescissor(center, size, sx1, sy1, sx2, sy2); + return addblurtiles(sx1, sy1, sx2, sy2); + } + + bool addscissorbox(const vec &bbmin, const vec &bbmax) + { + float sx1 = 1, sy1 = 1, sx2 = -1, sy2 = -1; + loopi(8) + { + vec v(i&1 ? bbmax.x : bbmin.x, i&2 ? bbmax.y : bbmin.y, i&4 ? bbmax.z : bbmin.z); + addscissorvert(v, sx1, sy1, sx2, sy2); + } + return addblurtiles(sx1, sy1, sx2, sy2); + } + + bool screenrect() const { return true; } + bool filter() const { return blurdepthfx!=0; } + bool highprecision() const { return colorfmt==GL_RG16F || colorfmt==GL_RGB16F; } + bool emulatehighprecision() const { return depthfxemuprecision && !blurdepthfx; } + + bool shouldrender() + { + extern void finddepthfxranges(); + finddepthfxranges(); + return (numdepthfxranges && scissorx1 < scissorx2 && scissory1 < scissory2) || debugdepthfx; + } + + bool dorender() + { + glClearColor(1, 1, 1, 1); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + depthfxing = true; + refracting = -1; + + extern void renderdepthobstacles(const vec &bbmin, const vec &bbmax, float scale, float *ranges, int numranges); + float scale = depthfxscale; + float *ranges = depthfxranges; + int numranges = numdepthfxranges; + if(highprecision()) + { + scale = depthfxfpscale; + ranges = NULL; + numranges = 0; + } + else if(emulatehighprecision()) + { + scale = depthfxfpscale; + ranges = NULL; + numranges = -3; + } + renderdepthobstacles(depthfxmin, depthfxmax, scale, ranges, numranges); + + refracting = 0; + depthfxing = false; + + return numdepthfxranges > 0; + } + + void dodebug(int w, int h) + { + if(numdepthfxranges > 0) + { + gle::colorf(0, 1, 0); + debugscissor(w, h, true); + gle::colorf(0, 0, 1); + debugblurtiles(w, h, true); + gle::colorf(1, 1, 1); + } + } +} depthfxtex; + +void cleanupdepthfx() +{ + depthfxtex.cleanup(true); +} + +void viewdepthfxtex() +{ + if(!depthfx) return; + depthfxtex.debug(); +} + +bool depthfxing = false; + +bool binddepthfxtex() +{ + if(!reflecting && !refracting && depthfx && depthfxtex.rendertex && numdepthfxranges>0) + { + glActiveTexture_(GL_TEXTURE2); + glBindTexture(GL_TEXTURE_2D, depthfxtex.rendertex); + glActiveTexture_(GL_TEXTURE0); + return true; + } + return false; +} + +void binddepthfxparams(float blend, float minblend = 0, bool allow = true, void *owner = NULL) +{ + if(!reflecting && !refracting && depthfx && depthfxtex.rendertex && numdepthfxranges>0) + { + float scale = 0, offset = -1, texscale = 0; + if(!depthfxtex.highprecision()) + { + float select[4] = { 0, 0, 0, 0 }; + if(!depthfxtex.emulatehighprecision()) + { + loopi(numdepthfxranges) if(depthfxowners[i]==owner) + { + select[i] = float(depthfxscale)/blend; + scale = 1.0f/blend; + offset = -float(depthfxranges[i] - depthfxbias)/blend; + break; + } + } + else if(allow) + { + select[0] = float(depthfxfpscale)/blend; + select[1] = select[0]/256; + select[2] = select[1]/256; + scale = 1.0f/blend; + offset = 0; + } + LOCALPARAMF(depthfxselect, select[0], select[1], select[2], select[3]); + } + else if(allow) + { + scale = 1.0f/blend; + offset = 0; + texscale = float(depthfxfpscale)/blend; + } + LOCALPARAMF(depthfxparams, scale, offset, texscale, minblend); + } +} + +void drawdepthfxtex() +{ + if(!depthfx) return; + + depthfxtex.render(1< 0) + { + int remaining = expire - lastmillis; + if(flags&DL_EXPAND) + curradius = initradius + (radius - initradius) * (1.0f - remaining/float(fade + peak)); + else if(!(flags&DL_FLASH) && remaining > fade) + curradius = initradius + (radius - initradius) * (1.0f - float(remaining - fade)/peak); + else if(flags&DL_SHRINK) + curradius = (radius*remaining)/fade; + else curradius = radius; + } + else curradius = radius; + } + + void calccolor() + { + if(flags&DL_FLASH || peak <= 0) curcolor = color; + else + { + int peaking = expire - lastmillis - fade; + if(peaking <= 0) curcolor = color; + else curcolor.lerp(initcolor, color, 1.0f - float(peaking)/peak); + } + + float intensity = 1.0f; + if(fade > 0) + { + int fading = expire - lastmillis; + if(fading < fade) intensity = float(fading)/fade; + } + curcolor.mul(intensity); + // KLUGE: this prevents nvidia drivers from trying to recompile dynlight fragment programs + loopk(3) if(fmod(curcolor[k], 1.0f/256) < 0.001f) curcolor[k] += 0.001f; + } +}; + +vector dynlights; +vector closedynlights; + +void adddynlight(const vec &o, float radius, const vec &color, int fade, int peak, int flags, float initradius, const vec &initcolor, physent *owner) +{ + if(!maxdynlights) return; + if(o.dist(camera1->o) > dynlightdist || radius <= 0) return; + + int insert = 0, expire = fade + peak + lastmillis; + loopvrev(dynlights) if(expire>=dynlights[i].expire) { insert = i+1; break; } + dynlight d; + d.o = d.hud = o; + d.radius = radius; + d.initradius = initradius; + d.color = color; + d.initcolor = initcolor; + d.fade = fade; + d.peak = peak; + d.expire = expire; + d.flags = flags; + d.owner = owner; + dynlights.insert(insert, d); +} + +void cleardynlights() +{ + int faded = -1; + loopv(dynlights) if(lastmillis0) dynlights.remove(0, faded); +} + +void removetrackeddynlights(physent *owner) +{ + loopvrev(dynlights) if(owner ? dynlights[i].owner == owner : dynlights[i].owner != NULL) dynlights.remove(i); +} + +void updatedynlights() +{ + cleardynlights(); + game::adddynlights(); + + loopv(dynlights) + { + dynlight &d = dynlights[i]; + if(d.owner) game::dynlighttrack(d.owner, d.o, d.hud); + d.calcradius(); + d.calccolor(); + } +} + +int finddynlights() +{ + closedynlights.setsize(0); + if(!maxdynlights) return 0; + physent e; + e.type = ENT_CAMERA; + loopvj(dynlights) + { + dynlight &d = dynlights[j]; + if(d.curradius <= 0) continue; + d.dist = camera1->o.dist(d.o) - d.curradius; + if(d.dist > dynlightdist || isfoggedsphere(d.curradius, d.o) || pvsoccludedsphere(d.o, d.curradius)) + continue; + if(reflecting || refracting > 0) + { + if(d.o.z + d.curradius < reflectz) continue; + } + else if(refracting < 0 && d.o.z - d.curradius > reflectz) continue; + e.o = d.o; + e.radius = e.xradius = e.yradius = e.eyeheight = e.aboveeye = d.curradius; + if(!collide(&e, vec(0, 0, 0), 0, false)) continue; + + int insert = 0; + loopvrev(closedynlights) if(d.dist >= closedynlights[i]->dist) { insert = i+1; break; } + closedynlights.insert(insert, &d); + if(closedynlights.length() >= DYNLIGHTMASK) break; + } + return closedynlights.length(); +} + +bool getdynlight(int n, vec &o, float &radius, vec &color) +{ + if(!closedynlights.inrange(n)) return false; + dynlight &d = *closedynlights[n]; + o = d.o; + radius = d.curradius; + color = d.curcolor; + return true; +} + +void dynlightreaching(const vec &target, vec &color, vec &dir, bool hud) +{ + vec dyncolor(0, 0, 0);//, dyndir(0, 0, 0); + loopv(dynlights) + { + dynlight &d = dynlights[i]; + if(d.curradius<=0) continue; + + vec ray(hud ? d.hud : d.o); + ray.sub(target); + float mag = ray.squaredlen(); + if(mag >= d.curradius*d.curradius) continue; + + vec color = d.curcolor; + color.mul(1 - sqrtf(mag)/d.curradius); + dyncolor.add(color); + //dyndir.add(ray.mul(intensity/mag)); + } +#if 0 + if(!dyndir.iszero()) + { + dyndir.normalize(); + float x = dyncolor.magnitude(), y = color.magnitude(); + if(x+y>0) + { + dir.mul(x); + dyndir.mul(y); + dir.add(dyndir).div(x+y); + if(dir.iszero()) dir = vec(0, 0, 1); + else dir.normalize(); + } + } +#endif + color.add(dyncolor); +} + +void calcdynlightmask(vtxarray *va) +{ + uint mask = 0; + int offset = 0; + loopv(closedynlights) + { + dynlight &d = *closedynlights[i]; + if(d.o.dist_to_bb(va->geommin, va->geommax) >= d.curradius) continue; + + mask |= (i+1)<= maxdynlights*DYNLIGHTBITS) break; + } + va->dynlightmask = mask; +} + +int setdynlights(vtxarray *va) +{ + if(closedynlights.empty() || !va->dynlightmask) return 0; + + extern bool minimizedynlighttcusage(); + + static vec4 posv[MAXDYNLIGHTS]; + static vec colorv[MAXDYNLIGHTS]; + + int index = 0; + for(uint mask = va->dynlightmask; mask; mask >>= DYNLIGHTBITS, index++) + { + dynlight &d = *closedynlights[(mask&DYNLIGHTMASK)-1]; + + float scale = 1.0f/d.curradius; + vec origin = vec(d.o).mul(-scale); + + if(index>0 && minimizedynlighttcusage()) + { + scale /= posv[0].w; + origin.sub(vec(posv[0]).mul(scale)); + } + + posv[index] = vec4(origin, scale); + colorv[index] = d.curcolor; + } + + GLOBALPARAMV(dynlightpos, posv, index); + GLOBALPARAMV(dynlightcolor, colorv, index); + + return index; +} + diff --git a/src/engine/engine.h b/src/engine/engine.h new file mode 100644 index 0000000..9f41f45 --- /dev/null +++ b/src/engine/engine.h @@ -0,0 +1,613 @@ +#ifndef __ENGINE_H__ +#define __ENGINE_H__ + +#include "cube.h" +#include "world.h" + +#ifndef STANDALONE + +#include "octa.h" +#include "lightmap.h" +#include "bih.h" +#include "texture.h" +#include "model.h" + +extern dynent *player; +extern physent *camera1; // special ent that acts as camera, same object as player1 in FPS mode + +extern int worldscale, worldsize; +extern int mapversion; +extern char *maptitle; +extern vector texmru; +extern int xtraverts, xtravertsva; +extern const ivec cubecoords[8]; +extern const ivec facecoords[6][4]; +extern const uchar fv[6][4]; +extern const uchar fvmasks[64]; +extern const uchar faceedgesidx[6][4]; +extern bool inbetweenframes, renderedframe; + +extern SDL_Window *screen; +extern int screenw, screenh; +extern int zpass; + +extern vector entgroup; + +// rendertext +struct font +{ + struct charinfo + { + short x, y, w, h, offsetx, offsety, advance, tex; + }; + + char *name; + vector texs; + vector chars; + int charoffset, defaultw, defaulth, scale; + + font() : name(NULL) {} + ~font() { DELETEA(name); } +}; + +#define FONTH (curfont->scale) +#define FONTW (FONTH/2) +#define MINRESW 640 +#define MINRESH 480 + +extern font *curfont; +extern const matrix4x3 *textmatrix; + +extern void reloadfonts(); + +// texture +extern int hwtexsize, hwcubetexsize, hwmaxaniso, maxtexsize; + +extern Texture *textureload(const char *name, int clamp = 0, bool mipit = true, bool msg = true); +extern int texalign(const void *data, int w, int bpp); +extern void cleanuptexture(Texture *t); +extern uchar *loadalphamask(Texture *t); +extern void loadlayermasks(); +extern Texture *cubemapload(const char *name, bool mipit = true, bool msg = true, bool transient = false); +extern void drawcubemap(int size, const vec &o, float yaw, float pitch, const cubemapside &side, bool onlysky = false); +extern void loadshaders(); +extern void setuptexparameters(int tnum, void *pixels, int clamp, int filter, GLenum format = GL_RGB, GLenum target = GL_TEXTURE_2D, bool swizzle = false); +extern void createtexture(int tnum, int w, int h, void *pixels, int clamp, int filter, GLenum component = GL_RGB, GLenum target = GL_TEXTURE_2D, int pw = 0, int ph = 0, int pitch = 0, bool resize = true, GLenum format = GL_FALSE, bool swizzle = false); +extern void blurtexture(int n, int bpp, int w, int h, uchar *dst, const uchar *src, int margin = 0); +extern void blurnormals(int n, int w, int h, bvec *dst, const bvec *src, int margin = 0); +extern void renderpostfx(); +extern void initenvmaps(); +extern void genenvmaps(); +extern ushort closestenvmap(const vec &o); +extern ushort closestenvmap(int orient, const ivec &co, int size); +extern GLuint lookupenvmap(ushort emid); +extern GLuint lookupenvmap(Slot &slot); +extern bool reloadtexture(Texture &tex); +extern bool reloadtexture(const char *name); +extern void setuptexcompress(); +extern void clearslots(); +extern void compacteditvslots(); +extern void compactmruvslots(); +extern void compactvslots(cube *c, int n = 8); +extern void compactvslot(int &index); +extern void compactvslot(VSlot &vs); +extern int compactvslots(); +extern void reloadtextures(); +extern void cleanuptextures(); + +// shadowmap + +extern int shadowmap, shadowmapcasters; +extern bool shadowmapping; +extern matrix4 shadowmatrix; + +extern bool isshadowmapcaster(const vec &o, float rad); +extern bool addshadowmapcaster(const vec &o, float xyrad, float zrad); +extern bool isshadowmapreceiver(vtxarray *va); +extern void rendershadowmap(); +extern void pushshadowmap(); +extern void popshadowmap(); +extern void rendershadowmapreceivers(); +extern void guessshadowdir(); + +// pvs +extern void clearpvs(); +extern bool pvsoccluded(const ivec &bbmin, const ivec &bbmax); +extern bool pvsoccludedsphere(const vec ¢er, float radius); +extern bool waterpvsoccluded(int height); +extern void setviewcell(const vec &p); +extern void savepvs(stream *f); +extern void loadpvs(stream *f, int numpvs); +extern int getnumviewcells(); + +static inline bool pvsoccluded(const ivec &bborigin, int size) +{ + return pvsoccluded(bborigin, ivec(bborigin).add(size)); +} + +// rendergl +extern bool hasVAO, hasFBO, hasAFBO, hasDS, hasTF, hasTRG, hasTSW, hasS3TC, hasFXT1, hasLATC, hasRGTC, hasAF, hasFBB, hasUBO, hasMBR; +extern int glversion, glslversion, glcompat; + +enum { DRAWTEX_NONE = 0, DRAWTEX_ENVMAP, DRAWTEX_MINIMAP, DRAWTEX_MODELPREVIEW }; + +extern float curfov, fovy, aspect, forceaspect; +extern int drawtex; +extern bool renderedgame; +extern const matrix4 viewmatrix; +extern matrix4 cammatrix, projmatrix, camprojmatrix, invcammatrix, invcamprojmatrix; +extern bvec fogcolor; +extern vec curfogcolor; +extern int fog; +extern float curfogstart, curfogend; + +extern void gl_checkextensions(); +extern void gl_init(); +extern void gl_resize(); +extern void cleanupgl(); +extern void rendergame(bool mainpass = false); +extern void invalidatepostfx(); +extern void gl_drawhud(); +extern void gl_drawframe(); +extern void gl_drawmainmenu(); +extern void drawminimap(); +extern void drawtextures(); +extern void enablepolygonoffset(GLenum type); +extern void disablepolygonoffset(GLenum type); +extern void calcspherescissor(const vec ¢er, float size, float &sx1, float &sy1, float &sx2, float &sy2); +extern int pushscissor(float sx1, float sy1, float sx2, float sy2); +extern void popscissor(); +extern void recomputecamera(); +extern void screenquad(); +extern void screenquad(float sw, float sh); +extern void screenquadflipped(float sw, float sh); +extern void screenquad(float sw, float sh, float sw2, float sh2); +extern void screenquadoffset(float x, float y, float w, float h); +extern void screenquadoffset(float x, float y, float w, float h, float x2, float y2, float w2, float h2); +extern void hudquad(float x, float y, float w, float h, float tx = 0, float ty = 0, float tw = 1, float th = 1); +extern void setfogcolor(const vec &v); +extern void zerofogcolor(); +extern void resetfogcolor(); +extern void setfogdist(float start, float end); +extern void clearfogdist(); +extern void resetfogdist(); +extern void writecrosshairs(stream *f); + +namespace modelpreview +{ + extern void start(int x, int y, int w, int h, bool background = true); + extern void end(); +} + +// renderextras +extern void render3dbox(vec &o, float tofloor, float toceil, float xradius, float yradius = 0); + +// octa +extern cube *newcubes(uint face = F_EMPTY, int mat = MAT_AIR); +extern cubeext *growcubeext(cubeext *ext, int maxverts); +extern void setcubeext(cube &c, cubeext *ext); +extern cubeext *newcubeext(cube &c, int maxverts = 0, bool init = true); +extern void getcubevector(cube &c, int d, int x, int y, int z, ivec &p); +extern void setcubevector(cube &c, int d, int x, int y, int z, const ivec &p); +extern int familysize(const cube &c); +extern void freeocta(cube *c); +extern void discardchildren(cube &c, bool fixtex = false, int depth = 0); +extern void optiface(uchar *p, cube &c); +extern void validatec(cube *c, int size = 0); +extern bool isvalidcube(const cube &c); +extern ivec lu; +extern int lusize; +extern cube &lookupcube(const ivec &to, int tsize = 0, ivec &ro = lu, int &rsize = lusize); +extern const cube *neighbourstack[32]; +extern int neighbourdepth; +extern const cube &neighbourcube(const cube &c, int orient, const ivec &co, int size, ivec &ro = lu, int &rsize = lusize); +extern void resetclipplanes(); +extern int getmippedtexture(const cube &p, int orient); +extern void forcemip(cube &c, bool fixtex = true); +extern bool subdividecube(cube &c, bool fullcheck=true, bool brighten=true); +extern void edgespan2vectorcube(cube &c); +extern int faceconvexity(const ivec v[4]); +extern int faceconvexity(const ivec v[4], int &vis); +extern int faceconvexity(const vertinfo *verts, int numverts, int size); +extern int faceconvexity(const cube &c, int orient); +extern void calcvert(const cube &c, const ivec &co, int size, ivec &vert, int i, bool solid = false); +extern void calcvert(const cube &c, const ivec &co, int size, vec &vert, int i, bool solid = false); +extern uint faceedges(const cube &c, int orient); +extern bool collapsedface(const cube &c, int orient); +extern bool touchingface(const cube &c, int orient); +extern bool flataxisface(const cube &c, int orient); +extern bool collideface(const cube &c, int orient); +extern int genclipplane(const cube &c, int i, vec *v, plane *clip); +extern void genclipplanes(const cube &c, const ivec &co, int size, clipplanes &p, bool collide = true); +extern bool visibleface(const cube &c, int orient, const ivec &co, int size, ushort mat = MAT_AIR, ushort nmat = MAT_AIR, ushort matmask = MATF_VOLUME); +extern int classifyface(const cube &c, int orient, const ivec &co, int size); +extern int visibletris(const cube &c, int orient, const ivec &co, int size, ushort nmat = MAT_ALPHA, ushort matmask = MAT_ALPHA); +extern int visibleorient(const cube &c, int orient); +extern void genfaceverts(const cube &c, int orient, ivec v[4]); +extern int calcmergedsize(int orient, const ivec &co, int size, const vertinfo *verts, int numverts); +extern void invalidatemerges(cube &c, const ivec &co, int size, bool msg); +extern void calcmerges(); + +extern int mergefaces(int orient, facebounds *m, int sz); +extern void mincubeface(const cube &cu, int orient, const ivec &o, int size, const facebounds &orig, facebounds &cf, ushort nmat = MAT_AIR, ushort matmask = MATF_VOLUME); + +static inline cubeext &ext(cube &c) +{ + return *(c.ext ? c.ext : newcubeext(c)); +} + +// ents +extern char *entname(entity &e); +extern bool haveselent(); +extern undoblock *copyundoents(undoblock *u); +extern void pasteundoent(int idx, const entity &ue); +extern void pasteundoents(undoblock *u); + +// octaedit +extern void cancelsel(); +extern void rendertexturepanel(int w, int h); +extern void addundo(undoblock *u); +extern void commitchanges(bool force = false); +extern void rendereditcursor(); +extern void tryedit(); + +extern bool prefabloaded(const char *name); +extern void renderprefab(const char *name, const vec &o, float yaw, float pitch, float roll, float size = 1, const vec &color = vec(1, 1, 1)); +extern void previewprefab(const char *name, const vec &color); + +// octarender +extern vector tjoints; +extern vector varoot, valist; + +extern ushort encodenormal(const vec &n); +extern vec decodenormal(ushort norm); +extern void guessnormals(const vec *pos, int numverts, vec *normals); +extern void reduceslope(ivec &n); +extern void findtjoints(); +extern void octarender(); +extern void allchanged(bool load = false); +extern void clearvas(cube *c); +extern void destroyva(vtxarray *va, bool reparent = true); +extern bool readva(vtxarray *va, ushort *&edata, vertex *&vdata); +extern void updatevabb(vtxarray *va, bool force = false); +extern void updatevabbs(bool force = false); + +// renderva +extern vtxarray *visibleva, *reflectedva; + +extern void visiblecubes(bool cull = true); +extern void setvfcP(float z = -1, const vec &bbmin = vec(-1, -1, -1), const vec &bbmax = vec(1, 1, 1)); +extern void savevfcP(); +extern void restorevfcP(); +extern void rendergeom(float causticspass = 0, bool fogpass = false); +extern void renderalphageom(bool fogpass = false); +extern void rendermapmodels(); +extern void renderreflectedgeom(bool causticspass = false, bool fogpass = false); +extern void renderreflectedmapmodels(); +extern void renderoutline(); +extern bool rendersky(bool explicitonly = false); + +extern bool isfoggedsphere(float rad, const vec &cv); +extern int isvisiblesphere(float rad, const vec &cv); +extern bool bboccluded(const ivec &bo, const ivec &br); +extern occludequery *newquery(void *owner); +extern void startquery(occludequery *query); +extern void endquery(occludequery *query); +extern bool checkquery(occludequery *query, bool nowait = false); +extern void resetqueries(); +extern int getnumqueries(); +extern void startbb(bool mask = true); +extern void endbb(bool mask = true); +extern void drawbb(const ivec &bo, const ivec &br); + +extern int oqfrags; + +// dynlight + +extern void updatedynlights(); +extern int finddynlights(); +extern void calcdynlightmask(vtxarray *va); +extern int setdynlights(vtxarray *va); +extern bool getdynlight(int n, vec &o, float &radius, vec &color); + +// material + +extern int showmat; + +extern int findmaterial(const char *name); +extern const char *findmaterialname(int mat); +extern const char *getmaterialdesc(int mat, const char *prefix = ""); +extern void genmatsurfs(const cube &c, const ivec &co, int size, vector &matsurfs); +extern void rendermatsurfs(materialsurface *matbuf, int matsurfs); +extern void rendermatgrid(materialsurface *matbuf, int matsurfs); +extern int optimizematsurfs(materialsurface *matbuf, int matsurfs); +extern void setupmaterials(int start = 0, int len = 0); +extern void rendermaterials(); +extern int visiblematerial(const cube &c, int orient, const ivec &co, int size, ushort matmask = MATF_VOLUME); + +// water +extern int refracting, refractfog; +extern bool reflecting, fading, fogging; +extern float reflectz; +extern int reflectdist, vertwater, waterrefract, waterreflect, waterfade, caustics, waterfallrefract; + +#define GETMATIDXVAR(name, var, type) \ + type get##name##var(int mat) \ + { \ + switch(mat&MATF_INDEX) \ + { \ + default: case 0: return name##var; \ + case 1: return name##2##var; \ + case 2: return name##3##var; \ + case 3: return name##4##var; \ + } \ + } + +extern const bvec &getwatercolor(int mat); +extern const bvec &getwaterfallcolor(int mat); +extern int getwaterfog(int mat); +extern const bvec &getlavacolor(int mat); +extern int getlavafog(int mat); +extern const bvec &getglasscolor(int mat); + +extern void cleanreflections(); +extern void queryreflections(); +extern void drawreflections(); +extern void renderwater(); +extern void setuplava(Texture *tex, float scale); +extern void renderlava(const materialsurface &m); +extern void flushlava(); +extern void loadcaustics(bool force = false); +extern void preloadwatershaders(bool force = false); + +// glare +extern bool glaring; + +extern void drawglaretex(); +extern void addglare(); + +// depthfx +extern bool depthfxing; + +extern void drawdepthfxtex(); + +// server +extern vector gameargs; + +extern void initserver(bool listen, bool dedicated); +extern void cleanupserver(); +extern void serverslice(bool dedicated, uint timeout); +extern void updatetime(); + +extern ENetSocket connectmaster(bool wait); +extern void localclienttoserver(int chan, ENetPacket *); +extern void localconnect(); +extern bool serveroption(char *opt); + +// serverbrowser +extern bool resolverwait(const char *name, ENetAddress *address); +extern int connectwithtimeout(ENetSocket sock, const char *hostname, const ENetAddress &address); +extern void addserver(const char *name, int port = 0, const char *password = NULL, bool keep = false); +extern void writeservercfg(); + +// client +extern void localdisconnect(bool cleanup = true); +extern void localservertoclient(int chan, ENetPacket *packet); +extern void connectserv(const char *servername, int port, const char *serverpassword); +extern void abortconnect(); +extern void clientkeepalive(); + +// command +extern hashnameset idents; +extern int identflags; + +extern void clearoverrides(); +extern void writecfg(const char *name = NULL); + +extern void checksleep(int millis); +extern void clearsleep(bool clearoverrides = true); + +// console +extern void processtextinput(const char *str, int len); +extern void processkey(int code, bool isdown, int modstate = 0); +extern int rendercommand(int x, int y, int w); +extern int renderconsole(int w, int h, int abovehud); +extern void conoutf(const char *s, ...) PRINTFARGS(1, 2); +extern void conoutf(int type, const char *s, ...) PRINTFARGS(2, 3); +extern void resetcomplete(); +extern void complete(char *s, int maxlen, const char *cmdprefix); +const char *getkeyname(int code); +extern const char *addreleaseaction(char *s); +extern void writebinds(stream *f); +extern void writecompletions(stream *f); + +// main +enum +{ + NOT_INITING = 0, + INIT_GAME, + INIT_LOAD, + INIT_RESET +}; +extern int initing, numcpus; + +enum +{ + CHANGE_GFX = 1<<0, + CHANGE_SOUND = 1<<1 +}; +extern bool initwarning(const char *desc, int level = INIT_RESET, int type = CHANGE_GFX); + +extern bool grabinput, minimized; + +extern bool interceptkey(int sym); + +extern float loadprogress; +extern void renderbackground(const char *caption = NULL, Texture *mapshot = NULL, const char *mapname = NULL, const char *mapinfo = NULL, bool restore = false, bool force = false); +extern void renderprogress(float bar, const char *text, GLuint tex = 0, bool background = false); + +extern void getfps(int &fps, int &bestdiff, int &worstdiff); +extern void swapbuffers(bool overlay = true); +extern int getclockmillis(); + +enum { KR_CONSOLE = 1<<0, KR_GUI = 1<<1, KR_EDITMODE = 1<<2 }; + +extern void keyrepeat(bool on, int mask = ~0); + +enum { TI_CONSOLE = 1<<0, TI_GUI = 1<<1 }; + +extern void textinput(bool on, int mask = ~0); + +// menu +extern void menuprocess(); +extern void addchange(const char *desc, int type); +extern void clearchanges(int type); + +// physics +extern void mousemove(int dx, int dy); +extern bool overlapsdynent(const vec &o, float radius); +extern void rotatebb(vec ¢er, vec &radius, int yaw); +extern float shadowray(const vec &o, const vec &ray, float radius, int mode, extentity *t = NULL); +struct ShadowRayCache; +extern ShadowRayCache *newshadowraycache(); +extern void freeshadowraycache(ShadowRayCache *&cache); +extern void resetshadowraycache(ShadowRayCache *cache); +extern float shadowray(ShadowRayCache *cache, const vec &o, const vec &ray, float radius, int mode, extentity *t = NULL); + +// world + +extern vector outsideents; + +extern void entcancel(); +extern void entitiesinoctanodes(); +extern void attachentities(); +extern void freeoctaentities(cube &c); +extern bool pointinsel(const selinfo &sel, const vec &o); + +extern void resetmap(); +extern void startmap(const char *name); + +// rendermodel +struct mapmodelinfo { string name; model *m; }; + +extern bool modelloaded(const char *name); +extern void findanims(const char *pattern, vector &anims); +extern void loadskin(const char *dir, const char *altdir, Texture *&skin, Texture *&masks); +extern mapmodelinfo *getmminfo(int i); +extern void startmodelquery(occludequery *query); +extern void endmodelquery(); +extern void preloadmodelshaders(bool force = false); +extern void preloadusedmapmodels(bool msg = false, bool bih = false); + +static inline model *loadmapmodel(int n) +{ + extern vector mapmodels; + if(mapmodels.inrange(n)) + { + model *m = mapmodels[n].m; + return m ? m : loadmodel(NULL, n); + } + return NULL; +} + +// renderparticles +extern void initparticles(); +extern void clearparticles(); +extern void clearparticleemitters(); +extern void seedparticles(); +extern void updateparticles(); +extern void renderparticles(bool mainpass = false); +extern bool printparticles(extentity &e, char *buf, int len); + +// decal +extern void initdecals(); +extern void cleardecals(); +extern void renderdecals(bool mainpass = false); + +// blob + +enum +{ + BLOB_STATIC = 0, + BLOB_DYNAMIC +}; + +extern int showblobs; + +extern void initblobs(int type = -1); +extern void resetblobs(); +extern void renderblob(int type, const vec &o, float radius, float fade = 1); +extern void flushblobs(); + +// rendersky +extern int explicitsky; +extern double skyarea; +extern char *skybox; + +extern void setupsky(); +extern void drawskybox(int farplane, bool limited, bool force = false); +extern bool limitsky(); +extern bool shouldrenderskyenvmap(); +extern bool shouldclearskyboxglare(); + +// 3dgui +extern void g3d_render(); +extern void g3d_render2d(); +extern bool g3d_windowhit(bool on, bool act); +extern bool g3d_key(int code, bool isdown); +extern bool g3d_input(const char *str, int len); +// menus +extern int mainmenu; + +extern void clearmainmenu(); +extern void g3d_mainmenu(); + +// sound +extern void clearmapsounds(); +extern void checkmapsounds(); +extern void updatesounds(); +extern void preloadmapsounds(); + +extern void initmumble(); +extern void closemumble(); +extern void updatemumble(); + +// grass +extern void generategrass(); +extern void rendergrass(); +extern void cleanupgrass(); + +// blendmap +extern int blendpaintmode; + +struct BlendMapCache; +extern BlendMapCache *newblendmapcache(); +extern void freeblendmapcache(BlendMapCache *&cache); +extern bool setblendmaporigin(BlendMapCache *cache, const ivec &o, int size); +extern bool hasblendmap(BlendMapCache *cache); +extern uchar lookupblendmap(BlendMapCache *cache, const vec &pos); +extern void resetblendmap(); +extern void enlargeblendmap(); +extern void shrinkblendmap(int octant); +extern void optimizeblendmap(); +extern void stoppaintblendmap(); +extern void trypaintblendmap(); +extern void renderblendbrush(GLuint tex, float x, float y, float w, float h); +extern void renderblendbrush(); +extern bool loadblendmap(stream *f, int info); +extern void saveblendmap(stream *f); +extern uchar shouldsaveblendmap(); + +// recorder + +namespace recorder +{ + extern void stop(); + extern void capture(bool overlay = true); + extern void cleanup(); +} + +#endif + +#endif + diff --git a/src/engine/explosion.h b/src/engine/explosion.h new file mode 100644 index 0000000..626d5bf --- /dev/null +++ b/src/engine/explosion.h @@ -0,0 +1,249 @@ +namespace sphere +{ + + struct vert + { + vec pos; + ushort s, t; + } *verts = NULL; + GLushort *indices = NULL; + int numverts = 0, numindices = 0; + GLuint vbuf = 0, ebuf = 0; + + void init(int slices, int stacks) + { + numverts = (stacks+1)*(slices+1); + verts = new vert[numverts]; + float ds = 1.0f/slices, dt = 1.0f/stacks, t = 1.0f; + loopi(stacks+1) + { + float rho = M_PI*(1-t), s = 0.0f, sinrho = i && i < stacks ? sin(rho) : 0, cosrho = !i ? 1 : (i < stacks ? cos(rho) : -1); + loopj(slices+1) + { + float theta = j==slices ? 0 : 2*M_PI*s; + vert &v = verts[i*(slices+1) + j]; + v.pos = vec(-sin(theta)*sinrho, cos(theta)*sinrho, cosrho); + v.s = ushort(s*0xFFFF); + v.t = ushort(t*0xFFFF); + s += ds; + } + t -= dt; + } + + numindices = (stacks-1)*slices*3*2; + indices = new ushort[numindices]; + GLushort *curindex = indices; + loopi(stacks) + { + loopk(slices) + { + int j = i%2 ? slices-k-1 : k; + if(i) + { + *curindex++ = i*(slices+1)+j; + *curindex++ = (i+1)*(slices+1)+j; + *curindex++ = i*(slices+1)+j+1; + } + if(i+1 < stacks) + { + *curindex++ = i*(slices+1)+j+1; + *curindex++ = (i+1)*(slices+1)+j; + *curindex++ = (i+1)*(slices+1)+j+1; + } + } + } + + if(!vbuf) glGenBuffers_(1, &vbuf); + gle::bindvbo(vbuf); + glBufferData_(GL_ARRAY_BUFFER, numverts*sizeof(vert), verts, GL_STATIC_DRAW); + DELETEA(verts); + + if(!ebuf) glGenBuffers_(1, &ebuf); + gle::bindebo(ebuf); + glBufferData_(GL_ELEMENT_ARRAY_BUFFER, numindices*sizeof(GLushort), indices, GL_STATIC_DRAW); + DELETEA(indices); + } + + void enable() + { + if(!vbuf) init(12, 6); + + gle::bindvbo(vbuf); + gle::bindebo(ebuf); + + gle::vertexpointer(sizeof(vert), &verts->pos); + gle::texcoord0pointer(sizeof(vert), &verts->s, GL_UNSIGNED_SHORT, 2, GL_TRUE); + gle::enablevertex(); + gle::enabletexcoord0(); + } + + void draw() + { + glDrawRangeElements_(GL_TRIANGLES, 0, numverts-1, numindices, GL_UNSIGNED_SHORT, indices); + xtraverts += numindices; + glde++; + } + + void disable() + { + gle::disablevertex(); + gle::disabletexcoord0(); + + gle::clearvbo(); + gle::clearebo(); + } + + void cleanup() + { + if(vbuf) { glDeleteBuffers_(1, &vbuf); vbuf = 0; } + if(ebuf) { glDeleteBuffers_(1, &ebuf); ebuf = 0; } + } +} + +static const float WOBBLE = 1.25f; + +struct fireballrenderer : listrenderer +{ + fireballrenderer(const char *texname) + : listrenderer(texname, 0, PT_FIREBALL|PT_GLARE|PT_SHADER) + {} + + void startrender() + { + if(glaring) SETSHADER(explosionglare); + else if(!reflecting && !refracting && depthfx && depthfxtex.rendertex && numdepthfxranges>0) + { + if(!depthfxtex.highprecision()) SETSHADER(explosionsoft8); + else SETSHADER(explosionsoft); + } + else SETSHADER(explosion); + + sphere::enable(); + } + + void endrender() + { + sphere::disable(); + } + + void cleanup() + { + sphere::cleanup(); + } + + int finddepthfxranges(void **owners, float *ranges, int numranges, int maxranges, vec &bbmin, vec &bbmax) + { + static struct fireballent : physent + { + fireballent() + { + type = ENT_CAMERA; + } + } e; + + for(listparticle *p = list; p; p = p->next) + { + int ts = p->fade <= 5 ? 1 : lastmillis-p->millis; + float pmax = p->val, + size = p->fade ? float(ts)/p->fade : 1, + psize = (p->size + pmax * size)*WOBBLE; + if(2*(p->size + pmax)*WOBBLE < depthfxblend || + (!depthfxtex.highprecision() && !depthfxtex.emulatehighprecision() && psize > depthfxscale - depthfxbias) || + isfoggedsphere(psize, p->o)) continue; + + e.o = p->o; + e.radius = e.xradius = e.yradius = e.eyeheight = e.aboveeye = psize; + if(!::collide(&e, vec(0, 0, 0), 0, false)) continue; + + if(depthfxscissor==2 && !depthfxtex.addscissorbox(p->o, psize)) continue; + + vec dir = camera1->o; + dir.sub(p->o); + float dist = dir.magnitude(); + dir.mul(psize/dist).add(p->o); + float depth = depthfxtex.eyedepth(dir); + + loopk(3) + { + bbmin[k] = min(bbmin[k], p->o[k] - psize); + bbmax[k] = max(bbmax[k], p->o[k] + psize); + } + + int pos = numranges; + loopi(numranges) if(depth < ranges[i]) { pos = i; break; } + if(pos >= maxranges) continue; + + if(numranges > pos) + { + int moved = min(numranges-pos, maxranges-(pos+1)); + memmove(&ranges[pos+1], &ranges[pos], moved*sizeof(float)); + memmove(&owners[pos+1], &owners[pos], moved*sizeof(void *)); + } + if(numranges < maxranges) numranges++; + + ranges[pos] = depth; + owners[pos] = p; + } + + return numranges; + } + + void seedemitter(particleemitter &pe, const vec &o, const vec &d, int fade, float size, int gravity) + { + pe.maxfade = max(pe.maxfade, fade); + pe.extendbb(o, (size+1+pe.ent->attr2)*WOBBLE); + } + + void renderpart(listparticle *p, const vec &o, const vec &d, int blend, int ts) + { + float pmax = p->val, + size = p->fade ? float(ts)/p->fade : 1, + psize = p->size + pmax * size; + + if(isfoggedsphere(psize*WOBBLE, p->o)) return; + + vec dir = vec(o).sub(camera1->o), s, t; + float dist = dir.magnitude(); + bool inside = dist <= psize*WOBBLE; + if(inside) + { + s = camright; + t = camup; + } + else + { + if(reflecting) { dir.z = o.z - reflectz; dist = dir.magnitude(); } + float mag2 = dir.magnitude2(); + dir.x /= mag2; + dir.y /= mag2; + dir.z /= dist; + s = vec(dir.y, -dir.x, 0); + t = vec(dir.x*dir.z, dir.y*dir.z, -mag2/dist); + } + + matrix3 rot(lastmillis/1000.0f*143*RAD, vec(1/SQRT3, 1/SQRT3, 1/SQRT3)); + LOCALPARAM(texgenS, rot.transposedtransform(s)); + LOCALPARAM(texgenT, rot.transposedtransform(t)); + + matrix4 m(rot, o); + m.scale(psize, psize, inside ? -psize : psize); + m.mul(camprojmatrix, m); + LOCALPARAM(explosionmatrix, m); + + LOCALPARAM(center, o); + LOCALPARAMF(millis, lastmillis/1000.0f); + LOCALPARAMF(blendparams, inside ? 0.5f : 4, inside ? 0.25f : 0); + binddepthfxparams(depthfxblend, inside ? blend/(2*255.0f) : 0, 2*(p->size + pmax)*WOBBLE >= depthfxblend, p); + + int passes = !reflecting && !refracting && inside ? 2 : 1; + loopi(passes) + { + gle::color(p->color, i ? blend/2 : blend); + if(i) glDepthFunc(GL_GEQUAL); + sphere::draw(); + if(i) glDepthFunc(GL_LESS); + } + } +}; +static fireballrenderer fireballs("packages/particles/explosion.png"), bluefireballs("packages/particles/plasma.png"); + diff --git a/src/engine/glare.cpp b/src/engine/glare.cpp new file mode 100644 index 0000000..dc639ad --- /dev/null +++ b/src/engine/glare.cpp @@ -0,0 +1,71 @@ +#include "engine.h" +#include "rendertarget.h" + +static struct glaretexture : rendertarget +{ + bool dorender() + { + extern void drawglare(); + drawglare(); + return true; + } +} glaretex; + +void cleanupglare() +{ + glaretex.cleanup(true); +} + +VARFP(glaresize, 6, 8, 10, cleanupglare()); +VARP(glare, 0, 0, 1); +VARP(blurglare, 0, 4, 7); +VARP(blurglareaspect, 0, 1, 1); +VARP(blurglaresigma, 1, 50, 200); + +VAR(debugglare, 0, 0, 1); + +void viewglaretex() +{ + if(!glare) return; + glaretex.debug(); +} + +bool glaring = false; + +void drawglaretex() +{ + if(!glare) return; + + int w = 1< (1<<5) && (screenw*h)/w >= (screenh*4)/3) h /= 2; + blury = ((1 + 4*blurglare)*(screenw*h)/w + screenh*2)/(screenh*4); + blury = clamp(blury, 1, MAXBLURRADIUS); + } + + glaretex.render(w, h, blurglare, blurglaresigma/100.0f, blury); +} + +FVAR(glaremod, 0.5f, 0.75f, 1); +FVARP(glarescale, 0, 1, 8); + +void addglare() +{ + if(!glare) return; + + glEnable(GL_BLEND); + glBlendFunc(GL_ONE, GL_ONE); + + SETSHADER(screenrect); + + glBindTexture(GL_TEXTURE_2D, glaretex.rendertex); + + float g = glarescale*glaremod; + gle::colorf(g, g, g); + + screenquad(1, 1); + + glDisable(GL_BLEND); +} + diff --git a/src/engine/grass.cpp b/src/engine/grass.cpp new file mode 100644 index 0000000..f91b6dc --- /dev/null +++ b/src/engine/grass.cpp @@ -0,0 +1,356 @@ +#include "engine.h" + +VARP(grass, 0, 0, 1); +VAR(dbggrass, 0, 0, 1); +VARP(grassdist, 0, 256, 10000); +FVARP(grasstaper, 0, 0.2, 1); +FVARP(grassstep, 0.5, 3, 8); +VARP(grassheight, 1, 4, 64); +VARP(grassmargin, 0, 8, 32); +FVAR(grassmarginfade, 0, 0.5f, 1); + +#define NUMGRASSWEDGES 8 + +static struct grasswedge +{ + vec dir, across, edge1, edge2; + plane bound1, bound2; + bvec4 vertbounds; + + grasswedge(int i) : + dir(2*M_PI*(i+0.5f)/float(NUMGRASSWEDGES), 0), + across(2*M_PI*((i+0.5f)/float(NUMGRASSWEDGES) + 0.25f), 0), + edge1(vec(2*M_PI*i/float(NUMGRASSWEDGES), 0).div(cos(M_PI/NUMGRASSWEDGES))), + edge2(vec(2*M_PI*(i+1)/float(NUMGRASSWEDGES), 0).div(cos(M_PI/NUMGRASSWEDGES))), + bound1(vec(2*M_PI*(i/float(NUMGRASSWEDGES) - 0.25f), 0), 0), + bound2(vec(2*M_PI*((i+1)/float(NUMGRASSWEDGES) + 0.25f), 0), 0) + { + across.div(-across.dot(bound1)); + + bvec vertbound1(bound1), vertbound2(bound2); + vertbounds = bvec4(vertbound1.x, vertbound1.y, vertbound2.x, vertbound2.y); + vertbounds.flip(); + } +} grasswedges[NUMGRASSWEDGES] = { 0, 1, 2, 3, 4, 5, 6, 7 }; + +struct grassvert +{ + vec pos; + bvec4 color; + vec2 tc; + svec2 lm; + bvec4 bounds; +}; + +static vector grassverts; +static GLuint grassvbo = 0; +static int grassvbosize = 0; + +struct grassgroup +{ + const grasstri *tri; + float dist; + int tex, lmtex, offset, numquads; +}; + +static vector grassgroups; + +#define NUMGRASSOFFSETS 32 + +static float grassoffsets[NUMGRASSOFFSETS] = { -1 }, grassanimoffsets[NUMGRASSOFFSETS]; +static int lastgrassanim = -1; + +VARR(grassanimmillis, 0, 3000, 60000); +FVARR(grassanimscale, 0, 0.03f, 1); + +static void animategrass() +{ + loopi(NUMGRASSOFFSETS) grassanimoffsets[i] = grassanimscale*sinf(2*M_PI*(grassoffsets[i] + lastmillis/float(grassanimmillis))); + lastgrassanim = lastmillis; +} + +VARR(grassscale, 1, 2, 64); +bvec grasscolor(255, 255, 255); +HVARFR(grasscolour, 0, 0xFFFFFF, 0xFFFFFF, +{ + if(!grasscolour) grasscolour = 0xFFFFFF; + grasscolor = bvec((grasscolour>>16)&0xFF, (grasscolour>>8)&0xFF, grasscolour&0xFF); +}); +FVARR(grassalpha, 0, 1, 1); + +static void gengrassquads(grassgroup *&group, const grasswedge &w, const grasstri &g, Texture *tex) +{ + float t = camera1->o.dot(w.dir); + int tstep = int(ceil(t/grassstep)); + float tstart = tstep*grassstep, + t0 = w.dir.dot(g.v[0]), t1 = w.dir.dot(g.v[1]), t2 = w.dir.dot(g.v[2]), t3 = w.dir.dot(g.v[3]), + tmin = min(min(t0, t1), min(t2, t3)), + tmax = max(max(t0, t1), max(t2, t3)); + if(tmax < tstart || tmin > t + grassdist) return; + + int minstep = max(int(ceil(tmin/grassstep)) - tstep, 1), + maxstep = int(floor(min(tmax, t + grassdist)/grassstep)) - tstep, + numsteps = maxstep - minstep + 1; + + float texscale = (grassscale*tex->ys)/float(grassheight*tex->xs), animscale = grassheight*texscale; + vec tc; + tc.cross(g.surface, w.dir).mul(texscale); + + int offset = tstep + maxstep; + if(offset < 0) offset = NUMGRASSOFFSETS - (-offset)%NUMGRASSOFFSETS; + offset += numsteps + NUMGRASSOFFSETS - numsteps%NUMGRASSOFFSETS; + + float leftdist = t0; + const vec *leftv = &g.v[0]; + if(t1 > leftdist) { leftv = &g.v[1]; leftdist = t1; } + if(t2 > leftdist) { leftv = &g.v[2]; leftdist = t2; } + if(t3 > leftdist) { leftv = &g.v[3]; leftdist = t3; } + float rightdist = leftdist; + const vec *rightv = leftv; + + vec across(w.across.x, w.across.y, g.surface.zdelta(w.across)), leftdir(0, 0, 0), rightdir(0, 0, 0), leftp = *leftv, rightp = *rightv; + float taperdist = grassdist*grasstaper, + taperscale = 1.0f / (grassdist - taperdist), + dist = maxstep*grassstep + tstart, + leftb = 0, rightb = 0, leftdb = 0, rightdb = 0; + for(int i = maxstep; i >= minstep; i--, offset--, leftp.add(leftdir), rightp.add(rightdir), leftb += leftdb, rightb += rightdb, dist -= grassstep) + { + if(dist <= leftdist) + { + const vec *prev = leftv; + float prevdist = leftdist; + if(--leftv < g.v) leftv += g.numv; + leftdist = leftv->dot(w.dir); + if(dist <= leftdist) + { + prev = leftv; + prevdist = leftdist; + if(--leftv < g.v) leftv += g.numv; + leftdist = leftv->dot(w.dir); + } + leftdir = vec(*leftv).sub(*prev); + leftdir.mul(grassstep/-w.dir.dot(leftdir)); + leftp = vec(leftdir).mul((prevdist - dist)/grassstep).add(*prev); + leftb = w.bound1.dist(leftp); + leftdb = w.bound1.dot(leftdir); + } + if(dist <= rightdist) + { + const vec *prev = rightv; + float prevdist = rightdist; + if(++rightv >= &g.v[g.numv]) rightv = g.v; + rightdist = rightv->dot(w.dir); + if(dist <= rightdist) + { + prev = rightv; + prevdist = rightdist; + if(++rightv >= &g.v[g.numv]) rightv = g.v; + rightdist = rightv->dot(w.dir); + } + rightdir = vec(*rightv).sub(*prev); + rightdir.mul(grassstep/-w.dir.dot(rightdir)); + rightp = vec(rightdir).mul((prevdist - dist)/grassstep).add(*prev); + rightb = w.bound2.dist(rightp); + rightdb = w.bound2.dot(rightdir); + } + vec p1 = leftp, p2 = rightp; + if(leftb > grassmargin) + { + if(w.bound1.dist(p2) >= grassmargin) continue; + p1.add(vec(across).mul(leftb - grassmargin)); + } + if(rightb > grassmargin) + { + if(w.bound2.dist(p1) >= grassmargin) continue; + p2.sub(vec(across).mul(rightb - grassmargin)); + } + + if(!group) + { + group = &grassgroups.add(); + group->tri = &g; + group->tex = tex->id; + extern bool brightengeom; + extern int fullbright; + int lmid = brightengeom && (g.lmid < LMID_RESERVED || (fullbright && editmode)) ? LMID_BRIGHT : g.lmid; + group->lmtex = lightmaptexs.inrange(lmid) ? lightmaptexs[lmid].id : notexture->id; + group->offset = grassverts.length()/4; + group->numquads = 0; + if(lastgrassanim!=lastmillis) animategrass(); + } + + group->numquads++; + + float tcoffset = grassoffsets[offset%NUMGRASSOFFSETS], + animoffset = animscale*grassanimoffsets[offset%NUMGRASSOFFSETS], + tc1 = tc.dot(p1) + tcoffset, tc2 = tc.dot(p2) + tcoffset, + fade = dist - t > taperdist ? (grassdist - (dist - t))*taperscale : 1, + height = grassheight * fade; + svec2 lm1(short(g.tcu.dot(p1)), short(g.tcv.dot(p1))), + lm2(short(g.tcu.dot(p2)), short(g.tcv.dot(p2))); + bvec4 color(grasscolor, uchar(fade*grassalpha*255)); + + #define GRASSVERT(n, tcv, modify) { \ + grassvert &gv = grassverts.add(); \ + gv.pos = p##n; \ + gv.color = color; \ + gv.tc = vec2(tc##n, tcv); \ + gv.lm = lm##n; \ + gv.bounds = w.vertbounds; \ + modify; \ + } + + GRASSVERT(2, 0, { gv.pos.z += height; gv.tc.x += animoffset; }); + GRASSVERT(1, 0, { gv.pos.z += height; gv.tc.x += animoffset; }); + GRASSVERT(1, 1, ); + GRASSVERT(2, 1, ); + } +} + +static void gengrassquads(vtxarray *va) +{ + loopv(va->grasstris) + { + grasstri &g = va->grasstris[i]; + if(isfoggedsphere(g.radius, g.center)) continue; + float dist = g.center.dist(camera1->o); + if(dist - g.radius > grassdist) continue; + + Slot &s = *lookupvslot(g.texture, false).slot; + if(!s.grasstex) + { + if(!s.autograss) continue; + s.grasstex = textureload(s.autograss, 2); + } + + grassgroup *group = NULL; + loopi(NUMGRASSWEDGES) + { + grasswedge &w = grasswedges[i]; + if(w.bound1.dist(g.center) > g.radius + grassmargin || w.bound2.dist(g.center) > g.radius + grassmargin) continue; + gengrassquads(group, w, g, s.grasstex); + } + if(group) group->dist = dist; + } +} + +static inline bool comparegrassgroups(const grassgroup &x, const grassgroup &y) +{ + return x.dist > y.dist; +} + +void generategrass() +{ + if(!grass || !grassdist) return; + + grassgroups.setsize(0); + grassverts.setsize(0); + + if(grassoffsets[0] < 0) loopi(NUMGRASSOFFSETS) grassoffsets[i] = rnd(0x1000000)/float(0x1000000); + + loopi(NUMGRASSWEDGES) + { + grasswedge &w = grasswedges[i]; + w.bound1.offset = -camera1->o.dot(w.bound1); + w.bound2.offset = -camera1->o.dot(w.bound2); + } + + for(vtxarray *va = visibleva; va; va = va->next) + { + if(va->grasstris.empty() || va->occluded >= OCCLUDE_GEOM) continue; + if(va->distance > grassdist) continue; + if(reflecting || refracting>0 ? va->o.z+va->sizeo.z>=reflectz) continue; + gengrassquads(va); + } + + if(grassgroups.empty()) return; + + grassgroups.sort(comparegrassgroups); + + if(!grassvbo) glGenBuffers_(1, &grassvbo); + gle::bindvbo(grassvbo); + int size = grassverts.length()*sizeof(grassvert); + grassvbosize = max(grassvbosize, size); + glBufferData_(GL_ARRAY_BUFFER, grassvbosize, size == grassvbosize ? grassverts.getbuf() : NULL, GL_STREAM_DRAW); + if(size != grassvbosize) glBufferSubData_(GL_ARRAY_BUFFER, 0, size, grassverts.getbuf()); + gle::clearvbo(); +} + +void rendergrass() +{ + if(!grass || !grassdist || grassgroups.empty() || dbggrass) return; + + glDisable(GL_CULL_FACE); + glEnable(GL_BLEND); + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + glDepthMask(GL_FALSE); + + GLOBALPARAM(camera, camera1->o); + + SETSHADER(grass); + + LOCALPARAMF(grassmargin, grassmargin, grassmargin ? grassmarginfade / grassmargin : 0.0f, grassmargin ? grassmarginfade : 1.0f); + + gle::bindvbo(grassvbo); + + const grassvert *ptr = 0; + gle::vertexpointer(sizeof(grassvert), ptr->pos.v); + gle::colorpointer(sizeof(grassvert), ptr->color.v); + gle::texcoord0pointer(sizeof(grassvert), ptr->tc.v); + gle::texcoord1pointer(sizeof(grassvert), ptr->lm.v, GL_SHORT); + gle::tangentpointer(sizeof(grassvert), ptr->bounds.v, GL_BYTE); + gle::enablevertex(); + gle::enablecolor(); + gle::enabletexcoord0(); + gle::enabletexcoord1(); + gle::enabletangent(); + gle::enablequads(); + + int texid = -1, lmtexid = -1; + loopv(grassgroups) + { + grassgroup &g = grassgroups[i]; + + if(reflecting || refracting) + { + if(refracting < 0 ? g.tri->minz > reflectz : g.tri->maxz + grassheight < reflectz) continue; + if(isfoggedsphere(g.tri->radius, g.tri->center)) continue; + } + + if(texid != g.tex) + { + glBindTexture(GL_TEXTURE_2D, g.tex); + texid = g.tex; + } + if(lmtexid != g.lmtex) + { + glActiveTexture_(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, g.lmtex); + glActiveTexture_(GL_TEXTURE0); + lmtexid = g.lmtex; + } + + gle::drawquads(g.offset, g.numquads); + xtravertsva += 4*g.numquads; + } + + gle::disablequads(); + gle::disablevertex(); + gle::disablecolor(); + gle::disabletexcoord0(); + gle::disabletexcoord1(); + gle::disabletangent(); + + gle::clearvbo(); + + glDisable(GL_BLEND); + glDepthMask(GL_TRUE); + glEnable(GL_CULL_FACE); +} + +void cleanupgrass() +{ + if(grassvbo) { glDeleteBuffers_(1, &grassvbo); grassvbo = 0; } + grassvbosize = 0; +} + diff --git a/src/engine/lensflare.h b/src/engine/lensflare.h new file mode 100644 index 0000000..1618e52 --- /dev/null +++ b/src/engine/lensflare.h @@ -0,0 +1,193 @@ +static struct flaretype +{ + int type; /* flaretex index, 0..5, -1 for 6+random shine */ + float loc; /* postion on axis */ + float scale; /* texture scaling */ + uchar alpha; /* color alpha */ +} flaretypes[] = +{ + {2, 1.30f, 0.04f, 153}, //flares + {3, 1.00f, 0.10f, 102}, + {1, 0.50f, 0.20f, 77}, + {3, 0.20f, 0.05f, 77}, + {0, 0.00f, 0.04f, 77}, + {5, -0.25f, 0.07f, 127}, + {5, -0.40f, 0.02f, 153}, + {5, -0.60f, 0.04f, 102}, + {5, -1.00f, 0.03f, 51}, + {-1, 1.00f, 0.30f, 255}, //shine - red, green, blue + {-2, 1.00f, 0.20f, 255}, + {-3, 1.00f, 0.25f, 255} +}; + +struct flare +{ + vec o, center; + float size; + bvec color; + bool sparkle; +}; + +VAR(flarelights, 0, 0, 1); +VARP(flarecutoff, 0, 1000, 10000); +VARP(flaresize, 20, 100, 500); + +struct flarerenderer : partrenderer +{ + int maxflares, numflares; + unsigned int shinetime; + flare *flares; + + flarerenderer(const char *texname, int maxflares) + : partrenderer(texname, 3, PT_FLARE|PT_SHADER), maxflares(maxflares), numflares(0), shinetime(0) + { + flares = new flare[maxflares]; + } + ~flarerenderer() + { + delete[] flares; + } + + void reset() + { + numflares = 0; + } + + void newflare(vec &o, const vec ¢er, uchar r, uchar g, uchar b, float mod, float size, bool sun, bool sparkle) + { + if(numflares >= maxflares) return; + vec target; //occlusion check (neccessary as depth testing is turned off) + if(!raycubelos(o, camera1->o, target)) return; + flare &f = flares[numflares++]; + f.o = o; + f.center = center; + f.size = size; + f.color = bvec(uchar(r*mod), uchar(g*mod), uchar(b*mod)); + f.sparkle = sparkle; + } + + void addflare(vec &o, uchar r, uchar g, uchar b, bool sun, bool sparkle) + { + //frustrum + fog check + if(isvisiblesphere(0.0f, o) > (sun?VFC_FOGGED:VFC_FULL_VISIBLE)) return; + //find closest point between camera line of sight and flare pos + vec flaredir = vec(o).sub(camera1->o); + vec center = vec(camdir).mul(flaredir.dot(camdir)).add(camera1->o); + float mod, size; + if(sun) //fixed size + { + mod = 1.0; + size = flaredir.magnitude() * flaresize / 100.0f; + } + else + { + mod = (flarecutoff-vec(o).sub(center).squaredlen())/flarecutoff; + if(mod < 0.0f) return; + size = flaresize / 5.0f; + } + newflare(o, center, r, g, b, mod, size, sun, sparkle); + } + + void makelightflares() + { + numflares = 0; //regenerate flarelist each frame + shinetime = lastmillis/10; + + if(editmode || !flarelights) return; + + const vector &ents = entities::getents(); + extern const vector &checklightcache(int x, int y); + const vector &lights = checklightcache(int(camera1->o.x), int(camera1->o.y)); + loopv(lights) + { + entity &e = *ents[lights[i]]; + if(e.type != ET_LIGHT) continue; + bool sun = (e.attr1==0); + float radius = float(e.attr1); + vec flaredir = vec(e.o).sub(camera1->o); + float len = flaredir.magnitude(); + if(!sun && (len > radius)) continue; + if(isvisiblesphere(0.0f, e.o) > (sun?VFC_FOGGED:VFC_FULL_VISIBLE)) continue; + vec center = vec(camdir).mul(flaredir.dot(camdir)).add(camera1->o); + float mod, size; + if(sun) //fixed size + { + mod = 1.0; + size = len * flaresize / 100.0f; + } + else + { + mod = (radius-len)/radius; + size = flaresize / 5.0f; + } + newflare(e.o, center, e.attr2, e.attr3, e.attr4, mod, size, sun, sun); + } + } + + int count() + { + return numflares; + } + + bool haswork() + { + return (numflares != 0) && !glaring && !reflecting && !refracting; + } + + void render() + { + textureshader->set(); + glDisable(GL_DEPTH_TEST); + if(!tex) tex = textureload(texname); + glBindTexture(GL_TEXTURE_2D, tex->id); + gle::defattrib(gle::ATTRIB_VERTEX, 3, GL_FLOAT); + gle::defattrib(gle::ATTRIB_TEXCOORD0, 2, GL_FLOAT); + gle::defattrib(gle::ATTRIB_COLOR, 4, GL_UNSIGNED_BYTE); + gle::begin(GL_QUADS); + loopi(numflares) + { + const flare &f = flares[i]; + vec center = f.center; + vec axis = vec(f.o).sub(center); + bvec4 color(f.color, 255); + loopj(f.sparkle?12:9) + { + const flaretype &ft = flaretypes[j]; + vec o = vec(axis).mul(ft.loc).add(center); + float sz = ft.scale * f.size; + int tex = ft.type; + if(ft.type < 0) //sparkles - always done last + { + shinetime = (shinetime + 1) % 10; + tex = 6+shinetime; + color.r = 0; + color.g = 0; + color.b = 0; + color[-ft.type-1] = f.color[-ft.type-1]; //only want a single channel + } + color.a = ft.alpha; + const float tsz = 0.25; //flares are aranged in 4x4 grid + float tx = tsz*(tex&0x03), ty = tsz*((tex>>2)&0x03); + gle::attribf(o.x+(-camright.x+camup.x)*sz, o.y+(-camright.y+camup.y)*sz, o.z+(-camright.z+camup.z)*sz); + gle::attribf(tx, ty+tsz); + gle::attrib(color); + gle::attribf(o.x+( camright.x+camup.x)*sz, o.y+( camright.y+camup.y)*sz, o.z+( camright.z+camup.z)*sz); + gle::attribf(tx+tsz, ty+tsz); + gle::attrib(color); + gle::attribf(o.x+( camright.x-camup.x)*sz, o.y+( camright.y-camup.y)*sz, o.z+( camright.z-camup.z)*sz); + gle::attribf(tx+tsz, ty); + gle::attrib(color); + gle::attribf(o.x+(-camright.x-camup.x)*sz, o.y+(-camright.y-camup.y)*sz, o.z+(-camright.z-camup.z)*sz); + gle::attribf(tx, ty); + gle::attrib(color); + } + } + gle::end(); + glEnable(GL_DEPTH_TEST); + } + + //square per round hole - use addflare(..) instead + particle *addpart(const vec &o, const vec &d, int fade, int color, float size, int gravity = 0) { return NULL; } +}; +static flarerenderer flares("packages/particles/lensflares.png", 64); + diff --git a/src/engine/lightmap.cpp b/src/engine/lightmap.cpp new file mode 100644 index 0000000..b4090e2 --- /dev/null +++ b/src/engine/lightmap.cpp @@ -0,0 +1,2729 @@ +#include "engine.h" + +#define MAXLIGHTMAPTASKS 4096 +#define LIGHTMAPBUFSIZE (2*1024*1024) + +struct lightmapinfo; +struct lightmaptask; + +struct lightmapworker +{ + uchar *buf; + int bufstart, bufused; + lightmapinfo *firstlightmap, *lastlightmap, *curlightmaps; + cube *c; + cubeext *ext; + uchar *colorbuf; + bvec *raybuf; + uchar *ambient, *blur; + vec *colordata, *raydata; + int type, bpp, w, h, orient, rotate; + VSlot *vslot; + Slot *slot; + vector lights; + ShadowRayCache *shadowraycache; + BlendMapCache *blendmapcache; + bool needspace, doneworking; + SDL_cond *spacecond; + SDL_Thread *thread; + + lightmapworker(); + ~lightmapworker(); + + void reset(); + bool setupthread(); + void cleanupthread(); + + static int work(void *data); +}; + +struct lightmapinfo +{ + lightmapinfo *next; + cube *c; + uchar *colorbuf; + bvec *raybuf; + bool packed; + int type, w, h, bpp, bufsize, surface, layers; +}; + +struct lightmaptask +{ + ivec o; + int size, usefaces, progress; + cube *c; + cubeext *ext; + lightmapinfo *lightmaps; + lightmapworker *worker; +}; + +struct lightmapext +{ + cube *c; + cubeext *ext; +}; + +static vector lightmapworkers; +static vector lightmaptasks[2]; +static vector lightmapexts; +static int packidx = 0, allocidx = 0; +static SDL_mutex *lightlock = NULL, *tasklock = NULL; +static SDL_cond *fullcond = NULL, *emptycond = NULL; + +int lightmapping = 0; + +vector lightmaps; + +VARR(lightprecision, 1, 32, 1024); +VARR(lighterror, 1, 8, 16); +VARR(bumperror, 1, 3, 16); +VARR(lightlod, 0, 0, 10); +bvec ambientcolor(0x19, 0x19, 0x19), skylightcolor(0, 0, 0); +HVARFR(ambient, 1, 0x191919, 0xFFFFFF, +{ + if(ambient <= 255) ambient |= (ambient<<8) | (ambient<<16); + ambientcolor = bvec((ambient>>16)&0xFF, (ambient>>8)&0xFF, ambient&0xFF); +}); +HVARFR(skylight, 0, 0, 0xFFFFFF, +{ + if(skylight <= 255) skylight |= (skylight<<8) | (skylight<<16); + skylightcolor = bvec((skylight>>16)&0xFF, (skylight>>8)&0xFF, skylight&0xFF); +}); + +extern void setupsunlight(); +bvec sunlightcolor(0, 0, 0); +HVARFR(sunlight, 0, 0, 0xFFFFFF, +{ + if(sunlight <= 255) sunlight |= (sunlight<<8) | (sunlight<<16); + sunlightcolor = bvec((sunlight>>16)&0xFF, (sunlight>>8)&0xFF, sunlight&0xFF); + setupsunlight(); +}); +FVARFR(sunlightscale, 0, 1, 16, setupsunlight()); +vec sunlightdir(0, 0, 1); +extern void setsunlightdir(); +VARFR(sunlightyaw, 0, 0, 360, setsunlightdir()); +VARFR(sunlightpitch, -90, 90, 90, setsunlightdir()); + +void setsunlightdir() +{ + sunlightdir = vec(sunlightyaw*RAD, sunlightpitch*RAD); + loopk(3) if(fabs(sunlightdir[k]) < 1e-5f) sunlightdir[k] = 0; + sunlightdir.normalize(); + setupsunlight(); +} + +entity sunlightent; +void setupsunlight() +{ + memclear(sunlightent); + sunlightent.type = ET_LIGHT; + sunlightent.attr1 = 0; + sunlightent.attr2 = int(sunlightcolor.x*sunlightscale); + sunlightent.attr3 = int(sunlightcolor.y*sunlightscale); + sunlightent.attr4 = int(sunlightcolor.z*sunlightscale); + float dist = min(min(sunlightdir.x ? 1/fabs(sunlightdir.x) : 1e16f, sunlightdir.y ? 1/fabs(sunlightdir.y) : 1e16f), sunlightdir.z ? 1/fabs(sunlightdir.z) : 1e16f); + sunlightent.o = vec(sunlightdir).mul(dist*worldsize).add(vec(worldsize/2, worldsize/2, worldsize/2)); +} + +VARR(skytexturelight, 0, 1, 1); +extern int useskytexture; + +static const surfaceinfo brightsurfaces[6] = +{ + brightsurface, + brightsurface, + brightsurface, + brightsurface, + brightsurface, + brightsurface +}; + +void brightencube(cube &c) +{ + if(!c.ext) newcubeext(c, 0, false); + memcpy(c.ext->surfaces, brightsurfaces, sizeof(brightsurfaces)); +} + +void setsurfaces(cube &c, const surfaceinfo *surfs, const vertinfo *verts, int numverts) +{ + if(!c.ext || c.ext->maxverts < numverts) newcubeext(c, numverts, false); + memcpy(c.ext->surfaces, surfs, sizeof(c.ext->surfaces)); + memcpy(c.ext->verts(), verts, numverts*sizeof(vertinfo)); +} + +void setsurface(cube &c, int orient, const surfaceinfo &src, const vertinfo *srcverts, int numsrcverts) +{ + int dstoffset = 0; + if(!c.ext) newcubeext(c, numsrcverts, true); + else + { + int numbefore = 0, beforeoffset = 0; + loopi(orient) + { + surfaceinfo &surf = c.ext->surfaces[i]; + int numverts = surf.totalverts(); + if(!numverts) continue; + numbefore += numverts; + beforeoffset = surf.verts + numverts; + } + int numafter = 0, afteroffset = c.ext->maxverts; + for(int i = 5; i > orient; i--) + { + surfaceinfo &surf = c.ext->surfaces[i]; + int numverts = surf.totalverts(); + if(!numverts) continue; + numafter += numverts; + afteroffset = surf.verts; + } + if(afteroffset - beforeoffset >= numsrcverts) dstoffset = beforeoffset; + else + { + cubeext *ext = c.ext; + if(numbefore + numsrcverts + numafter > c.ext->maxverts) + { + ext = growcubeext(c.ext, numbefore + numsrcverts + numafter); + memcpy(ext->surfaces, c.ext->surfaces, sizeof(ext->surfaces)); + } + int offset = 0; + if(numbefore == beforeoffset) + { + if(numbefore && c.ext != ext) memcpy(ext->verts(), c.ext->verts(), numbefore*sizeof(vertinfo)); + offset = numbefore; + } + else loopi(orient) + { + surfaceinfo &surf = ext->surfaces[i]; + int numverts = surf.totalverts(); + if(!numverts) continue; + memmove(ext->verts() + offset, c.ext->verts() + surf.verts, numverts*sizeof(vertinfo)); + surf.verts = offset; + offset += numverts; + } + dstoffset = offset; + offset += numsrcverts; + if(numafter && offset > afteroffset) + { + offset += numafter; + for(int i = 5; i > orient; i--) + { + surfaceinfo &surf = ext->surfaces[i]; + int numverts = surf.totalverts(); + if(!numverts) continue; + offset -= numverts; + memmove(ext->verts() + offset, c.ext->verts() + surf.verts, numverts*sizeof(vertinfo)); + surf.verts = offset; + } + } + if(c.ext != ext) setcubeext(c, ext); + } + } + surfaceinfo &dst = c.ext->surfaces[orient]; + dst = src; + dst.verts = dstoffset; + if(srcverts) memcpy(c.ext->verts() + dstoffset, srcverts, numsrcverts*sizeof(vertinfo)); +} + +// quality parameters, set by the calclight arg +VARN(lmshadows, lmshadows_, 0, 2, 2); +VARN(lmaa, lmaa_, 0, 3, 3); +VARN(lerptjoints, lerptjoints_, 0, 1, 1); +static int lmshadows = 2, lmaa = 3, lerptjoints = 1; + +static uint progress = 0, taskprogress = 0; +static GLuint progresstex = 0; +static int progresstexticks = 0, progresslightmap = -1; + +bool calclight_canceled = false; +volatile bool check_calclight_progress = false; + +void check_calclight_canceled() +{ + if(interceptkey(SDLK_ESCAPE)) + { + calclight_canceled = true; + loopv(lightmapworkers) lightmapworkers[i]->doneworking = true; + } + if(!calclight_canceled) check_calclight_progress = false; +} + +void show_calclight_progress() +{ + float bar1 = float(progress) / float(allocnodes); + defformatstring(text1, "%d%% using %d textures", int(bar1 * 100), lightmaps.length()); + + if(LM_PACKW <= hwtexsize && !progresstex) + { + glGenTextures(1, &progresstex); + createtexture(progresstex, LM_PACKW, LM_PACKH, NULL, 3, 1, GL_RGB); + } + + // only update once a sec (4 * 250 ms ticks) to not kill performance + if(progresstex && !calclight_canceled && progresslightmap >= 0 && !(progresstexticks++ % 4)) + { + if(tasklock) SDL_LockMutex(tasklock); + LightMap &lm = lightmaps[progresslightmap]; + uchar *data = lm.data; + int bpp = lm.bpp; + if(tasklock) SDL_UnlockMutex(tasklock); + glBindTexture(GL_TEXTURE_2D, progresstex); + glPixelStorei(GL_UNPACK_ALIGNMENT, texalign(data, LM_PACKW, bpp)); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, LM_PACKW, LM_PACKH, bpp > 3 ? GL_RGBA : GL_RGB, GL_UNSIGNED_BYTE, data); + } + renderprogress(bar1, text1, progresstexticks ? progresstex : 0); +} + +#define CHECK_PROGRESS_LOCKED(exit, before, after) CHECK_CALCLIGHT_PROGRESS_LOCKED(exit, show_calclight_progress, before, after) +#define CHECK_PROGRESS(exit) CHECK_PROGRESS_LOCKED(exit, , ) + +bool PackNode::insert(ushort &tx, ushort &ty, ushort tw, ushort th) +{ + if((available < tw && available < th) || w < tw || h < th) + return false; + if(child1) + { + bool inserted = child1->insert(tx, ty, tw, th) || + child2->insert(tx, ty, tw, th); + available = max(child1->available, child2->available); + if(!available) clear(); + return inserted; + } + if(w == tw && h == th) + { + available = 0; + tx = x; + ty = y; + return true; + } + + if(w - tw > h - th) + { + child1 = new PackNode(x, y, tw, h); + child2 = new PackNode(x + tw, y, w - tw, h); + } + else + { + child1 = new PackNode(x, y, w, th); + child2 = new PackNode(x, y + th, w, h - th); + } + + bool inserted = child1->insert(tx, ty, tw, th); + available = max(child1->available, child2->available); + return inserted; +} + +bool LightMap::insert(ushort &tx, ushort &ty, uchar *src, ushort tw, ushort th) +{ + if((type&LM_TYPE) != LM_BUMPMAP1 && !packroot.insert(tx, ty, tw, th)) + return false; + + copy(tx, ty, src, tw, th); + return true; +} + +void LightMap::copy(ushort tx, ushort ty, uchar *src, ushort tw, ushort th) +{ + uchar *dst = data + bpp * tx + ty * bpp * LM_PACKW; + loopi(th) + { + memcpy(dst, src, bpp * tw); + dst += bpp * LM_PACKW; + src += bpp * tw; + } + ++lightmaps; + lumels += tw * th; +} + +static void insertunlit(int i) +{ + LightMap &l = lightmaps[i]; + if((l.type&LM_TYPE) == LM_BUMPMAP1) + { + l.unlitx = l.unlity = -1; + return; + } + ushort x, y; + uchar unlit[4] = { ambientcolor[0], ambientcolor[1], ambientcolor[2], 255 }; + if(l.insert(x, y, unlit, 1, 1)) + { + if((l.type&LM_TYPE) == LM_BUMPMAP0) + { + bvec front(128, 128, 255); + ASSERT(lightmaps[i+1].insert(x, y, front.v, 1, 1)); + } + l.unlitx = x; + l.unlity = y; + } +} + +struct layoutinfo +{ + ushort x, y, lmid; + uchar w, h; +}; + +static void insertlightmap(lightmapinfo &li, layoutinfo &si) +{ + loopv(lightmaps) + { + if(lightmaps[i].type == li.type && lightmaps[i].insert(si.x, si.y, li.colorbuf, si.w, si.h)) + { + si.lmid = i + LMID_RESERVED; + if((li.type&LM_TYPE) == LM_BUMPMAP0) ASSERT(lightmaps[i+1].insert(si.x, si.y, (uchar *)li.raybuf, si.w, si.h)); + return; + } + } + + progresslightmap = lightmaps.length(); + + si.lmid = lightmaps.length() + LMID_RESERVED; + LightMap &l = lightmaps.add(); + l.type = li.type; + l.bpp = li.bpp; + l.data = new uchar[li.bpp*LM_PACKW*LM_PACKH]; + memset(l.data, 0, li.bpp*LM_PACKW*LM_PACKH); + ASSERT(l.insert(si.x, si.y, li.colorbuf, si.w, si.h)); + if((li.type&LM_TYPE) == LM_BUMPMAP0) + { + LightMap &r = lightmaps.add(); + r.type = LM_BUMPMAP1 | (li.type&~LM_TYPE); + r.bpp = 3; + r.data = new uchar[3*LM_PACKW*LM_PACKH]; + memset(r.data, 0, 3*LM_PACKW*LM_PACKH); + ASSERT(r.insert(si.x, si.y, (uchar *)li.raybuf, si.w, si.h)); + } +} + +static void copylightmap(lightmapinfo &li, layoutinfo &si) +{ + lightmaps[si.lmid-LMID_RESERVED].copy(si.x, si.y, li.colorbuf, si.w, si.h); + if((li.type&LM_TYPE)==LM_BUMPMAP0 && lightmaps.inrange(si.lmid+1-LMID_RESERVED)) + lightmaps[si.lmid+1-LMID_RESERVED].copy(si.x, si.y, (uchar *)li.raybuf, si.w, si.h); +} + +static inline bool htcmp(const lightmapinfo &k, const layoutinfo &v) +{ + int kw = k.w, kh = k.h; + if(kw != v.w || kh != v.h) return false; + LightMap &vlm = lightmaps[v.lmid - LMID_RESERVED]; + int ktype = k.type; + if(ktype != vlm.type) return false; + int kbpp = k.bpp; + const uchar *kcolor = k.colorbuf, *vcolor = vlm.data + kbpp*(v.x + v.y*LM_PACKW); + loopi(kh) + { + if(memcmp(kcolor, vcolor, kbpp*kw)) return false; + kcolor += kbpp*kw; + vcolor += kbpp*LM_PACKW; + } + if((ktype&LM_TYPE) != LM_BUMPMAP0) return true; + const bvec *kdir = k.raybuf, *vdir = (const bvec *)lightmaps[v.lmid+1 - LMID_RESERVED].data + (v.x + v.y*LM_PACKW); + loopi(kh) + { + if(memcmp(kdir, vdir, kw*sizeof(bvec))) return false; + kdir += kw; + vdir += LM_PACKW; + } + return true; +} + +static inline uint hthash(const lightmapinfo &k) +{ + int kw = k.w, kh = k.h, kbpp = k.bpp; + uint hash = kw + (kh<<8); + const uchar *color = k.colorbuf; + loopi(kw*kh) + { + hash ^= color[0] + (color[1] << 4) + (color[2] << 8); + color += kbpp; + } + return hash; +} + +static hashset compressed; + +VAR(lightcompress, 0, 3, 6); + +static bool packlightmap(lightmapinfo &l, layoutinfo &surface) +{ + surface.w = l.w; + surface.h = l.h; + if((int)l.w <= lightcompress && (int)l.h <= lightcompress) + { + layoutinfo *val = compressed.access(l); + if(!val) + { + insertlightmap(l, surface); + compressed[l] = surface; + } + else + { + surface.x = val->x; + surface.y = val->y; + surface.lmid = val->lmid; + return false; + } + } + else insertlightmap(l, surface); + return true; +} + +static void updatelightmap(const layoutinfo &surface) +{ + if(max(LM_PACKW, LM_PACKH) > hwtexsize || !lightmaps.inrange(surface.lmid-LMID_RESERVED)) return; + + LightMap &lm = lightmaps[surface.lmid-LMID_RESERVED]; + if(lm.tex < 0) + { + lm.offsetx = lm.offsety = 0; + lm.tex = lightmaptexs.length(); + LightMapTexture &tex = lightmaptexs.add(); + tex.type = lm.type; + tex.w = LM_PACKW; + tex.h = LM_PACKH; + tex.unlitx = lm.unlitx; + tex.unlity = lm.unlity; + glGenTextures(1, &tex.id); + createtexture(tex.id, tex.w, tex.h, NULL, 3, 1, tex.type&LM_ALPHA ? GL_RGBA : GL_RGB); + if((lm.type&LM_TYPE)==LM_BUMPMAP0 && lightmaps.inrange(surface.lmid+1-LMID_RESERVED)) + { + LightMap &lm2 = lightmaps[surface.lmid+1-LMID_RESERVED]; + lm2.offsetx = lm2.offsety = 0; + lm2.tex = lightmaptexs.length(); + LightMapTexture &tex2 = lightmaptexs.add(); + tex2.type = (lm.type&~LM_TYPE) | LM_BUMPMAP0; + tex2.w = LM_PACKW; + tex2.h = LM_PACKH; + tex2.unlitx = lm2.unlitx; + tex2.unlity = lm2.unlity; + glGenTextures(1, &tex2.id); + createtexture(tex2.id, tex2.w, tex2.h, NULL, 3, 1, GL_RGB); + } + } + + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glPixelStorei(GL_UNPACK_ROW_LENGTH, LM_PACKW); + + glBindTexture(GL_TEXTURE_2D, lightmaptexs[lm.tex].id); + glTexSubImage2D(GL_TEXTURE_2D, 0, lm.offsetx + surface.x, lm.offsety + surface.y, surface.w, surface.h, lm.type&LM_ALPHA ? GL_RGBA : GL_RGB, GL_UNSIGNED_BYTE, &lm.data[(surface.y*LM_PACKW + surface.x)*lm.bpp]); + if((lm.type&LM_TYPE)==LM_BUMPMAP0 && lightmaps.inrange(surface.lmid+1-LMID_RESERVED)) + { + LightMap &lm2 = lightmaps[surface.lmid+1-LMID_RESERVED]; + glBindTexture(GL_TEXTURE_2D, lightmaptexs[lm2.tex].id); + glTexSubImage2D(GL_TEXTURE_2D, 0, lm2.offsetx + surface.x, lm2.offsety + surface.y, surface.w, surface.h, GL_RGB, GL_UNSIGNED_BYTE, &lm2.data[(surface.y*LM_PACKW + surface.x)*3]); + } + + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); +} + + +static uint generatelumel(lightmapworker *w, const float tolerance, uint lightmask, const vector &lights, const vec &target, const vec &normal, vec &sample, int x, int y) +{ + vec avgray(0, 0, 0); + float r = 0, g = 0, b = 0; + uint lightused = 0; + loopv(lights) + { + if(lightmask&(1<type==ET_SPOTLIGHT) + { + vec spot = vec(light.attached->o).sub(light.o).normalize(); + float maxatten = sincos360[clamp(int(light.attached->attr1), 1, 89)].x, spotatten = (ray.dot(spot) - maxatten) / (1 - maxatten); + if(spotatten <= 0) continue; + attenuation *= spotatten; + } + if(lmshadows && mag) + { + float dist = shadowray(w->shadowraycache, light.o, ray, mag - tolerance, RAY_SHADOW | (lmshadows > 1 ? RAY_ALPHAPOLY : 0)); + if(dist < mag - tolerance) continue; + } + lightused |= 1<type&LM_TYPE) + { + case LM_BUMPMAP0: + intensity = attenuation; + avgray.add(ray.mul(-attenuation)); + break; + default: + intensity = angle * attenuation; + break; + } + r += intensity * float(light.attr2); + g += intensity * float(light.attr3); + b += intensity * float(light.attr4); + } + if(sunlight) + { + float angle = sunlightdir.dot(normal); + if(angle > 0 && + (!lmshadows || + shadowray(w->shadowraycache, vec(sunlightdir).mul(tolerance).add(target), sunlightdir, 1e16f, RAY_SHADOW | (lmshadows > 1 ? RAY_ALPHAPOLY : 0) | (skytexturelight ? RAY_SKIPSKY | (useskytexture ? RAY_SKYTEX : 0) : 0)) > 1e15f)) + { + float intensity; + switch(w->type&LM_TYPE) + { + case LM_BUMPMAP0: + intensity = 1; + avgray.add(sunlightdir); + break; + default: + intensity = angle; + break; + } + r += intensity * (sunlightcolor.x*sunlightscale); + g += intensity * (sunlightcolor.y*sunlightscale); + b += intensity * (sunlightcolor.z*sunlightscale); + } + } + switch(w->type&LM_TYPE) + { + case LM_BUMPMAP0: + if(avgray.iszero()) break; + // transform to tangent space + extern const vec orientation_tangent[8][3]; + extern const vec orientation_bitangent[8][3]; + vec S(orientation_tangent[w->rotate][dimension(w->orient)]), + T(orientation_bitangent[w->rotate][dimension(w->orient)]); + normal.orthonormalize(S, T); + avgray.normalize(); + w->raydata[y*w->w+x].add(vec(S.dot(avgray)/S.magnitude(), T.dot(avgray)/T.magnitude(), normal.dot(avgray))); + break; + } + sample.x = min(255.0f, max(r, float(ambientcolor[0]))); + sample.y = min(255.0f, max(g, float(ambientcolor[1]))); + sample.z = min(255.0f, max(b, float(ambientcolor[2]))); + return lightused; +} + +static bool lumelsample(const vec &sample, int aasample, int stride) +{ + if(sample.x >= int(ambientcolor[0])+1 || sample.y >= int(ambientcolor[1])+1 || sample.z >= int(ambientcolor[2])+1) return true; +#define NCHECK(n) \ + if((n).x >= int(ambientcolor[0])+1 || (n).y >= int(ambientcolor[1])+1 || (n).z >= int(ambientcolor[2])+1) \ + return true; + const vec *n = &sample - stride - aasample; + NCHECK(n[0]); NCHECK(n[aasample]); NCHECK(n[2*aasample]); + n += stride; + NCHECK(n[0]); NCHECK(n[2*aasample]); + n += stride; + NCHECK(n[0]); NCHECK(n[aasample]); NCHECK(n[2*aasample]); + return false; +} + +static void calcskylight(lightmapworker *w, const vec &o, const vec &normal, float tolerance, uchar *skylight, int flags = RAY_ALPHAPOLY, extentity *t = NULL) +{ + static const vec rays[17] = + { + vec(cosf(21*RAD)*cosf(50*RAD), sinf(21*RAD)*cosf(50*RAD), sinf(50*RAD)), + vec(cosf(111*RAD)*cosf(50*RAD), sinf(111*RAD)*cosf(50*RAD), sinf(50*RAD)), + vec(cosf(201*RAD)*cosf(50*RAD), sinf(201*RAD)*cosf(50*RAD), sinf(50*RAD)), + vec(cosf(291*RAD)*cosf(50*RAD), sinf(291*RAD)*cosf(50*RAD), sinf(50*RAD)), + + vec(cosf(66*RAD)*cosf(70*RAD), sinf(66*RAD)*cosf(70*RAD), sinf(70*RAD)), + vec(cosf(156*RAD)*cosf(70*RAD), sinf(156*RAD)*cosf(70*RAD), sinf(70*RAD)), + vec(cosf(246*RAD)*cosf(70*RAD), sinf(246*RAD)*cosf(70*RAD), sinf(70*RAD)), + vec(cosf(336*RAD)*cosf(70*RAD), sinf(336*RAD)*cosf(70*RAD), sinf(70*RAD)), + + vec(0, 0, 1), + + vec(cosf(43*RAD)*cosf(60*RAD), sinf(43*RAD)*cosf(60*RAD), sinf(60*RAD)), + vec(cosf(133*RAD)*cosf(60*RAD), sinf(133*RAD)*cosf(60*RAD), sinf(60*RAD)), + vec(cosf(223*RAD)*cosf(60*RAD), sinf(223*RAD)*cosf(60*RAD), sinf(60*RAD)), + vec(cosf(313*RAD)*cosf(60*RAD), sinf(313*RAD)*cosf(60*RAD), sinf(60*RAD)), + + vec(cosf(88*RAD)*cosf(80*RAD), sinf(88*RAD)*cosf(80*RAD), sinf(80*RAD)), + vec(cosf(178*RAD)*cosf(80*RAD), sinf(178*RAD)*cosf(80*RAD), sinf(80*RAD)), + vec(cosf(268*RAD)*cosf(80*RAD), sinf(268*RAD)*cosf(80*RAD), sinf(80*RAD)), + vec(cosf(358*RAD)*cosf(80*RAD), sinf(358*RAD)*cosf(80*RAD), sinf(80*RAD)), + + }; + flags |= RAY_SHADOW; + if(skytexturelight) flags |= RAY_SKIPSKY | (useskytexture ? RAY_SKYTEX : 0); + int hit = 0; + if(w) loopi(17) + { + if(normal.dot(rays[i])>=0 && shadowray(w->shadowraycache, vec(rays[i]).mul(tolerance).add(o), rays[i], 1e16f, flags, t)>1e15f) hit++; + } + else loopi(17) + { + if(normal.dot(rays[i])>=0 && shadowray(vec(rays[i]).mul(tolerance).add(o), rays[i], 1e16f, flags, t)>1e15f) hit++; + } + + loopk(3) skylight[k] = uchar(ambientcolor[k] + (max(skylightcolor[k], ambientcolor[k]) - ambientcolor[k])*hit/17.0f); +} + +static inline bool hasskylight() +{ + return skylightcolor[0]>ambientcolor[0] || skylightcolor[1]>ambientcolor[1] || skylightcolor[2]>ambientcolor[2]; +} + +VARR(blurlms, 0, 0, 2); +VARR(blurskylight, 0, 0, 2); + +static inline void generatealpha(lightmapworker *w, float tolerance, const vec &pos, uchar &alpha) +{ + alpha = lookupblendmap(w->blendmapcache, pos); + if(w->slot->layermask) + { + static const int sdim[] = { 1, 0, 0 }, tdim[] = { 2, 2, 1 }; + int dim = dimension(w->orient); + float k = 8.0f/w->vslot->scale, + s = (pos[sdim[dim]] * k - w->vslot->offset.y) / w->slot->layermaskscale, + t = (pos[tdim[dim]] * (dim <= 1 ? -k : k) - w->vslot->offset.y) / w->slot->layermaskscale; + const texrotation &r = texrotations[w->rotate]; + if(r.swapxy) swap(s, t); + if(r.flipx) s = -s; + if(r.flipy) t = -t; + const ImageData &mask = *w->slot->layermask; + int mx = int(floor(s))%mask.w, my = int(floor(t))%mask.h; + if(mx < 0) mx += mask.w; + if(my < 0) my += mask.h; + uchar maskval = mask.data[mask.bpp*(mx + 1) - 1 + mask.pitch*my]; + switch(w->slot->layermaskmode) + { + case 2: alpha = min(alpha, maskval); break; + case 3: alpha = max(alpha, maskval); break; + case 4: alpha = min(alpha, uchar(0xFF - maskval)); break; + case 5: alpha = max(alpha, uchar(0xFF - maskval)); break; + default: alpha = maskval; break; + } + } +} + +VAR(edgetolerance, 1, 4, 64); +VAR(adaptivesample, 0, 2, 2); + +enum +{ + NO_SURFACE = 0, + SURFACE_AMBIENT_BOTTOM, + SURFACE_AMBIENT_TOP, + SURFACE_LIGHTMAP_BOTTOM, + SURFACE_LIGHTMAP_TOP, + SURFACE_LIGHTMAP_BLEND +}; + +#define SURFACE_AMBIENT SURFACE_AMBIENT_BOTTOM +#define SURFACE_LIGHTMAP SURFACE_LIGHTMAP_BOTTOM + +static bool generatelightmap(lightmapworker *w, float lpu, const lerpvert *lv, int numv, vec origin1, const vec &xstep1, const vec &ystep1, vec origin2, const vec &xstep2, const vec &ystep2, float side0, float sidestep) +{ + static const float aacoords[8][2] = + { + {0.0f, 0.0f}, + {-0.5f, -0.5f}, + {0.0f, -0.5f}, + {-0.5f, 0.0f}, + + {0.3f, -0.6f}, + {0.6f, 0.3f}, + {-0.3f, 0.6f}, + {-0.6f, -0.3f}, + }; + float tolerance = 0.5 / lpu; + uint lightmask = 0, lightused = 0; + vec offsets1[8], offsets2[8]; + loopi(8) + { + offsets1[i] = vec(xstep1).mul(aacoords[i][0]).add(vec(ystep1).mul(aacoords[i][1])); + offsets2[i] = vec(xstep2).mul(aacoords[i][0]).add(vec(ystep2).mul(aacoords[i][1])); + } + if((w->type&LM_TYPE) == LM_BUMPMAP0) memclear(w->raydata, (LM_MAXW + 4)*(LM_MAXH + 4)); + + origin1.sub(vec(ystep1).add(xstep1).mul(blurlms)); + origin2.sub(vec(ystep2).add(xstep2).mul(blurlms)); + + int aasample = min(1 << lmaa, 4); + int stride = aasample*(w->w+1); + vec *sample = w->colordata; + uchar *skylight = w->ambient; + lerpbounds start, end; + initlerpbounds(-blurlms, -blurlms, lv, numv, start, end); + float sidex = side0 + blurlms*sidestep; + for(int y = 0; y < w->h; ++y, sidex += sidestep) + { + vec normal, nstep; + lerpnormal(-blurlms, y - blurlms, lv, numv, start, end, normal, nstep); + + for(int x = 0; x < w->w; ++x, normal.add(nstep), skylight += w->bpp) + { +#define EDGE_TOLERANCE(x, y) \ + (x < blurlms \ + || x+1 > w->w - blurlms \ + || y < blurlms \ + || y+1 > w->h - blurlms \ + ? edgetolerance : 1) + float t = EDGE_TOLERANCE(x, y) * tolerance; + vec u = x < sidex ? vec(xstep1).mul(x).add(vec(ystep1).mul(y)).add(origin1) : vec(xstep2).mul(x).add(vec(ystep2).mul(y)).add(origin2); + lightused |= generatelumel(w, t, 0, w->lights, u, vec(normal).normalize(), *sample, x, y); + if(hasskylight()) + { + if((w->type&LM_TYPE)==LM_BUMPMAP0 || !adaptivesample || sample->xyz 1 ? RAY_ALPHAPOLY : 0); + else loopk(3) skylight[k] = max(skylightcolor[k], ambientcolor[k]); + } + else loopk(3) skylight[k] = ambientcolor[k]; + if(w->type&LM_ALPHA) generatealpha(w, t, u, skylight[3]); + sample += aasample; + } + sample += aasample; + } + if(adaptivesample > 1 && min(w->w, w->h) >= 2) lightmask = ~lightused; + sample = w->colordata; + initlerpbounds(-blurlms, -blurlms, lv, numv, start, end); + sidex = side0 + blurlms*sidestep; + for(int y = 0; y < w->h; ++y, sidex += sidestep) + { + vec normal, nstep; + lerpnormal(-blurlms, y - blurlms, lv, numv, start, end, normal, nstep); + + for(int x = 0; x < w->w; ++x, normal.add(nstep)) + { + vec ¢er = *sample++; + if(adaptivesample && x > 0 && x+1 < w->w && y > 0 && y+1 < w->h && !lumelsample(center, aasample, stride)) + loopi(aasample-1) *sample++ = center; + else + { +#define AA_EDGE_TOLERANCE(x, y, i) EDGE_TOLERANCE(x + aacoords[i][0], y + aacoords[i][1]) + vec u = x < sidex ? vec(xstep1).mul(x).add(vec(ystep1).mul(y)).add(origin1) : vec(xstep2).mul(x).add(vec(ystep2).mul(y)).add(origin2); + const vec *offsets = x < sidex ? offsets1 : offsets2; + vec n = vec(normal).normalize(); + loopi(aasample-1) + generatelumel(w, AA_EDGE_TOLERANCE(x, y, i+1) * tolerance, lightmask, w->lights, vec(u).add(offsets[i+1]), n, *sample++, x, y); + if(lmaa == 3) + { + loopi(4) + { + vec s; + generatelumel(w, AA_EDGE_TOLERANCE(x, y, i+4) * tolerance, lightmask, w->lights, vec(u).add(offsets[i+4]), n, s, x, y); + center.add(s); + } + center.div(5); + } + } + } + if(aasample > 1) + { + vec u = w->w < sidex ? vec(xstep1).mul(w->w).add(vec(ystep1).mul(y)).add(origin1) : vec(xstep2).mul(w->w).add(vec(ystep2).mul(y)).add(origin2); + const vec *offsets = w->w < sidex ? offsets1 : offsets2; + vec n = vec(normal).normalize(); + generatelumel(w, edgetolerance * tolerance, lightmask, w->lights, vec(u).add(offsets[1]), n, sample[1], w->w-1, y); + if(aasample > 2) + generatelumel(w, edgetolerance * tolerance, lightmask, w->lights, vec(u).add(offsets[3]), n, sample[3], w->w-1, y); + } + sample += aasample; + } + + if(aasample > 1) + { + vec normal, nstep; + lerpnormal(-blurlms, w->h - blurlms, lv, numv, start, end, normal, nstep); + + for(int x = 0; x <= w->w; ++x, normal.add(nstep)) + { + vec u = x < sidex ? vec(xstep1).mul(x).add(vec(ystep1).mul(w->h)).add(origin1) : vec(xstep2).mul(x).add(vec(ystep2).mul(w->h)).add(origin2); + const vec *offsets = x < sidex ? offsets1 : offsets2; + vec n = vec(normal).normalize(); + generatelumel(w, edgetolerance * tolerance, lightmask, w->lights, vec(u).add(offsets[1]), n, sample[1], min(x, w->w-1), w->h-1); + if(aasample > 2) + generatelumel(w, edgetolerance * tolerance, lightmask, w->lights, vec(u).add(offsets[2]), n, sample[2], min(x, w->w-1), w->h-1); + sample += aasample; + } + } + return true; +} + +static int finishlightmap(lightmapworker *w) +{ + if(hasskylight() && blurskylight && (w->w>1 || w->h>1)) + { + blurtexture(blurskylight, w->bpp, w->w, w->h, w->blur, w->ambient); + swap(w->blur, w->ambient); + } + vec *sample = w->colordata; + int aasample = min(1 << lmaa, 4), stride = aasample*(w->w+1); + float weight = 1.0f / (1.0f + 4.0f*lmaa), + cweight = weight * (lmaa == 3 ? 5.0f : 1.0f); + uchar *skylight = w->ambient; + vec *ray = w->raydata; + uchar *dstcolor = blurlms && (w->w > 1 || w->h > 1) ? w->blur : w->colorbuf; + uchar mincolor[4] = { 255, 255, 255, 255 }, maxcolor[4] = { 0, 0, 0, 0 }; + bvec *dstray = blurlms && (w->w > 1 || w->h > 1) ? (bvec *)w->raydata : w->raybuf; + bvec minray(255, 255, 255), maxray(0, 0, 0); + loop(y, w->h) + { + loop(x, w->w) + { + vec l(0, 0, 0); + const vec ¢er = *sample++; + loopi(aasample-1) l.add(*sample++); + if(aasample > 1) + { + l.add(sample[1]); + if(aasample > 2) l.add(sample[3]); + } + vec *next = sample + stride - aasample; + if(aasample > 1) + { + l.add(next[1]); + if(aasample > 2) l.add(next[2]); + l.add(next[aasample+1]); + } + + int r = int(center.x*cweight + l.x*weight), + g = int(center.y*cweight + l.y*weight), + b = int(center.z*cweight + l.z*weight), + ar = skylight[0], ag = skylight[1], ab = skylight[2]; + dstcolor[0] = max(ar, r); + dstcolor[1] = max(ag, g); + dstcolor[2] = max(ab, b); + loopk(3) + { + mincolor[k] = min(mincolor[k], dstcolor[k]); + maxcolor[k] = max(maxcolor[k], dstcolor[k]); + } + if(w->type&LM_ALPHA) + { + dstcolor[3] = skylight[3]; + mincolor[3] = min(mincolor[3], dstcolor[3]); + maxcolor[3] = max(maxcolor[3], dstcolor[3]); + } + if((w->type&LM_TYPE) == LM_BUMPMAP0) + { + if(ray->iszero()) dstray[0] = bvec(128, 128, 255); + else + { + // bias the normals towards the amount of ambient/skylight in the lumel + // this is necessary to prevent the light values in shaders from dropping too far below the skylight (to the ambient) if N.L is small + ray->normalize(); + int l = max(r, max(g, b)), a = max(ar, max(ag, ab)); + ray->mul(max(l-a, 0)); + ray->z += a; + dstray[0] = bvec(ray->normalize()); + } + loopk(3) + { + minray[k] = min(minray[k], dstray[0][k]); + maxray[k] = max(maxray[k], dstray[0][k]); + } + ray++; + dstray++; + } + dstcolor += w->bpp; + skylight += w->bpp; + } + sample += aasample; + } + if(int(maxcolor[0]) - int(mincolor[0]) <= lighterror && + int(maxcolor[1]) - int(mincolor[1]) <= lighterror && + int(maxcolor[2]) - int(mincolor[2]) <= lighterror && + mincolor[3] >= maxcolor[3]) + { + uchar color[3]; + loopk(3) color[k] = (int(maxcolor[k]) + int(mincolor[k])) / 2; + if(color[0] <= int(ambientcolor[0]) + lighterror && + color[1] <= int(ambientcolor[1]) + lighterror && + color[2] <= int(ambientcolor[2]) + lighterror && + (maxcolor[3]==0 || mincolor[3]==255)) + return mincolor[3]==255 ? SURFACE_AMBIENT_TOP : SURFACE_AMBIENT_BOTTOM; + if((w->type&LM_TYPE) != LM_BUMPMAP0 || + (int(maxray.x) - int(minray.x) <= bumperror && + int(maxray.y) - int(minray.z) <= bumperror && + int(maxray.z) - int(minray.z) <= bumperror)) + + { + memcpy(w->colorbuf, color, 3); + if(w->type&LM_ALPHA) w->colorbuf[3] = mincolor[3]; + if((w->type&LM_TYPE) == LM_BUMPMAP0) + { + loopk(3) w->raybuf[0][k] = uchar((int(maxray[k])+int(minray[k]))/2); + } + w->lastlightmap->w = w->w = 1; + w->lastlightmap->h = w->h = 1; + } + } + if(blurlms && (w->w>1 || w->h>1)) + { + blurtexture(blurlms, w->bpp, w->w, w->h, w->colorbuf, w->blur, blurlms); + if((w->type&LM_TYPE) == LM_BUMPMAP0) blurnormals(blurlms, w->w, w->h, w->raybuf, (const bvec *)w->raydata, blurlms); + w->lastlightmap->w = (w->w -= 2*blurlms); + w->lastlightmap->h = (w->h -= 2*blurlms); + } + if(mincolor[3]==255) return SURFACE_LIGHTMAP_TOP; + else if(maxcolor[3]==0) return SURFACE_LIGHTMAP_BOTTOM; + else return SURFACE_LIGHTMAP_BLEND; +} + +static int previewlightmapalpha(lightmapworker *w, float lpu, const vec &origin1, const vec &xstep1, const vec &ystep1, const vec &origin2, const vec &xstep2, const vec &ystep2, float side0, float sidestep) +{ + extern int fullbrightlevel; + float tolerance = 0.5 / lpu; + uchar *dst = w->colorbuf; + uchar minalpha = 255, maxalpha = 0; + float sidex = side0; + for(int y = 0; y < w->h; ++y, sidex += sidestep) + { + for(int x = 0; x < w->w; ++x, dst += 4) + { + vec u = x < sidex ? + vec(xstep1).mul(x).add(vec(ystep1).mul(y)).add(origin1) : + vec(xstep2).mul(x).add(vec(ystep2).mul(y)).add(origin2); + loopk(3) dst[k] = fullbrightlevel; + generatealpha(w, tolerance, u, dst[3]); + minalpha = min(minalpha, dst[3]); + maxalpha = max(maxalpha, dst[3]); + } + } + if(minalpha==255) return SURFACE_AMBIENT_TOP; + if(maxalpha==0) return SURFACE_AMBIENT_BOTTOM; + if(minalpha==maxalpha) w->w = w->h = 1; + if((w->type&LM_TYPE) == LM_BUMPMAP0) loopi(w->w*w->h) w->raybuf[i] = bvec(128, 128, 255); + return SURFACE_LIGHTMAP_BLEND; +} + +static void clearsurfaces(cube *c) +{ + loopi(8) + { + if(c[i].ext) + { + loopj(6) + { + surfaceinfo &surf = c[i].ext->surfaces[j]; + if(!surf.used()) continue; + surf.clear(); + int numverts = surf.numverts&MAXFACEVERTS; + if(numverts) + { + if(!(c[i].merged&(1<verts() + surf.verts; + loopk(numverts) + { + vertinfo &v = verts[k]; + v.u = 0; + v.v = 0; + v.norm = 0; + } + } + } + } + if(c[i].children) clearsurfaces(c[i].children); + } +} + +#define LIGHTCACHESIZE 1024 + +static struct lightcacheentry +{ + int x, y; + vector lights; +} lightcache[LIGHTCACHESIZE]; + +#define LIGHTCACHEHASH(x, y) (((((x)^(y))<<5) + (((x)^(y))>>5)) & (LIGHTCACHESIZE - 1)) + +VARF(lightcachesize, 4, 6, 12, clearlightcache()); + +void clearlightcache(int id) +{ + if(id >= 0) + { + const extentity &light = *entities::getents()[id]; + int radius = light.attr1; + if(radius) + { + for(int x = int(max(light.o.x-radius, 0.0f))>>lightcachesize, ex = int(min(light.o.x+radius, worldsize-1.0f))>>lightcachesize; x <= ex; x++) + for(int y = int(max(light.o.y-radius, 0.0f))>>lightcachesize, ey = int(min(light.o.y+radius, worldsize-1.0f))>>lightcachesize; y <= ey; y++) + { + lightcacheentry &lce = lightcache[LIGHTCACHEHASH(x, y)]; + if(lce.x != x || lce.y != y) continue; + lce.x = -1; + lce.lights.setsize(0); + } + return; + } + } + + for(lightcacheentry *lce = lightcache; lce < &lightcache[LIGHTCACHESIZE]; lce++) + { + lce->x = -1; + lce->lights.setsize(0); + } +} + +const vector &checklightcache(int x, int y) +{ + x >>= lightcachesize; + y >>= lightcachesize; + lightcacheentry &lce = lightcache[LIGHTCACHEHASH(x, y)]; + if(lce.x == x && lce.y == y) return lce.lights; + + lce.lights.setsize(0); + int csize = 1< &ents = entities::getents(); + loopv(ents) + { + const extentity &light = *ents[i]; + switch(light.type) + { + case ET_LIGHT: + { + int radius = light.attr1; + if(radius > 0) + { + if(light.o.x + radius < cx || light.o.x - radius > cx + csize || + light.o.y + radius < cy || light.o.y - radius > cy + csize) + continue; + } + break; + } + default: continue; + } + lce.lights.add(i); + } + + lce.x = x; + lce.y = y; + return lce.lights; +} + +static inline void addlight(lightmapworker *w, const extentity &light, int cx, int cy, int cz, int size, const vec *v, const vec *n, int numv) +{ + int radius = light.attr1; + if(radius > 0) + { + if(light.o.x + radius < cx || light.o.x - radius > cx + size || + light.o.y + radius < cy || light.o.y - radius > cy + size || + light.o.z + radius < cz || light.o.z - radius > cz + size) + return; + } + + loopi(4) + { + vec p(light.o); + p.sub(v[i]); + float dist = p.dot(n[i]); + if(dist >= 0 && (!radius || dist < radius)) + { + w->lights.add(&light); + break; + } + } +} + +static bool findlights(lightmapworker *w, int cx, int cy, int cz, int size, const vec *v, const vec *n, int numv, const Slot &slot, const VSlot &vslot) +{ + w->lights.setsize(0); + const vector &ents = entities::getents(); + static volatile bool usinglightcache = false; + if(size <= 1< &lights = checklightcache(cx, cy); + loopv(lights) + { + const extentity &light = *ents[lights[i]]; + switch(light.type) + { + case ET_LIGHT: addlight(w, light, cx, cy, cz, size, v, n, numv); break; + } + } + if(lightlock) { usinglightcache = false; SDL_UnlockMutex(lightlock); } + } + else loopv(ents) + { + const extentity &light = *ents[i]; + switch(light.type) + { + case ET_LIGHT: addlight(w, light, cx, cy, cz, size, v, n, numv); break; + } + } + if(vslot.layer && (setblendmaporigin(w->blendmapcache, ivec(cx, cy, cz), size) || slot.layermask)) return true; + return w->lights.length() || hasskylight() || sunlight; +} + +static int packlightmaps(lightmapworker *w = NULL) +{ + int numpacked = 0; + for(; packidx < lightmaptasks[0].length(); packidx++, numpacked++) + { + lightmaptask &t = lightmaptasks[0][packidx]; + if(!t.lightmaps) break; + if(t.ext && t.c->ext != t.ext) + { + lightmapext &e = lightmapexts.add(); + e.c = t.c; + e.ext = t.ext; + } + progress = t.progress; + lightmapinfo *l = t.lightmaps; + if(l == (lightmapinfo *)-1) continue; + int space = 0; + for(; l && l->c == t.c; l = l->next) + { + l->packed = true; + space += l->bufsize; + if(l->surface < 0 || !t.ext) continue; + surfaceinfo &surf = t.ext->surfaces[l->surface]; + layoutinfo layout; + packlightmap(*l, layout); + int numverts = surf.numverts&MAXFACEVERTS; + vertinfo *verts = t.ext->verts() + surf.verts; + if(l->layers&LAYER_DUP) + { + if(l->type&LM_ALPHA) surf.lmid[0] = layout.lmid; + else { surf.lmid[1] = layout.lmid; verts += numverts; } + } + else + { + if(l->layers&LAYER_TOP) surf.lmid[0] = layout.lmid; + if(l->layers&LAYER_BOTTOM) surf.lmid[1] = layout.lmid; + } + ushort offsetx = layout.x*((USHRT_MAX+1)/LM_PACKW), offsety = layout.y*((USHRT_MAX+1)/LM_PACKH); + loopk(numverts) + { + vertinfo &v = verts[k]; + v.u += offsetx; + v.v += offsety; + } + } + if(t.worker == w) + { + w->bufused -= space; + w->bufstart = (w->bufstart + space)%LIGHTMAPBUFSIZE; + w->firstlightmap = l; + if(!l) + { + w->lastlightmap = NULL; + w->bufstart = w->bufused = 0; + } + } + if(t.worker->needspace) SDL_CondSignal(t.worker->spacecond); + } + return numpacked; +} + +static lightmapinfo *alloclightmap(lightmapworker *w) +{ + int needspace1 = sizeof(lightmapinfo) + w->w*w->h*w->bpp, + needspace2 = (w->type&LM_TYPE) == LM_BUMPMAP0 ? w->w*w->h*3 : 0, + needspace = needspace1 + needspace2, + bufend = (w->bufstart + w->bufused)%LIGHTMAPBUFSIZE, + availspace = LIGHTMAPBUFSIZE - w->bufused, + availspace1 = min(availspace, LIGHTMAPBUFSIZE - bufend), + availspace2 = min(availspace, w->bufstart); + if(availspace < needspace || (max(availspace1, availspace2) < needspace && (availspace1 < needspace1 || availspace2 < needspace2))) + { + if(tasklock) SDL_LockMutex(tasklock); + while(!w->doneworking) + { + lightmapinfo *l = w->firstlightmap; + for(; l && l->packed; l = l->next) + { + w->bufused -= l->bufsize; + w->bufstart = (w->bufstart + l->bufsize)%LIGHTMAPBUFSIZE; + } + w->firstlightmap = l; + if(!l) + { + w->lastlightmap = NULL; + w->bufstart = w->bufused = 0; + } + bufend = (w->bufstart + w->bufused)%LIGHTMAPBUFSIZE; + availspace = LIGHTMAPBUFSIZE - w->bufused; + availspace1 = min(availspace, LIGHTMAPBUFSIZE - bufend); + availspace2 = min(availspace, w->bufstart); + if(availspace >= needspace && (max(availspace1, availspace2) >= needspace || (availspace1 >= needspace1 && availspace2 >= needspace2))) break; + if(packlightmaps(w)) continue; + if(!w->spacecond || !tasklock) break; + w->needspace = true; + SDL_CondWait(w->spacecond, tasklock); + w->needspace = false; + } + if(tasklock) SDL_UnlockMutex(tasklock); + } + int usedspace = needspace; + lightmapinfo *l = NULL; + if(availspace1 >= needspace1) + { + l = (lightmapinfo *)&w->buf[bufend]; + w->colorbuf = (uchar *)(l + 1); + if((w->type&LM_TYPE) != LM_BUMPMAP0) w->raybuf = NULL; + else if(availspace1 >= needspace) w->raybuf = (bvec *)&w->buf[bufend + needspace1]; + else + { + w->raybuf = (bvec *)w->buf; + usedspace += availspace1 - needspace1; + } + } + else if(availspace2 >= needspace) + { + usedspace += availspace1; + l = (lightmapinfo *)w->buf; + w->colorbuf = (uchar *)(l + 1); + w->raybuf = (w->type&LM_TYPE) == LM_BUMPMAP0 ? (bvec *)&w->buf[needspace1] : NULL; + } + else return NULL; + w->bufused += usedspace; + l->next = NULL; + l->c = w->c; + l->type = w->type; + l->w = w->w; + l->h = w->h; + l->bpp = w->bpp; + l->colorbuf = w->colorbuf; + l->raybuf = w->raybuf; + l->packed = false; + l->bufsize = usedspace; + l->surface = -1; + l->layers = 0; + if(!w->firstlightmap) w->firstlightmap = l; + if(w->lastlightmap) w->lastlightmap->next = l; + w->lastlightmap = l; + if(!w->curlightmaps) w->curlightmaps = l; + return l; +} + +static void freelightmap(lightmapworker *w) +{ + lightmapinfo *l = w->lastlightmap; + if(!l || l->surface >= 0) return; + if(w->firstlightmap == w->lastlightmap) + { + w->firstlightmap = w->lastlightmap = w->curlightmaps = NULL; + w->bufstart = w->bufused = 0; + } + else + { + w->bufused -= l->bufsize - sizeof(lightmapinfo); + l->bufsize = sizeof(lightmapinfo); + l->packed = true; + } + if(w->curlightmaps == l) w->curlightmaps = NULL; +} + +static int setupsurface(lightmapworker *w, plane planes[2], int numplanes, const vec *p, const vec *n, int numverts, vertinfo *litverts, bool preview = false) +{ + vec u, v, t; + vec2 c[MAXFACEVERTS]; + + u = vec(p[2]).sub(p[0]).normalize(); + v.cross(planes[0], u); + c[0] = vec2(0, 0); + if(numplanes >= 2) t.cross(planes[1], u); else t = v; + vec r1 = vec(p[1]).sub(p[0]); + c[1] = vec2(r1.dot(u), min(r1.dot(v), 0.0f)); + c[2] = vec2(vec(p[2]).sub(p[0]).dot(u), 0); + for(int i = 3; i < numverts; i++) + { + vec r = vec(p[i]).sub(p[0]); + c[i] = vec2(r.dot(u), max(r.dot(t), 0.0f)); + } + + float carea = 1e16f; + vec2 cx(0, 0), cy(0, 0), co(0, 0), cmin(0, 0), cmax(0, 0); + loopi(numverts) + { + vec2 px = vec2(c[i+1 < numverts ? i+1 : 0]).sub(c[i]); + float len = px.squaredlen(); + if(!len) continue; + px.mul(1/sqrtf(len)); + vec2 py(-px.y, px.x), pmin(0, 0), pmax(0, 0); + if(numplanes >= 2 && (i == 0 || i >= 3)) px.neg(); + loopj(numverts) + { + vec2 rj = vec2(c[j]).sub(c[i]), pj(rj.dot(px), rj.dot(py)); + pmin.x = min(pmin.x, pj.x); + pmin.y = min(pmin.y, pj.y); + pmax.x = max(pmax.x, pj.x); + pmax.y = max(pmax.y, pj.y); + } + float area = (pmax.x-pmin.x)*(pmax.y-pmin.y); + if(area < carea) { carea = area; cx = px; cy = py; co = c[i]; cmin = pmin; cmax = pmax; } + } + + int scale = int(min(cmax.x - cmin.x, cmax.y - cmin.y)); + float lpu = 16.0f / float(lightlod && scale < (1 << lightlod) ? max(lightprecision / 2, 1) : lightprecision); + int lw = clamp(int(ceil((cmax.x - cmin.x + 1)*lpu)), LM_MINW, LM_MAXW), lh = clamp(int(ceil((cmax.y - cmin.y + 1)*lpu)), LM_MINH, LM_MAXH); + w->w = lw; + w->h = lh; + if(!preview) + { + w->w += 2*blurlms; + w->h += 2*blurlms; + } + if(!alloclightmap(w)) return NO_SURFACE; + + vec2 cscale = vec2(cmax).sub(cmin).div(vec2(lw-1, lh-1)), + comin = vec2(cx).mul(cmin.x).add(vec2(cy).mul(cmin.y)).add(co); + loopi(numverts) + { + vec2 ri = vec2(c[i]).sub(comin); + c[i] = vec2(ri.dot(cx)/cscale.x, ri.dot(cy)/cscale.y); + } + + vec xstep1 = vec(v).mul(cx.y).add(vec(u).mul(cx.x)).mul(cscale.x), + ystep1 = vec(v).mul(cy.y).add(vec(u).mul(cy.x)).mul(cscale.y), + origin1 = vec(v).mul(comin.y).add(vec(u).mul(comin.x)).add(p[0]), + xstep2 = xstep1, ystep2 = ystep1, origin2 = origin1; + float side0 = LM_MAXW + 1, sidestep = 0; + if(numplanes >= 2) + { + xstep2 = vec(t).mul(cx.y).add(vec(u).mul(cx.x)).mul(cscale.x); + ystep2 = vec(t).mul(cy.y).add(vec(u).mul(cy.x)).mul(cscale.y); + origin2 = vec(t).mul(comin.y).add(vec(u).mul(comin.x)).add(p[0]); + if(cx.y) { side0 = comin.y/-(cx.y*cscale.x); sidestep = cy.y*cscale.y/-(cx.y*cscale.x); } + else if(cy.y) { side0 = ceil(comin.y/-(cy.y*cscale.y))*(LM_MAXW + 1); sidestep = -(LM_MAXW + 1); if(cy.y < 0) { side0 = (LM_MAXW + 1) - side0; sidestep = -sidestep; } } + else side0 = comin.y <= 0 ? LM_MAXW + 1 : -1; + } + + int surftype = NO_SURFACE; + if(preview) + { + surftype = previewlightmapalpha(w, lpu, origin1, xstep1, ystep1, origin2, xstep2, ystep2, side0, sidestep); + } + else + { + lerpvert lv[MAXFACEVERTS]; + int numv = numverts; + calclerpverts(c, n, lv, numv); + + if(!generatelightmap(w, lpu, lv, numv, origin1, xstep1, ystep1, origin2, xstep2, ystep2, side0, sidestep)) return NO_SURFACE; + surftype = finishlightmap(w); + } + if(surftypew) texscale.x *= float(w->w - 1) / (lw - 1); + if(lh != w->h) texscale.y *= float(w->h - 1) / (lh - 1); + loopk(numverts) + { + litverts[k].u = ushort(floor(clamp(c[k].x*texscale.x, 0.0f, float(USHRT_MAX)))); + litverts[k].v = ushort(floor(clamp(c[k].y*texscale.y, 0.0f, float(USHRT_MAX)))); + } + return surftype; +} + +static void removelmalpha(lightmapworker *w) +{ + if(!(w->type&LM_ALPHA)) return; + for(uchar *dst = w->colorbuf, *src = w->colorbuf, *end = &src[w->w*w->h*4]; + src < end; + dst += 3, src += 4) + { + dst[0] = src[0]; + dst[1] = src[1]; + dst[2] = src[2]; + } + w->type &= ~LM_ALPHA; + w->bpp = 3; + w->lastlightmap->type = w->type; + w->lastlightmap->bpp = w->bpp; +} + +static lightmapinfo *setupsurfaces(lightmapworker *w, lightmaptask &task) +{ + cube &c = *task.c; + const ivec &co = task.o; + int size = task.size, usefacemask = task.usefaces; + + w->curlightmaps = NULL; + w->c = &c; + + surfaceinfo surfaces[6]; + vertinfo litverts[6*2*MAXFACEVERTS]; + int numlitverts = 0; + memclear(surfaces); + loopi(6) + { + int usefaces = usefacemask&0xF; + usefacemask >>= 4; + if(!usefaces) + { + if(!c.ext) continue; + surfaceinfo &surf = surfaces[i]; + surf = c.ext->surfaces[i]; + int numverts = surf.totalverts(); + if(numverts) + { + memcpy(&litverts[numlitverts], c.ext->verts() + surf.verts, numverts*sizeof(vertinfo)); + surf.verts = numlitverts; + numlitverts += numverts; + } + continue; + } + + VSlot &vslot = lookupvslot(c.texture[i], false), + *layer = vslot.layer && !(c.material&MAT_ALPHA) ? &lookupvslot(vslot.layer, false) : NULL; + Shader *shader = vslot.slot->shader; + int shadertype = shader->type; + if(layer) shadertype |= layer->slot->shader->type; + + surfaceinfo &surf = surfaces[i]; + vertinfo *curlitverts = &litverts[numlitverts]; + int numverts = c.ext ? c.ext->surfaces[i].numverts&MAXFACEVERTS : 0; + ivec mo(co); + int msz = size, convex = 0; + if(numverts) + { + vertinfo *verts = c.ext->verts() + c.ext->surfaces[i].verts; + loopj(numverts) curlitverts[j].set(verts[j].getxyz()); + if(c.merged&(1<slot = vslot.slot; + w->vslot = &vslot; + w->type = shader->type&SHADER_NORMALSLMS ? LM_BUMPMAP0 : LM_DIFFUSE; + if(layer) w->type |= LM_ALPHA; + w->bpp = w->type&LM_ALPHA ? 4 : 3; + w->orient = i; + w->rotate = vslot.rotation; + int surftype = setupsurface(w, planes, numplanes, pos, n, numverts, curlitverts); + switch(surftype) + { + case SURFACE_LIGHTMAP_BOTTOM: + if((shader->type^layer->slot->shader->type)&SHADER_NORMALSLMS || + (shader->type&SHADER_NORMALSLMS && vslot.rotation!=layer->rotation)) + { + freelightmap(w); + break; + } + // fall through + case SURFACE_LIGHTMAP_BLEND: + case SURFACE_LIGHTMAP_TOP: + { + if(!(surf.numverts&MAXFACEVERTS)) + { + surf.verts = numlitverts; + surf.numverts |= numverts; + numlitverts += numverts; + } + + w->lastlightmap->surface = i; + w->lastlightmap->layers = (surftype==SURFACE_LIGHTMAP_BOTTOM ? LAYER_BOTTOM : LAYER_TOP); + if(surftype==SURFACE_LIGHTMAP_BLEND) + { + surf.numverts |= LAYER_BLEND; + w->lastlightmap->layers = LAYER_TOP; + if((shader->type^layer->slot->shader->type)&SHADER_NORMALSLMS || + (shader->type&SHADER_NORMALSLMS && vslot.rotation!=layer->rotation)) + break; + w->lastlightmap->layers |= LAYER_BOTTOM; + } + else + { + if(surftype==SURFACE_LIGHTMAP_BOTTOM) + { + surf.numverts |= LAYER_BOTTOM; + w->lastlightmap->layers = LAYER_BOTTOM; + } + else + { + surf.numverts |= LAYER_TOP; + w->lastlightmap->layers = LAYER_TOP; + } + if(w->type&LM_ALPHA) removelmalpha(w); + } + continue; + } + + case SURFACE_AMBIENT_BOTTOM: + freelightmap(w); + surf.numverts |= layer ? LAYER_BOTTOM : LAYER_TOP; + continue; + + case SURFACE_AMBIENT_TOP: + freelightmap(w); + surf.numverts |= LAYER_TOP; + continue; + + default: + freelightmap(w); + continue; + } + + w->slot = layer->slot; + w->vslot = layer; + w->type = layer->slot->shader->type&SHADER_NORMALSLMS ? LM_BUMPMAP0 : LM_DIFFUSE; + w->bpp = 3; + w->rotate = layer->rotation; + vertinfo *blendverts = surf.numverts&MAXFACEVERTS ? &curlitverts[numverts] : curlitverts; + switch(setupsurface(w, planes, numplanes, pos, n, numverts, blendverts)) + { + case SURFACE_LIGHTMAP_TOP: + { + if(!(surf.numverts&MAXFACEVERTS)) + { + surf.verts = numlitverts; + surf.numverts |= numverts; + numlitverts += numverts; + } + else if(!(surf.numverts&LAYER_DUP)) + { + surf.numverts |= LAYER_DUP; + w->lastlightmap->layers |= LAYER_DUP; + loopk(numverts) + { + vertinfo &src = curlitverts[k]; + vertinfo &dst = blendverts[k]; + dst.setxyz(src.getxyz()); + dst.norm = src.norm; + } + numlitverts += numverts; + } + surf.numverts |= LAYER_BOTTOM; + w->lastlightmap->layers |= LAYER_BOTTOM; + + w->lastlightmap->surface = i; + break; + } + + case SURFACE_AMBIENT_TOP: + { + freelightmap(w); + surf.numverts |= LAYER_BOTTOM; + break; + } + + default: freelightmap(w); break; + } + } + loopk(6) + { + surfaceinfo &surf = surfaces[k]; + if(surf.used()) + { + cubeext *ext = c.ext && c.ext->maxverts >= numlitverts ? c.ext : growcubeext(c.ext, numlitverts); + memcpy(ext->surfaces, surfaces, sizeof(ext->surfaces)); + memcpy(ext->verts(), litverts, numlitverts*sizeof(vertinfo)); + task.ext = ext; + break; + } + } + return w->curlightmaps ? w->curlightmaps : (lightmapinfo *)-1; +} + +int lightmapworker::work(void *data) +{ + lightmapworker *w = (lightmapworker *)data; + SDL_LockMutex(tasklock); + while(!w->doneworking) + { + if(allocidx < lightmaptasks[0].length()) + { + lightmaptask &t = lightmaptasks[0][allocidx++]; + t.worker = w; + SDL_UnlockMutex(tasklock); + lightmapinfo *l = setupsurfaces(w, t); + SDL_LockMutex(tasklock); + t.lightmaps = l; + packlightmaps(w); + } + else + { + if(packidx >= lightmaptasks[0].length()) SDL_CondSignal(emptycond); + SDL_CondWait(fullcond, tasklock); + } + } + SDL_UnlockMutex(tasklock); + return 0; +} + +static bool processtasks(bool finish = false) +{ + if(tasklock) SDL_LockMutex(tasklock); + while(finish || lightmaptasks[1].length()) + { + if(packidx >= lightmaptasks[0].length()) + { + if(lightmaptasks[1].empty()) break; + lightmaptasks[0].setsize(0); + lightmaptasks[0].move(lightmaptasks[1]); + packidx = allocidx = 0; + if(fullcond) SDL_CondBroadcast(fullcond); + } + else if(lightmapping > 1) + { + SDL_CondWaitTimeout(emptycond, tasklock, 250); + CHECK_PROGRESS_LOCKED({ SDL_UnlockMutex(tasklock); return false; }, SDL_UnlockMutex(tasklock), SDL_LockMutex(tasklock)); + } + else + { + while(allocidx < lightmaptasks[0].length()) + { + lightmaptask &t = lightmaptasks[0][allocidx++]; + t.worker = lightmapworkers[0]; + t.lightmaps = setupsurfaces(lightmapworkers[0], t); + packlightmaps(lightmapworkers[0]); + CHECK_PROGRESS(return false); + } + } + } + if(tasklock) SDL_UnlockMutex(tasklock); + return true; +} + +static void generatelightmaps(cube *c, const ivec &co, int size) +{ + CHECK_PROGRESS(return); + + taskprogress++; + + loopi(8) + { + ivec o(i, co, size); + if(c[i].children) + generatelightmaps(c[i].children, o, size >> 1); + else if(!isempty(c[i])) + { + if(c[i].ext) + { + loopj(6) + { + surfaceinfo &surf = c[i].ext->surfaces[j]; + if(surf.lmid[0] >= LMID_RESERVED || surf.lmid[1] >= LMID_RESERVED) goto nextcube; + surf.clear(); + } + } + int usefacemask = 0; + loopj(6) if(c[i].texture[j] != DEFAULT_SKY && (!(c[i].merged&(1<surfaces[j].numverts&MAXFACEVERTS))) + { + usefacemask |= visibletris(c[i], j, o, size)<<(4*j); + } + if(usefacemask) + { + lightmaptask &t = lightmaptasks[1].add(); + t.o = o; + t.size = size; + t.usefaces = usefacemask; + t.c = &c[i]; + t.ext = NULL; + t.lightmaps = NULL; + t.progress = taskprogress; + if(lightmaptasks[1].length() >= MAXLIGHTMAPTASKS && !processtasks()) return; + } + } + nextcube:; + } +} + +static bool previewblends(lightmapworker *w, cube &c, const ivec &co, int size) +{ + if(isempty(c) || c.material&MAT_ALPHA) return false; + + int usefacemask = 0; + loopi(6) if(c.texture[i] != DEFAULT_SKY && lookupvslot(c.texture[i], false).layer) + usefacemask |= visibletris(c, i, co, size)<<(4*i); + if(!usefacemask) return false; + + if(!setblendmaporigin(w->blendmapcache, co, size)) + { + if(!c.ext) return false; + bool blends = false; + loopi(6) if(c.ext->surfaces[i].numverts&LAYER_BOTTOM) + { + c.ext->surfaces[i].brighten(); + blends = true; + } + return blends; + } + + w->firstlightmap = w->lastlightmap = w->curlightmaps = NULL; + w->bufstart = w->bufused = 0; + w->c = &c; + + surfaceinfo surfaces[6]; + vertinfo litverts[6*2*MAXFACEVERTS]; + int numlitverts = 0; + memcpy(surfaces, c.ext ? c.ext->surfaces : brightsurfaces, sizeof(surfaces)); + loopi(6) + { + int usefaces = usefacemask&0xF; + usefacemask >>= 4; + if(!usefaces) + { + surfaceinfo &surf = surfaces[i]; + int numverts = surf.totalverts(); + if(numverts) + { + memcpy(&litverts[numlitverts], c.ext->verts() + surf.verts, numverts*sizeof(vertinfo)); + surf.verts = numlitverts; + numlitverts += numverts; + } + continue; + } + + VSlot &vslot = lookupvslot(c.texture[i], false), + &layer = lookupvslot(vslot.layer, false); + Shader *shader = vslot.slot->shader; + int shadertype = shader->type | layer.slot->shader->type; + + vertinfo *curlitverts = &litverts[numlitverts]; + int numverts = 0; + ivec v[4]; + genfaceverts(c, i, v); + int convex = flataxisface(c, i) ? 0 : faceconvexity(v), + order = usefaces&4 || convex < 0 ? 1 : 0; + ivec vo = ivec(co).mask(0xFFF).shl(3); + curlitverts[numverts++].set(v[order].mul(size).add(vo)); + if(usefaces&1) curlitverts[numverts++].set(v[order+1].mul(size).add(vo)); + curlitverts[numverts++].set(v[order+2].mul(size).add(vo)); + if(usefaces&2) curlitverts[numverts++].set(v[(order+3)&3].mul(size).add(vo)); + + vec pos[4], n[4], po(ivec(co).mask(~0xFFF)); + loopj(numverts) pos[j] = vec(curlitverts[j].getxyz()).mul(1.0f/8).add(po); + + plane planes[2]; + int numplanes = 0; + planes[numplanes++].toplane(pos[0], pos[1], pos[2]); + if(numverts < 4 || !convex) loopk(numverts) n[k] = planes[0]; + else + { + planes[numplanes++].toplane(pos[0], pos[2], pos[3]); + vec avg = vec(planes[0]).add(planes[1]).normalize(); + n[0] = avg; + n[1] = planes[0]; + n[2] = avg; + for(int k = 3; k < numverts; k++) n[k] = planes[1]; + } + + surfaceinfo &surf = surfaces[i]; + w->slot = vslot.slot; + w->vslot = &vslot; + w->type = shadertype&SHADER_NORMALSLMS ? LM_BUMPMAP0|LM_ALPHA : LM_DIFFUSE|LM_ALPHA; + w->bpp = 4; + w->orient = i; + w->rotate = vslot.rotation; + int surftype = setupsurface(w, planes, numplanes, pos, n, numverts, curlitverts, true); + switch(surftype) + { + case SURFACE_AMBIENT_TOP: + surf = brightsurface; + continue; + + case SURFACE_AMBIENT_BOTTOM: + surf = brightbottomsurface; + continue; + + case SURFACE_LIGHTMAP_BLEND: + { + if(surf.numverts == (LAYER_BLEND|numverts) && + surf.lmid[0] == surf.lmid[1] && + (surf.numverts&MAXFACEVERTS) == numverts && + !memcmp(curlitverts, c.ext->verts() + surf.verts, numverts*sizeof(vertinfo)) && + lightmaps.inrange(surf.lmid[0]-LMID_RESERVED) && + lightmaps[surf.lmid[0]-LMID_RESERVED].type==w->type) + { + vertinfo *oldverts = c.ext->verts() + surf.verts; + layoutinfo layout; + layout.w = w->w; + layout.h = w->h; + layout.x = (oldverts[0].x - curlitverts[0].x)/((USHRT_MAX+1)/LM_PACKW); + layout.y = (oldverts[0].y - curlitverts[0].y)/((USHRT_MAX+1)/LM_PACKH); + if(LM_PACKW - layout.x >= w->w && LM_PACKH - layout.y >= w->h) + { + layout.lmid = surf.lmid[0]; + copylightmap(*w->lastlightmap, layout); + updatelightmap(layout); + surf.verts = numlitverts; + numlitverts += numverts; + continue; + } + } + + surf.verts = numlitverts; + surf.numverts = LAYER_BLEND|numverts; + numlitverts += numverts; + layoutinfo layout; + if(packlightmap(*w->lastlightmap, layout)) updatelightmap(layout); + surf.lmid[0] = surf.lmid[1] = layout.lmid; + ushort offsetx = layout.x*((USHRT_MAX+1)/LM_PACKW), offsety = layout.y*((USHRT_MAX+1)/LM_PACKH); + loopk(numverts) + { + vertinfo &v = curlitverts[k]; + v.u += offsetx; + v.v += offsety; + } + continue; + } + } + } + + setsurfaces(c, surfaces, litverts, numlitverts); + return true; +} + +static bool previewblends(lightmapworker *w, cube *c, const ivec &co, int size, const ivec &bo, const ivec &bs) +{ + bool changed = false; + loopoctabox(co, size, bo, bs) + { + ivec o(i, co, size); + cubeext *ext = c[i].ext; + if(ext && ext->va && ext->va->hasmerges) + { + changed = true; + destroyva(ext->va); + ext->va = NULL; + invalidatemerges(c[i], co, size, true); + } + if(c[i].children ? previewblends(w, c[i].children, o, size/2, bo, bs) : previewblends(w, c[i], o, size)) + { + changed = true; + ext = c[i].ext; + if(ext && ext->va) + { + destroyva(ext->va); + ext->va = NULL; + } + } + } + return changed; +} + +void previewblends(const ivec &bo, const ivec &bs) +{ + loadlayermasks(); + if(lightmapworkers.empty()) lightmapworkers.add(new lightmapworker); + lightmapworkers[0]->reset(); + if(previewblends(lightmapworkers[0], worldroot, ivec(0, 0, 0), worldsize/2, bo, bs)) + commitchanges(true); +} + +void cleanuplightmaps() +{ + loopv(lightmaps) + { + LightMap &lm = lightmaps[i]; + lm.tex = lm.offsetx = lm.offsety = -1; + } + loopv(lightmaptexs) glDeleteTextures(1, &lightmaptexs[i].id); + lightmaptexs.shrink(0); + if(progresstex) { glDeleteTextures(1, &progresstex); progresstex = 0; } +} + +void resetlightmaps(bool fullclean) +{ + cleanuplightmaps(); + lightmaps.shrink(0); + compressed.clear(); + clearlightcache(); + if(fullclean) while(lightmapworkers.length()) delete lightmapworkers.pop(); +} + +lightmapworker::lightmapworker() +{ + buf = new uchar[LIGHTMAPBUFSIZE]; + bufstart = bufused = 0; + firstlightmap = lastlightmap = curlightmaps = NULL; + ambient = new uchar[4*(LM_MAXW + 4)*(LM_MAXH + 4)]; + blur = new uchar[4*(LM_MAXW + 4)*(LM_MAXH + 4)]; + colordata = new vec[4*(LM_MAXW+1 + 4)*(LM_MAXH+1 + 4)]; + raydata = new vec[(LM_MAXW + 4)*(LM_MAXH + 4)]; + shadowraycache = newshadowraycache(); + blendmapcache = newblendmapcache(); + needspace = doneworking = false; + spacecond = NULL; + thread = NULL; +} + +lightmapworker::~lightmapworker() +{ + cleanupthread(); + delete[] buf; + delete[] ambient; + delete[] blur; + delete[] colordata; + delete[] raydata; + freeshadowraycache(shadowraycache); + freeblendmapcache(blendmapcache); +} + +void lightmapworker::cleanupthread() +{ + if(spacecond) { SDL_DestroyCond(spacecond); spacecond = NULL; } + thread = NULL; +} + +void lightmapworker::reset() +{ + bufstart = bufused = 0; + firstlightmap = lastlightmap = curlightmaps = NULL; + needspace = doneworking = false; + resetshadowraycache(shadowraycache); +} + +bool lightmapworker::setupthread() +{ + if(!spacecond) spacecond = SDL_CreateCond(); + if(!spacecond) return false; + thread = SDL_CreateThread(work, "lightmap worker", this); + return thread!=NULL; +} + +static Uint32 calclighttimer(Uint32 interval, void *param) +{ + check_calclight_progress = true; + return interval; +} + +bool setlightmapquality(int quality) +{ + switch(quality) + { + case 1: lmshadows = 2; lmaa = 3; lerptjoints = 1; break; + case 0: lmshadows = lmshadows_; lmaa = lmaa_; lerptjoints = lerptjoints_; break; + case -1: lmshadows = 1; lmaa = 0; lerptjoints = 0; break; + default: return false; + } + return true; +} + +VARP(lightthreads, 0, 0, 16); + +#define ALLOCLOCK(name, init) { if(lightmapping > 1) name = init(); if(!name) lightmapping = 1; } +#define FREELOCK(name, destroy) { if(name) { destroy(name); name = NULL; } } + +static void cleanuplocks() +{ + FREELOCK(lightlock, SDL_DestroyMutex); + FREELOCK(tasklock, SDL_DestroyMutex); + FREELOCK(fullcond, SDL_DestroyCond); + FREELOCK(emptycond, SDL_DestroyCond); +} + +static void setupthreads(int numthreads) +{ + loopi(2) lightmaptasks[i].setsize(0); + lightmapexts.setsize(0); + packidx = allocidx = 0; + lightmapping = numthreads; + if(lightmapping > 1) + { + ALLOCLOCK(lightlock, SDL_CreateMutex); + ALLOCLOCK(tasklock, SDL_CreateMutex); + ALLOCLOCK(fullcond, SDL_CreateCond); + ALLOCLOCK(emptycond, SDL_CreateCond); + } + while(lightmapworkers.length() < lightmapping) lightmapworkers.add(new lightmapworker); + loopi(lightmapping) + { + lightmapworker *w = lightmapworkers[i]; + w->reset(); + if(lightmapping <= 1 || w->setupthread()) continue; + w->cleanupthread(); + lightmapping = i >= 1 ? max(i, 2) : 1; + break; + } + if(lightmapping <= 1) cleanuplocks(); +} + +static void cleanupthreads() +{ + processtasks(true); + if(lightmapping > 1) + { + SDL_LockMutex(tasklock); + loopv(lightmapworkers) lightmapworkers[i]->doneworking = true; + SDL_CondBroadcast(fullcond); + loopv(lightmapworkers) + { + lightmapworker *w = lightmapworkers[i]; + if(w->needspace && w->spacecond) SDL_CondSignal(w->spacecond); + } + SDL_UnlockMutex(tasklock); + loopv(lightmapworkers) + { + lightmapworker *w = lightmapworkers[i]; + if(w->thread) SDL_WaitThread(w->thread, NULL); + } + } + loopv(lightmapexts) + { + lightmapext &e = lightmapexts[i]; + setcubeext(*e.c, e.ext); + } + loopv(lightmapworkers) lightmapworkers[i]->cleanupthread(); + cleanuplocks(); + lightmapping = 0; +} + +void calclight(int *quality) +{ + if(!setlightmapquality(*quality)) + { + conoutf(CON_ERROR, "valid range for calclight quality is -1..1"); + return; + } + renderbackground("computing lightmaps... (esc to abort)"); + mpremip(true); + optimizeblendmap(); + loadlayermasks(); + int numthreads = lightthreads > 0 ? lightthreads : numcpus; + if(numthreads > 1) preloadusedmapmodels(false, true); + resetlightmaps(false); + clearsurfaces(worldroot); + taskprogress = progress = 0; + progresstexticks = 0; + progresslightmap = -1; + calclight_canceled = false; + check_calclight_progress = false; + SDL_TimerID timer = SDL_AddTimer(250, calclighttimer, NULL); + Uint32 start = SDL_GetTicks(); + calcnormals(lerptjoints > 0); + show_calclight_progress(); + setupthreads(numthreads); + generatelightmaps(worldroot, ivec(0, 0, 0), worldsize >> 1); + cleanupthreads(); + clearnormals(); + Uint32 end = SDL_GetTicks(); + if(timer) SDL_RemoveTimer(timer); + uint total = 0, lumels = 0; + loopv(lightmaps) + { + insertunlit(i); + if(!editmode) lightmaps[i].finalize(); + total += lightmaps[i].lightmaps; + lumels += lightmaps[i].lumels; + } + if(!editmode) compressed.clear(); + initlights(); + renderbackground("lighting done..."); + allchanged(); + if(calclight_canceled) + conoutf("calclight aborted"); + else + conoutf("generated %d lightmaps using %d%% of %d textures (%.1f seconds)", + total, + lightmaps.length() ? lumels * 100 / (lightmaps.length() * LM_PACKW * LM_PACKH) : 0, + lightmaps.length(), + (end - start) / 1000.0f); +} + +COMMAND(calclight, "i"); + +VAR(patchnormals, 0, 0, 1); + +void patchlight(int *quality) +{ + if(noedit(true)) return; + if(!setlightmapquality(*quality)) + { + conoutf(CON_ERROR, "valid range for patchlight quality is -1..1"); + return; + } + renderbackground("patching lightmaps... (esc to abort)"); + loadlayermasks(); + int numthreads = lightthreads > 0 ? lightthreads : numcpus; + if(numthreads > 1) preloadusedmapmodels(false, true); + cleanuplightmaps(); + taskprogress = progress = 0; + progresstexticks = 0; + progresslightmap = -1; + int total = 0, lumels = 0; + loopv(lightmaps) + { + if((lightmaps[i].type&LM_TYPE) != LM_BUMPMAP1) progresslightmap = i; + total -= lightmaps[i].lightmaps; + lumels -= lightmaps[i].lumels; + } + calclight_canceled = false; + check_calclight_progress = false; + SDL_TimerID timer = SDL_AddTimer(250, calclighttimer, NULL); + if(patchnormals) renderprogress(0, "computing normals..."); + Uint32 start = SDL_GetTicks(); + if(patchnormals) calcnormals(lerptjoints > 0); + show_calclight_progress(); + setupthreads(numthreads); + generatelightmaps(worldroot, ivec(0, 0, 0), worldsize >> 1); + cleanupthreads(); + if(patchnormals) clearnormals(); + Uint32 end = SDL_GetTicks(); + if(timer) SDL_RemoveTimer(timer); + loopv(lightmaps) + { + total += lightmaps[i].lightmaps; + lumels += lightmaps[i].lumels; + } + initlights(); + renderbackground("lighting done..."); + allchanged(); + if(calclight_canceled) + conoutf("patchlight aborted"); + else + conoutf("patched %d lightmaps using %d%% of %d textures (%.1f seconds)", + total, + lightmaps.length() ? lumels * 100 / (lightmaps.length() * LM_PACKW * LM_PACKH) : 0, + lightmaps.length(), + (end - start) / 1000.0f); +} + +COMMAND(patchlight, "i"); + +void clearlightmaps() +{ + if(noedit(true)) return; + renderprogress(0, "clearing lightmaps..."); + resetlightmaps(false); + clearsurfaces(worldroot); + initlights(); + allchanged(); +} + +COMMAND(clearlightmaps, ""); + +void setfullbrightlevel(int fullbrightlevel) +{ + if(lightmaptexs.length() > LMID_BRIGHT) + { + uchar bright[3] = { uchar(fullbrightlevel), uchar(fullbrightlevel), uchar(fullbrightlevel) }; + createtexture(lightmaptexs[LMID_BRIGHT].id, 1, 1, bright, 0, 1); + } + initlights(); +} + +VARF(fullbright, 0, 0, 1, if(lightmaptexs.length()) { initlights(); lightents(); }); +VARF(fullbrightlevel, 0, 128, 255, setfullbrightlevel(fullbrightlevel)); + +vector lightmaptexs; + +static void rotatenormals(LightMap &lmlv, int x, int y, int w, int h, bool flipx, bool flipy, bool swapxy) +{ + uchar *lv = lmlv.data + 3*(y*LM_PACKW + x); + int stride = 3*(LM_PACKW-w); + loopi(h) + { + loopj(w) + { + if(flipx) lv[0] = 255 - lv[0]; + if(flipy) lv[1] = 255 - lv[1]; + if(swapxy) swap(lv[0], lv[1]); + lv += 3; + } + lv += stride; + } +} + +static void rotatenormals(cube *c) +{ + loopi(8) + { + cube &ch = c[i]; + if(ch.children) + { + rotatenormals(ch.children); + continue; + } + else if(!ch.ext) continue; + loopj(6) if(lightmaps.inrange(ch.ext->surfaces[j].lmid[0]+1-LMID_RESERVED)) + { + VSlot &vslot = lookupvslot(ch.texture[j], false); + if(!vslot.rotation) continue; + surfaceinfo &surface = ch.ext->surfaces[j]; + int numverts = surface.numverts&MAXFACEVERTS; + if(!numverts) continue; + LightMap &lmlv = lightmaps[surface.lmid[0]+1-LMID_RESERVED]; + if((lmlv.type&LM_TYPE)!=LM_BUMPMAP1) continue; + ushort x1 = USHRT_MAX, y1 = USHRT_MAX, x2 = 0, y2 = 0; + vertinfo *verts = ch.ext->verts() + surface.verts; + loopk(numverts) + { + vertinfo &v = verts[k]; + x1 = min(x1, v.u); + y1 = min(y1, v.u); + x2 = max(x2, v.u); + y2 = max(y2, v.v); + } + if(x1 > x2 || y1 > y2) continue; + x1 /= (USHRT_MAX+1)/LM_PACKW; + y1 /= (USHRT_MAX+1)/LM_PACKH; + x2 /= (USHRT_MAX+1)/LM_PACKW; + y2 /= (USHRT_MAX+1)/LM_PACKH; + const texrotation &r = texrotations[vslot.rotation < 4 ? 4-vslot.rotation : vslot.rotation]; + rotatenormals(lmlv, x1, y1, x2-x1, y1-y1, r.flipx, r.flipy, r.swapxy); + } + } +} + +void fixlightmapnormals() +{ + rotatenormals(worldroot); +} + +void fixrotatedlightmaps(cube &c, const ivec &co, int size) +{ + if(c.children) + { + loopi(8) fixrotatedlightmaps(c.children[i], ivec(i, co, size>>1), size>>1); + return; + } + if(!c.ext) return; + loopi(6) + { + if(c.merged&(1<surfaces[i]; + int numverts = surf.numverts&MAXFACEVERTS; + if(numverts!=4 || (surf.lmid[0] < LMID_RESERVED && surf.lmid[1] < LMID_RESERVED)) continue; + vertinfo *verts = c.ext->verts() + surf.verts; + int vis = visibletris(c, i, co, size); + if(!vis || vis==3) continue; + if((verts[0].u != verts[1].u || verts[0].v != verts[1].v) && + (verts[0].u != verts[3].u || verts[0].v != verts[3].v) && + (verts[2].u != verts[1].u || verts[2].v != verts[1].v) && + (verts[2].u != verts[3].u || verts[2].v != verts[3].v)) + continue; + if(vis&4) + { + vertinfo tmp = verts[0]; + verts[0].x = verts[1].x; verts[0].y = verts[1].y; verts[0].z = verts[1].z; + verts[1].x = verts[2].x; verts[1].y = verts[2].y; verts[1].z = verts[2].z; + verts[2].x = verts[3].x; verts[2].y = verts[3].y; verts[2].z = verts[3].z; + verts[3].x = tmp.x; verts[3].y = tmp.y; verts[3].z = tmp.z; + if(surf.numverts&LAYER_DUP) loopk(4) + { + vertinfo &v = verts[k], &b = verts[k+4]; + b.x = v.x; + b.y = v.y; + b.z = v.z; + } + } + surf.numverts = (surf.numverts & ~MAXFACEVERTS) | 3; + if(vis&2) + { + verts[1] = verts[2]; verts[2] = verts[3]; + if(surf.numverts&LAYER_DUP) { verts[3] = verts[4]; verts[4] = verts[6]; verts[5] = verts[7]; } + } + else if(surf.numverts&LAYER_DUP) { verts[3] = verts[4]; verts[4] = verts[5]; verts[5] = verts[6]; } + } +} + +void fixrotatedlightmaps() +{ + loopi(8) fixrotatedlightmaps(worldroot[i], ivec(i, ivec(0, 0, 0), worldsize>>1), worldsize>>1); +} + +static void copylightmap(LightMap &lm, uchar *dst, size_t stride) +{ + const uchar *c = lm.data; + loopi(LM_PACKH) + { + memcpy(dst, c, lm.bpp*LM_PACKW); + c += lm.bpp*LM_PACKW; + dst += stride; + } +} + +void genreservedlightmaptexs() +{ + while(lightmaptexs.length() < LMID_RESERVED) + { + LightMapTexture &tex = lightmaptexs.add(); + tex.type = lightmaptexs.length()&1 ? LM_DIFFUSE : LM_BUMPMAP1; + glGenTextures(1, &tex.id); + } + uchar unlit[3] = { ambientcolor[0], ambientcolor[1], ambientcolor[2] }; + createtexture(lightmaptexs[LMID_AMBIENT].id, 1, 1, unlit, 0, 1); + bvec front(128, 128, 255); + createtexture(lightmaptexs[LMID_AMBIENT1].id, 1, 1, &front, 0, 1); + uchar bright[3] = { uchar(fullbrightlevel), uchar(fullbrightlevel), uchar(fullbrightlevel) }; + createtexture(lightmaptexs[LMID_BRIGHT].id, 1, 1, bright, 0, 1); + createtexture(lightmaptexs[LMID_BRIGHT1].id, 1, 1, &front, 0, 1); + uchar dark[3] = { 0, 0, 0 }; + createtexture(lightmaptexs[LMID_DARK].id, 1, 1, dark, 0, 1); + createtexture(lightmaptexs[LMID_DARK1].id, 1, 1, &front, 0, 1); +} + +static void findunlit(int i) +{ + LightMap &lm = lightmaps[i]; + if(lm.unlitx>=0) return; + else if((lm.type&LM_TYPE)==LM_BUMPMAP0) + { + if(i+1>=lightmaps.length() || (lightmaps[i+1].type&LM_TYPE)!=LM_BUMPMAP1) return; + } + else if((lm.type&LM_TYPE)!=LM_DIFFUSE) return; + uchar *data = lm.data; + loop(y, 2) loop(x, LM_PACKW) + { + if(!data[0] && !data[1] && !data[2]) + { + memcpy(data, ambientcolor.v, 3); + if((lm.type&LM_TYPE)==LM_BUMPMAP0) ((bvec *)lightmaps[i+1].data)[y*LM_PACKW + x] = bvec(128, 128, 255); + lm.unlitx = x; + lm.unlity = y; + return; + } + if(data[0]==ambientcolor[0] && data[1]==ambientcolor[1] && data[2]==ambientcolor[2]) + { + if((lm.type&LM_TYPE)!=LM_BUMPMAP0 || ((bvec *)lightmaps[i+1].data)[y*LM_PACKW + x] == bvec(128, 128, 255)) + { + lm.unlitx = x; + lm.unlity = y; + return; + } + } + data += lm.bpp; + } +} + +VARF(roundlightmaptex, 0, 4, 16, { cleanuplightmaps(); initlights(); allchanged(); }); +VARF(batchlightmaps, 0, 4, 256, { cleanuplightmaps(); initlights(); allchanged(); }); + +void genlightmaptexs(int flagmask, int flagval) +{ + if(lightmaptexs.length() < LMID_RESERVED) genreservedlightmaptexs(); + + int remaining[LM_TYPE+1] = { 0 }, total = 0; + loopv(lightmaps) + { + LightMap &lm = lightmaps[i]; + if(lm.tex >= 0 || (lm.type&flagmask)!=flagval) continue; + int type = lm.type&LM_TYPE; + remaining[type]++; + total++; + if(lm.unlitx < 0) findunlit(i); + } + + int sizelimit = (maxtexsize ? min(maxtexsize, hwtexsize) : hwtexsize)/max(LM_PACKW, LM_PACKH); + sizelimit = min(batchlightmaps, sizelimit*sizelimit); + while(total) + { + int type = LM_DIFFUSE; + LightMap *firstlm = NULL; + loopv(lightmaps) + { + LightMap &lm = lightmaps[i]; + if(lm.tex >= 0 || (lm.type&flagmask) != flagval) continue; + type = lm.type&LM_TYPE; + firstlm = &lm; + break; + } + if(!firstlm) break; + int used = 0, uselimit = min(remaining[type], sizelimit); + do used++; while((1<type; + tex.w = LM_PACKW<<((used+1)/2); + tex.h = LM_PACKH<<(used/2); + int bpp = firstlm->bpp; + uchar *data = used ? new uchar[bpp*tex.w*tex.h] : NULL; + int offsetx = 0, offsety = 0; + loopv(lightmaps) + { + LightMap &lm = lightmaps[i]; + if(lm.tex >= 0 || (lm.type&flagmask) != flagval || (lm.type&LM_TYPE) != type) continue; + + lm.tex = lightmaptexs.length()-1; + lm.offsetx = offsetx; + lm.offsety = offsety; + if(tex.unlitx < 0 && lm.unlitx >= 0) + { + tex.unlitx = offsetx + lm.unlitx; + tex.unlity = offsety + lm.unlity; + } + + if(data) copylightmap(lm, &data[bpp*(offsety*tex.w + offsetx)], bpp*tex.w); + + offsetx += LM_PACKW; + if(offsetx >= tex.w) { offsetx = 0; offsety += LM_PACKH; } + if(offsety >= tex.h) break; + } + + glGenTextures(1, &tex.id); + createtexture(tex.id, tex.w, tex.h, data ? data : firstlm->data, 3, 1, bpp==4 ? GL_RGBA : GL_RGB); + if(data) delete[] data; + } +} + +bool brightengeom = false, shouldlightents = false; + +void clearlights() +{ + clearlightcache(); + const vector &ents = entities::getents(); + loopv(ents) + { + extentity &e = *ents[i]; + e.light.color = vec(1, 1, 1); + e.light.dir = vec(0, 0, 1); + } + shouldlightents = false; + + genlightmaptexs(LM_ALPHA, 0); + genlightmaptexs(LM_ALPHA, LM_ALPHA); + brightengeom = true; +} + +void lightent(extentity &e, float height) +{ + if(e.type==ET_LIGHT) return; + float ambient = 0.0f; + if(e.type==ET_MAPMODEL) + { + model *m = loadmodel(NULL, e.attr2); + if(m) height = m->above()*0.75f; + } + else if(e.type>=ET_GAMESPECIFIC) ambient = 0.4f; + vec target(e.o.x, e.o.y, e.o.z + height); + lightreaching(target, e.light.color, e.light.dir, false, &e, ambient); +} + +void lightents(bool force) +{ + if(!force && !shouldlightents) return; + + const vector &ents = entities::getents(); + loopv(ents) lightent(*ents[i]); + + shouldlightents = false; +} + +void initlights() +{ + if((fullbright && editmode) || lightmaps.empty()) + { + clearlights(); + return; + } + + clearlightcache(); + genlightmaptexs(LM_ALPHA, 0); + genlightmaptexs(LM_ALPHA, LM_ALPHA); + brightengeom = false; + shouldlightents = true; +} + +static inline void fastskylight(const vec &o, float tolerance, uchar *skylight, int flags = RAY_ALPHAPOLY, extentity *t = NULL, bool fast = false) +{ + flags |= RAY_SHADOW; + if(skytexturelight) flags |= RAY_SKIPSKY | (useskytexture ? RAY_SKYTEX : 0); + if(fast) + { + static const vec ray(0, 0, 1); + if(shadowray(vec(ray).mul(tolerance).add(o), ray, 1e16f, flags, t)>1e15f) + memcpy(skylight, skylightcolor.v, 3); + else memcpy(skylight, ambientcolor.v, 3); + } + else + { + static const vec rays[5] = + { + vec(cosf(66*RAD)*cosf(65*RAD), sinf(66*RAD)*cosf(65*RAD), sinf(65*RAD)), + vec(cosf(156*RAD)*cosf(65*RAD), sinf(156*RAD)*cosf(65*RAD), sinf(65*RAD)), + vec(cosf(246*RAD)*cosf(65*RAD), sinf(246*RAD)*cosf(65*RAD), sinf(65*RAD)), + vec(cosf(336*RAD)*cosf(65*RAD), sinf(336*RAD)*cosf(65*RAD), sinf(65*RAD)), + vec(0, 0, 1), + }; + int hit = 0; + loopi(5) if(shadowray(vec(rays[i]).mul(tolerance).add(o), rays[i], 1e16f, flags, t)>1e15f) hit++; + loopk(3) skylight[k] = uchar(ambientcolor[k] + (max(skylightcolor[k], ambientcolor[k]) - ambientcolor[k])*hit/5.0f); + } +} + +void lightreaching(const vec &target, vec &color, vec &dir, bool fast, extentity *t, float ambient) +{ + if((fullbright && editmode) || lightmaps.empty()) + { + color = vec(1, 1, 1); + dir = vec(0, 0, 1); + return; + } + + color = dir = vec(0, 0, 0); + const vector &ents = entities::getents(); + const vector &lights = checklightcache(int(target.x), int(target.y)); + loopv(lights) + { + extentity &e = *ents[lights[i]]; + if(e.type != ET_LIGHT) + continue; + + vec ray(target); + ray.sub(e.o); + float mag = ray.magnitude(); + if(e.attr1 && mag >= float(e.attr1)) + continue; + + if(mag < 1e-4f) ray = vec(0, 0, -1); + else + { + ray.div(mag); + if(shadowray(e.o, ray, mag, RAY_SHADOW | RAY_POLY, t) < mag) + continue; + } + + float intensity = 1; + if(e.attr1) + intensity -= mag / float(e.attr1); + if(e.attached && e.attached->type==ET_SPOTLIGHT) + { + vec spot = vec(e.attached->o).sub(e.o).normalize(); + float maxatten = sincos360[clamp(int(e.attached->attr1), 1, 89)].x, spotatten = (ray.dot(spot) - maxatten) / (1 - maxatten); + if(spotatten <= 0) continue; + intensity *= spotatten; + } + + //if(target==player->o) + //{ + // conoutf(CON_DEBUG, "%d - %f %f", i, intensity, mag); + //} + + vec lightcol = vec(e.attr2, e.attr3, e.attr4).mul(1.0f/255); + color.add(vec(lightcol).mul(intensity)); + dir.add(vec(ray).mul(-intensity*lightcol.x*lightcol.y*lightcol.z)); + } + if(sunlight && shadowray(target, sunlightdir, 1e16f, RAY_SHADOW | RAY_POLY | (skytexturelight ? RAY_SKIPSKY | (useskytexture ? RAY_SKYTEX : 0) : 0), t) > 1e15f) + { + vec lightcol = vec(sunlightcolor.x, sunlightcolor.y, sunlightcolor.z).mul(sunlightscale/255); + color.add(lightcol); + dir.add(vec(sunlightdir).mul(lightcol.x*lightcol.y*lightcol.z)); + } + if(hasskylight()) + { + uchar skylight[3]; + if(t) calcskylight(NULL, target, vec(0, 0, 0), 0.5f, skylight, RAY_POLY, t); + else fastskylight(target, 0.5f, skylight, RAY_POLY, t, fast); + loopk(3) color[k] = min(1.5f, max(max(skylight[k]/255.0f, ambient), color[k])); + } + else loopk(3) color[k] = min(1.5f, max(max(ambientcolor[k]/255.0f, ambient), color[k])); + if(dir.iszero()) dir = vec(0, 0, 1); + else dir.normalize(); +} + +entity *brightestlight(const vec &target, const vec &dir) +{ + if(sunlight && sunlightdir.dot(dir) > 0 && shadowray(target, sunlightdir, 1e16f, RAY_SHADOW | RAY_POLY | (skytexturelight ? RAY_SKIPSKY | (useskytexture ? RAY_SKYTEX : 0) : 0)) > 1e15f) + return &sunlightent; + const vector &ents = entities::getents(); + const vector &lights = checklightcache(int(target.x), int(target.y)); + extentity *brightest = NULL; + float bintensity = 0; + loopv(lights) + { + extentity &e = *ents[lights[i]]; + if(e.type != ET_LIGHT || vec(e.o).sub(target).dot(dir)<0) + continue; + + vec ray(target); + ray.sub(e.o); + float mag = ray.magnitude(); + if(e.attr1 && mag >= float(e.attr1)) + continue; + + ray.div(mag); + if(shadowray(e.o, ray, mag, RAY_SHADOW | RAY_POLY) < mag) + continue; + float intensity = 1; + if(e.attr1) + intensity -= mag / float(e.attr1); + if(e.attached && e.attached->type==ET_SPOTLIGHT) + { + vec spot = vec(e.attached->o).sub(e.o).normalize(); + float maxatten = sincos360[clamp(int(e.attached->attr1), 1, 89)].x, spotatten = (ray.dot(spot) - maxatten) / (1 - maxatten); + if(spotatten <= 0) continue; + intensity *= spotatten; + } + + if(!brightest || intensity > bintensity) + { + brightest = &e; + bintensity = intensity; + } + } + return brightest; +} + +void dumplms() +{ + loopv(lightmaps) + { + ImageData temp(LM_PACKW, LM_PACKH, lightmaps[i].bpp, lightmaps[i].data); + const char *map = game::getclientmap(), *name = strrchr(map, '/'); + defformatstring(buf, "lightmap_%s_%d.png", name ? name+1 : map, i); + savepng(buf, temp, true); + } +} + +COMMAND(dumplms, ""); + diff --git a/src/engine/lightmap.h b/src/engine/lightmap.h new file mode 100644 index 0000000..51ddc73 --- /dev/null +++ b/src/engine/lightmap.h @@ -0,0 +1,146 @@ +#define LM_MINW 2 +#define LM_MINH 2 +#define LM_MAXW 128 +#define LM_MAXH 128 +#define LM_PACKW 512 +#define LM_PACKH 512 + +struct PackNode +{ + PackNode *child1, *child2; + ushort x, y, w, h; + int available; + + PackNode() : child1(0), child2(0), x(0), y(0), w(LM_PACKW), h(LM_PACKH), available(min(LM_PACKW, LM_PACKH)) {} + PackNode(ushort x, ushort y, ushort w, ushort h) : child1(0), child2(0), x(x), y(y), w(w), h(h), available(min(w, h)) {} + + void clear() + { + DELETEP(child1); + DELETEP(child2); + } + + ~PackNode() + { + clear(); + } + + bool insert(ushort &tx, ushort &ty, ushort tw, ushort th); +}; + +enum +{ + LM_DIFFUSE = 0, + LM_BUMPMAP0, + LM_BUMPMAP1, + LM_TYPE = 0x0F, + + LM_ALPHA = 1<<4, + LM_FLAGS = 0xF0 +}; + +struct LightMap +{ + int type, bpp, tex, offsetx, offsety; + PackNode packroot; + uint lightmaps, lumels; + int unlitx, unlity; + uchar *data; + + LightMap() + : type(LM_DIFFUSE), bpp(3), tex(-1), offsetx(-1), offsety(-1), + lightmaps(0), lumels(0), unlitx(-1), unlity(-1), + data(NULL) + { + } + + ~LightMap() + { + if(data) delete[] data; + } + + void finalize() + { + packroot.clear(); + packroot.available = 0; + } + + void copy(ushort tx, ushort ty, uchar *src, ushort tw, ushort th); + bool insert(ushort &tx, ushort &ty, uchar *src, ushort tw, ushort th); +}; + +extern vector lightmaps; + +struct LightMapTexture +{ + int w, h, type; + GLuint id; + int unlitx, unlity; + + LightMapTexture() + : w(0), h(0), type(LM_DIFFUSE), id(0), unlitx(-1), unlity(-1) + {} +}; + +extern vector lightmaptexs; + +extern bvec ambientcolor, skylightcolor, sunlightcolor; +extern float sunlightscale; +extern vec sunlightdir; + +extern void clearlights(); +extern void initlights(); +extern void lightents(bool force = false); +extern void clearlightcache(int id = -1); +extern void resetlightmaps(bool fullclean = true); +extern void brightencube(cube &c); +extern void setsurfaces(cube &c, const surfaceinfo *surfs, const vertinfo *verts, int numverts); +extern void setsurface(cube &c, int orient, const surfaceinfo &surf, const vertinfo *verts, int numverts); +extern void previewblends(const ivec &bo, const ivec &bs); + +struct lerpvert +{ + vec normal; + vec2 tc; + + bool operator==(const lerpvert &l) const { return tc == l.tc;; } + bool operator!=(const lerpvert &l) const { return tc != l.tc; } +}; + +struct lerpbounds +{ + const lerpvert *min; + const lerpvert *max; + float u, ustep; + vec normal, nstep; + int winding; +}; + +extern void calcnormals(bool lerptjoints = false); +extern void clearnormals(); +extern void findnormal(const vec &key, const vec &surface, vec &v); +extern void calclerpverts(const vec2 *c, const vec *n, lerpvert *lv, int &numv); +extern void initlerpbounds(float u, float v, const lerpvert *lv, int numv, lerpbounds &start, lerpbounds &end); +extern void lerpnormal(float u, float v, const lerpvert *lv, int numv, lerpbounds &start, lerpbounds &end, vec &normal, vec &nstep); + +#define CHECK_CALCLIGHT_PROGRESS_LOCKED(exit, show_calclight_progress, before, after) \ + if(check_calclight_progress) \ + { \ + if(!calclight_canceled) \ + { \ + before; \ + show_calclight_progress(); \ + check_calclight_canceled(); \ + after; \ + } \ + if(calclight_canceled) { exit; } \ + } +#define CHECK_CALCLIGHT_PROGRESS(exit, show_calclight_progress) CHECK_CALCLIGHT_PROGRESS_LOCKED(exit, show_calclight_progress, , ) + +extern bool calclight_canceled; +extern volatile bool check_calclight_progress; + +extern void check_calclight_canceled(); + +extern int lightmapping; + diff --git a/src/engine/lightning.h b/src/engine/lightning.h new file mode 100644 index 0000000..bc6e21c --- /dev/null +++ b/src/engine/lightning.h @@ -0,0 +1,123 @@ +#define MAXLIGHTNINGSTEPS 64 +#define LIGHTNINGSTEP 8 +int lnjitterx[2][MAXLIGHTNINGSTEPS], lnjittery[2][MAXLIGHTNINGSTEPS]; +int lnjitterframe = 0, lastlnjitter = 0; + +VAR(lnjittermillis, 0, 100, 1000); +VAR(lnjitterradius, 0, 4, 100); +FVAR(lnjitterscale, 0, 0.5f, 10); +VAR(lnscrollmillis, 1, 300, 5000); +FVAR(lnscrollscale, 0, 0.125f, 10); +FVAR(lnblendpower, 0, 0.25f, 1000); + +static void calclightningjitter(int frame) +{ + loopi(MAXLIGHTNINGSTEPS) + { + lnjitterx[lnjitterframe][i] = -lnjitterradius + rnd(2*lnjitterradius + 1); + lnjittery[lnjitterframe][i] = -lnjitterradius + rnd(2*lnjitterradius + 1); + } +} + +static void setuplightning() +{ + if(!lastlnjitter || lastmillis-lastlnjitter > lnjittermillis) + { + if(!lastlnjitter) calclightningjitter(lnjitterframe); + lastlnjitter = lastmillis - (lastmillis%lnjittermillis); + calclightningjitter(lnjitterframe ^= 1); + } +} + +static void renderlightning(Texture *tex, const vec &o, const vec &d, float sz) +{ + vec step(d); + step.sub(o); + float len = step.magnitude(); + int numsteps = clamp(int(ceil(len/LIGHTNINGSTEP)), 2, MAXLIGHTNINGSTEPS); + step.div(numsteps+1); + int jitteroffset = detrnd(int(d.x+d.y+d.z), MAXLIGHTNINGSTEPS); + vec cur(o), up, right; + up.orthogonal(step); + up.normalize(); + right.cross(up, step); + right.normalize(); + float scroll = -float(lastmillis%lnscrollmillis)/lnscrollmillis, + scrollscale = lnscrollscale*(LIGHTNINGSTEP*tex->ys)/(sz*tex->xs), + blend = pow(clamp(float(lastmillis - lastlnjitter)/lnjittermillis, 0.0f, 1.0f), lnblendpower), + jitter0 = (1-blend)*lnjitterscale*sz/lnjitterradius, jitter1 = blend*lnjitterscale*sz/lnjitterradius; + gle::begin(GL_TRIANGLE_STRIP); + loopj(numsteps) + { + vec next(cur); + next.add(step); + if(j+1==numsteps) next = d; + else + { + int lj = (j+jitteroffset)%MAXLIGHTNINGSTEPS; + next.add(vec(right).mul((jitter1*lnjitterx[lnjitterframe][lj] + jitter0*lnjitterx[lnjitterframe^1][lj]))); + next.add(vec(up).mul((jitter1*lnjittery[lnjitterframe][lj] + jitter0*lnjittery[lnjitterframe^1][lj]))); + } + vec dir1 = next, dir2 = next, across; + dir1.sub(cur); + dir2.sub(camera1->o); + across.cross(dir2, dir1).normalize().mul(sz); + gle::attribf(cur.x-across.x, cur.y-across.y, cur.z-across.z); + gle::attribf(scroll, 1); + gle::attribf(cur.x+across.x, cur.y+across.y, cur.z+across.z); + gle::attribf(scroll, 0); + scroll += scrollscale; + if(j+1==numsteps) + { + gle::attribf(next.x-across.x, next.y-across.y, next.z-across.z); + gle::attribf(scroll, 1); + gle::attribf(next.x+across.x, next.y+across.y, next.z+across.z); + gle::attribf(scroll, 0); + } + cur = next; + } + gle::end(); +} + +struct lightningrenderer : listrenderer +{ + lightningrenderer() + : listrenderer("packages/particles/lightning.jpg", 2, PT_LIGHTNING|PT_TRACK|PT_GLARE) + {} + + void startrender() + { + glDisable(GL_CULL_FACE); + gle::defattrib(gle::ATTRIB_VERTEX, 3, GL_FLOAT); + gle::defattrib(gle::ATTRIB_TEXCOORD0, 2, GL_FLOAT); + } + + void endrender() + { + glEnable(GL_CULL_FACE); + } + + void update() + { + setuplightning(); + } + + void seedemitter(particleemitter &pe, const vec &o, const vec &d, int fade, float size, int gravity) + { + pe.maxfade = max(pe.maxfade, fade); + pe.extendbb(o, size); + pe.extendbb(d, size); + } + + void renderpart(listparticle *p, const vec &o, const vec &d, int blend, int ts) + { + blend = min(blend<<2, 255); + if(type&PT_MOD) //multiply alpha into color + gle::colorub((p->color.r*blend)>>8, (p->color.g*blend)>>8, (p->color.b*blend)>>8); + else + gle::color(p->color, blend); + renderlightning(tex, o, d, p->size); + } +}; +static lightningrenderer lightnings; + diff --git a/src/engine/main.cpp b/src/engine/main.cpp new file mode 100644 index 0000000..f522479 --- /dev/null +++ b/src/engine/main.cpp @@ -0,0 +1,1422 @@ +// main.cpp: initialisation & main loop + +#include "engine.h" + +#ifdef SDL_VIDEO_DRIVER_X11 +#include "SDL_syswm.h" +#endif + +extern void cleargamma(); + +void cleanup() +{ + recorder::stop(); + cleanupserver(); + SDL_ShowCursor(SDL_TRUE); + SDL_SetRelativeMouseMode(SDL_FALSE); + if(screen) SDL_SetWindowGrab(screen, SDL_FALSE); + cleargamma(); + freeocta(worldroot); + extern void clear_command(); clear_command(); + extern void clear_console(); clear_console(); + extern void clear_mdls(); clear_mdls(); + extern void clear_sound(); clear_sound(); + closelogfile(); + #ifdef __APPLE__ + if(screen) SDL_SetWindowFullscreen(screen, 0); + #endif + SDL_Quit(); +} + +extern void writeinitcfg(); + +void quit() // normal exit +{ + writeinitcfg(); + writeservercfg(); + abortconnect(); + disconnect(); + localdisconnect(); + writecfg(); + cleanup(); + exit(EXIT_SUCCESS); +} + +void fatal(const char *s, ...) // failure exit +{ + static int errors = 0; + errors++; + + if(errors <= 2) // print up to one extra recursive error + { + defvformatstring(msg,s,s); + logoutf("%s", msg); + + if(errors <= 1) // avoid recursion + { + if(SDL_WasInit(SDL_INIT_VIDEO)) + { + SDL_ShowCursor(SDL_TRUE); + SDL_SetRelativeMouseMode(SDL_FALSE); + if(screen) SDL_SetWindowGrab(screen, SDL_FALSE); + cleargamma(); + #ifdef __APPLE__ + if(screen) SDL_SetWindowFullscreen(screen, 0); + #endif + } + SDL_Quit(); + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Cube 2: Sauerbraten fatal error", msg, NULL); + } + } + + exit(EXIT_FAILURE); +} + +int curtime = 0, lastmillis = 1, elapsedtime = 0, totalmillis = 1; + +dynent *player = NULL; + +int initing = NOT_INITING; + +bool initwarning(const char *desc, int level, int type) +{ + if(initing < level) + { + addchange(desc, type); + return true; + } + return false; +} + +VAR(desktopw, 1, 0, 0); +VAR(desktoph, 1, 0, 0); +int screenw = 0, screenh = 0; +SDL_Window *screen = NULL; +SDL_GLContext glcontext = NULL; + +#define SCR_MINW 320 +#define SCR_MINH 200 +#define SCR_MAXW 10000 +#define SCR_MAXH 10000 +#define SCR_DEFAULTW 1024 +#define SCR_DEFAULTH 768 +VARF(scr_w, SCR_MINW, -1, SCR_MAXW, initwarning("screen resolution")); +VARF(scr_h, SCR_MINH, -1, SCR_MAXH, initwarning("screen resolution")); +VARF(depthbits, 0, 0, 32, initwarning("depth-buffer precision")); +VARF(fsaa, -1, -1, 16, initwarning("anti-aliasing")); + +void writeinitcfg() +{ + stream *f = openutf8file("init.cfg", "w"); + if(!f) return; + f->printf("// automatically written on exit, DO NOT MODIFY\n// modify settings in game\n"); + extern int fullscreen, fullscreendesktop; + f->printf("fullscreen %d\n", fullscreen); + f->printf("fullscreendesktop %d\n", fullscreendesktop); + f->printf("scr_w %d\n", scr_w); + f->printf("scr_h %d\n", scr_h); + f->printf("depthbits %d\n", depthbits); + f->printf("fsaa %d\n", fsaa); + extern int usesound, soundchans, soundfreq, soundbufferlen; + extern char *audiodriver; + f->printf("usesound %d\n", usesound); + f->printf("soundchans %d\n", soundchans); + f->printf("soundfreq %d\n", soundfreq); + f->printf("soundbufferlen %d\n", soundbufferlen); + if(audiodriver[0]) f->printf("audiodriver %s\n", escapestring(audiodriver)); + delete f; +} + +COMMAND(quit, ""); + +static void getbackgroundres(int &w, int &h) +{ + float wk = 1, hk = 1; + if(w < 1024) wk = 1024.0f/w; + if(h < 768) hk = 768.0f/h; + wk = hk = max(wk, hk); + w = int(ceil(w*wk)); + h = int(ceil(h*hk)); +} + +string backgroundcaption = ""; +Texture *backgroundmapshot = NULL; +string backgroundmapname = ""; +char *backgroundmapinfo = NULL; + +void setbackgroundinfo(const char *caption = NULL, Texture *mapshot = NULL, const char *mapname = NULL, const char *mapinfo = NULL) +{ + renderedframe = false; + copystring(backgroundcaption, caption ? caption : ""); + backgroundmapshot = mapshot; + copystring(backgroundmapname, mapname ? mapname : ""); + if(mapinfo != backgroundmapinfo) + { + DELETEA(backgroundmapinfo); + if(mapinfo) backgroundmapinfo = newstring(mapinfo); + } +} + +void restorebackground(bool force = false) +{ + if(renderedframe) + { + if(!force) return; + setbackgroundinfo(); + } + renderbackground(backgroundcaption[0] ? backgroundcaption : NULL, backgroundmapshot, backgroundmapname[0] ? backgroundmapname : NULL, backgroundmapinfo, true); +} + +void bgquad(float x, float y, float w, float h, float tx = 0, float ty = 0, float tw = 1, float th = 1) +{ + gle::begin(GL_TRIANGLE_STRIP); + gle::attribf(x, y); gle::attribf(tx, ty); + gle::attribf(x+w, y); gle::attribf(tx + tw, ty); + gle::attribf(x, y+h); gle::attribf(tx, ty + th); + gle::attribf(x+w, y+h); gle::attribf(tx + tw, ty + th); + gle::end(); +} + +#include + +static bool file_does_indeed_exist(const char *name) { + struct stat buffer; + return !stat(name, & buffer); +} + +void renderbackground(const char *caption, Texture *mapshot, const char *mapname, const char *mapinfo, bool restore, bool force) +{ + if(!inbetweenframes && !force) return; + + if(!restore || force) stopsounds(); // stop sounds while loading + + int w = screenw, h = screenh; + if(forceaspect) w = int(ceil(h*forceaspect)); + getbackgroundres(w, h); + gettextres(w, h); + + static int lastupdate = -1, lastw = -1, lasth = -1; + static float backgroundu = 0, backgroundv = 0, detailu = 0, detailv = 0; + static int numdecals = 0; + static struct decal { float x, y, size; int side; } decals[12]; + if((renderedframe && !mainmenu && lastupdate != lastmillis) || lastw != w || lasth != h) + { + lastupdate = lastmillis; + lastw = w; + lasth = h; + + backgroundu = rndscale(1); + backgroundv = rndscale(1); + detailu = rndscale(1); + detailv = rndscale(1); + numdecals = sizeof(decals)/sizeof(decals[0]); + numdecals = numdecals/3 + rnd((numdecals*2)/3 + 1); + float maxsize = min(w, h)/16.0f; + loopi(numdecals) + { + decal d = { rndscale(w), rndscale(h), maxsize/2 + rndscale(maxsize/2), rnd(2) }; + decals[i] = d; + } + } + else if(lastupdate != lastmillis) lastupdate = lastmillis; + + //~if (mapname) { + //~defformatstring(backpath, "background/%s.png", mapname); + //~if (file_does_indeed_exist(backpath)) { + //~settexture(backpath, 0); + //~bgquad(0, 0, 1920, 1080, 0, 0, 1920, 1080); + //~gle::begin(GL_TRIANGLE_STRIP); + //~gle::attribf(0.0f, 0.0f); gle::attribf(0.0f, 0.0f); + //~gle::attribf(1.0f, 0.0f); gle::attribf(1920.0f, 0.0f); + //~gle::attribf(0.0f, 1.0f); gle::attribf(0.0f, 1.0f); + //~gle::attribf(1.0f, 1.0f); gle::attribf(1920.0f, 1.0f); + //~gle::end(); + //~} + //~} else { + loopi(restore ? 1 : 3) + { + hudmatrix.ortho(0, w, h, 0, -1, 1); + resethudmatrix(); + + hudshader->set(); + gle::colorf(1, 1, 1); + + gle::defvertex(2); + gle::deftexcoord0(); + + //~defformatstring(backpath, "background/%s.png", mapname); + //~if (file_does_indeed_exist(backpath)) { + //~settexture(backpath, 0); + settexture("background/daemex.png", 0); + bgquad(0, 0, screenw, screenh, 0, 0, 1, 1); + //~settexture("data/background.png", 0); + //~float bu = w*0.67f/256.0f + backgroundu, bv = h*0.67f/256.0f + backgroundv; + //~bgquad(0, 0, w, h, 0, 0, bu, bv); + //~glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glEnable(GL_BLEND); + //~settexture("data/background_detail.png", 0); + //~float du = w*0.8f/512.0f + detailu, dv = h*0.8f/512.0f + detailv; + //~bgquad(0, 0, w, h, 0, 0, du, dv); + //~settexture("data/background_decal.png", 3); + //~gle::begin(GL_QUADS); + //~loopj(numdecals) + //~{ + //~float hsz = decals[j].size, hx = clamp(decals[j].x, hsz, w-hsz), hy = clamp(decals[j].y, hsz, h-hsz), side = decals[j].side; + //~gle::attribf(hx-hsz, hy-hsz); gle::attribf(side, 0); + //~gle::attribf(hx+hsz, hy-hsz); gle::attribf(1-side, 0); + //~gle::attribf(hx+hsz, hy+hsz); gle::attribf(1-side, 1); + //~gle::attribf(hx-hsz, hy+hsz); gle::attribf(side, 1); + //~} + //~gle::end(); + float lh = 0.5f*min(w, h), lw = lh*2, + lx = 0.5f*(w - lw), ly = 0.5f*(h*0.5f - lh); + settexture((maxtexsize ? min(maxtexsize, hwtexsize) : hwtexsize) >= 1024 && (screenw > 1280 || screenh > 800) ? "data/logo_1024.png" : "data/logo.png", 3); + bgquad(lx, ly, lw, lh); + if(caption) + { + int tw = text_width(caption); + float tsz = 0.04f*min(w, h)/FONTH, + tx = 0.5f*(w - tw*tsz), ty = h - 0.075f*1.5f*min(w, h) - 1.25f*FONTH*tsz; + pushhudmatrix(); + hudmatrix.translate(tx, ty, 0); + hudmatrix.scale(tsz, tsz, 1); + flushhudmatrix(); + draw_text(caption, 0, 0); + pophudmatrix(); + } + if(mapshot || mapname) + { + int infowidth = 12*FONTH; + float sz = 0.35f*min(w, h), msz = (0.75f*min(w, h) - sz)/(infowidth + FONTH), x = 0.5f*(w-sz), y = ly+lh - sz/15; + if(mapinfo) + { + int mw, mh; + text_bounds(mapinfo, mw, mh, infowidth); + x -= 0.5f*(mw*msz + FONTH*msz); + } + if(mapshot && mapshot!=notexture) + { + glBindTexture(GL_TEXTURE_2D, mapshot->id); + bgquad(x, y, sz, sz); + } + else + { + int qw, qh; + text_bounds("?", qw, qh); + float qsz = sz*0.5f/max(qw, qh); + pushhudmatrix(); + hudmatrix.translate(x + 0.5f*(sz - qw*qsz), y + 0.5f*(sz - qh*qsz), 0); + hudmatrix.scale(qsz, qsz, 1); + flushhudmatrix(); + draw_text("?", 0, 0); + pophudmatrix(); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + } + settexture("data/mapshot_frame.png", 3); + bgquad(x, y, sz, sz); + if(mapname) + { + int tw = text_width(mapname); + float tsz = sz/(8*FONTH), + tx = 0.9f*sz - tw*tsz, ty = 0.9f*sz - FONTH*tsz; + if(tx < 0.1f*sz) { tsz = 0.1f*sz/tw; tx = 0.1f; } + pushhudmatrix(); + hudmatrix.translate(x+tx, y+ty, 0); + hudmatrix.scale(tsz, tsz, 1); + flushhudmatrix(); + draw_text(mapname, 0, 0); + pophudmatrix(); + } + if(mapinfo) + { + pushhudmatrix(); + hudmatrix.translate(x+sz+FONTH*msz, y, 0); + hudmatrix.scale(msz, msz, 1); + flushhudmatrix(); + draw_text(mapinfo, 0, 0, 0xFF, 0xFF, 0xFF, 0xFF, -1, infowidth); + pophudmatrix(); + } + } + glDisable(GL_BLEND); + if(!restore) swapbuffers(false); + } + //~} + + if(!restore) setbackgroundinfo(caption, mapshot, mapname, mapinfo); +} + +VAR(progressbackground, 0, 0, 1); + +float loadprogress = 0; + +void renderprogress(float bar, const char *text, GLuint tex, bool background) // also used during loading +{ + if(!inbetweenframes || drawtex) return; + + extern int menufps, maxfps; + int fps = menufps ? (maxfps ? min(maxfps, menufps) : menufps) : maxfps; + if(fps) + { + static int lastprogress = 0; + int ticks = SDL_GetTicks(), diff = ticks - lastprogress; + if(bar > 0 && diff >= 0 && diff < (1000 + fps-1)/fps) return; + lastprogress = ticks; + } + + clientkeepalive(); // make sure our connection doesn't time out while loading maps etc. + + SDL_PumpEvents(); // keep the event queue awake to avoid 'beachball' cursor + + extern int mesa_swap_bug, curvsync; + bool forcebackground = progressbackground || (mesa_swap_bug && (curvsync || totalmillis==1)); + if(background || forcebackground) restorebackground(forcebackground); + + int w = screenw, h = screenh; + if(forceaspect) w = int(ceil(h*forceaspect)); + getbackgroundres(w, h); + gettextres(w, h); + + hudmatrix.ortho(0, w, h, 0, -1, 1); + resethudmatrix(); + + hudshader->set(); + gle::colorf(1, 1, 1); + + gle::defvertex(2); + gle::deftexcoord0(); + + float fh = 0.075f*min(w, h), fw = fh*10, + fx = renderedframe ? w - fw - fh/4 : 0.5f*(w - fw), + fy = renderedframe ? fh/4 : h - fh*1.5f, + fu1 = 0/512.0f, fu2 = 511/512.0f, + fv1 = 0/64.0f, fv2 = 52/64.0f; + settexture("data/loading_frame.png", 3); + bgquad(fx, fy, fw, fh, fu1, fv1, fu2-fu1, fv2-fv1); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + float bw = fw*(511 - 2*17)/511.0f, bh = fh*20/52.0f, + bx = fx + fw*17/511.0f, by = fy + fh*16/52.0f, + bv1 = 0/32.0f, bv2 = 20/32.0f, + su1 = 0/32.0f, su2 = 7/32.0f, sw = fw*7/511.0f, + eu1 = 23/32.0f, eu2 = 30/32.0f, ew = fw*7/511.0f, + mw = bw - sw - ew, + ex = bx+sw + max(mw*bar, fw*7/511.0f); + if(bar > 0) + { + settexture("data/loading_bar.png", 3); + gle::begin(GL_QUADS); + gle::attribf(bx, by); gle::attribf(su1, bv1); + gle::attribf(bx+sw, by); gle::attribf(su2, bv1); + gle::attribf(bx+sw, by+bh); gle::attribf(su2, bv2); + gle::attribf(bx, by+bh); gle::attribf(su1, bv2); + + gle::attribf(bx+sw, by); gle::attribf(su2, bv1); + gle::attribf(ex, by); gle::attribf(eu1, bv1); + gle::attribf(ex, by+bh); gle::attribf(eu1, bv2); + gle::attribf(bx+sw, by+bh); gle::attribf(su2, bv2); + + gle::attribf(ex, by); gle::attribf(eu1, bv1); + gle::attribf(ex+ew, by); gle::attribf(eu2, bv1); + gle::attribf(ex+ew, by+bh); gle::attribf(eu2, bv2); + gle::attribf(ex, by+bh); gle::attribf(eu1, bv2); + gle::end(); + } + + if(text) + { + int tw = text_width(text); + float tsz = bh*0.8f/FONTH; + if(tw*tsz > mw) tsz = mw/tw; + pushhudmatrix(); + hudmatrix.translate(bx+sw, by + (bh - FONTH*tsz)/2, 0); + hudmatrix.scale(tsz, tsz, 1); + flushhudmatrix(); + draw_text(text, 0, 0); + pophudmatrix(); + } + + glDisable(GL_BLEND); + + if(tex) + { + glBindTexture(GL_TEXTURE_2D, tex); + float sz = 0.35f*min(w, h), x = 0.5f*(w-sz), y = 0.5f*min(w, h) - sz/15; + bgquad(x, y, sz, sz); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + settexture("data/mapshot_frame.png", 3); + bgquad(x, y, sz, sz); + glDisable(GL_BLEND); + } + + swapbuffers(false); +} + +int keyrepeatmask = 0, textinputmask = 0; +Uint32 textinputtime = 0; +VAR(textinputfilter, 0, 5, 1000); + +void keyrepeat(bool on, int mask) +{ + if(on) keyrepeatmask |= mask; + else keyrepeatmask &= ~mask; +} + +void textinput(bool on, int mask) +{ + if(on) + { + if(!textinputmask) + { + SDL_StartTextInput(); + textinputtime = SDL_GetTicks(); + } + textinputmask |= mask; + } + else if(textinputmask) + { + textinputmask &= ~mask; + if(!textinputmask) SDL_StopTextInput(); + } +} + +#ifdef WIN32 +// SDL_WarpMouseInWindow behaves erratically on Windows, so force relative mouse instead. +VARN(relativemouse, userelativemouse, 1, 1, 0); +#else +VARNP(relativemouse, userelativemouse, 0, 1, 1); +#endif + +bool shouldgrab = false, grabinput = false, minimized = false, canrelativemouse = true, relativemouse = false; + +#ifdef SDL_VIDEO_DRIVER_X11 +VAR(sdl_xgrab_bug, 0, 0, 1); +#endif + +void inputgrab(bool on, bool delay = false) +{ +#ifdef SDL_VIDEO_DRIVER_X11 + bool wasrelativemouse = relativemouse; +#endif + if(on) + { + SDL_ShowCursor(SDL_FALSE); + if(canrelativemouse && userelativemouse) + { + if(SDL_SetRelativeMouseMode(SDL_TRUE) >= 0) + { + SDL_SetWindowGrab(screen, SDL_TRUE); + relativemouse = true; + } + else + { + SDL_SetWindowGrab(screen, SDL_FALSE); + canrelativemouse = false; + relativemouse = false; + } + } + } + else + { + SDL_ShowCursor(SDL_TRUE); + if(relativemouse) + { + SDL_SetWindowGrab(screen, SDL_FALSE); + SDL_SetRelativeMouseMode(SDL_FALSE); + relativemouse = false; + } + } + shouldgrab = delay; + +#ifdef SDL_VIDEO_DRIVER_X11 + if((relativemouse || wasrelativemouse) && sdl_xgrab_bug) + { + // Workaround for buggy SDL X11 pointer grabbing + union { SDL_SysWMinfo info; uchar buf[sizeof(SDL_SysWMinfo) + 128]; }; + SDL_GetVersion(&info.version); + if(SDL_GetWindowWMInfo(screen, &info) && info.subsystem == SDL_SYSWM_X11) + { + if(relativemouse) + { + uint mask = ButtonPressMask | ButtonReleaseMask | PointerMotionMask | FocusChangeMask; + XGrabPointer(info.info.x11.display, info.info.x11.window, True, mask, GrabModeAsync, GrabModeAsync, info.info.x11.window, None, CurrentTime); + } + else XUngrabPointer(info.info.x11.display, CurrentTime); + } + } +#endif +} + +bool initwindowpos = false; + +void setfullscreen(bool enable) +{ + if(!screen) return; + //initwarning(enable ? "fullscreen" : "windowed"); + extern int fullscreendesktop; + SDL_SetWindowFullscreen(screen, enable ? (fullscreendesktop ? SDL_WINDOW_FULLSCREEN_DESKTOP : SDL_WINDOW_FULLSCREEN) : 0); + if(!enable) + { + SDL_SetWindowSize(screen, scr_w, scr_h); + if(initwindowpos) + { + int winx = SDL_WINDOWPOS_CENTERED, winy = SDL_WINDOWPOS_CENTERED; + SDL_SetWindowPosition(screen, winx, winy); + initwindowpos = false; + } + } +} + +#ifdef _DEBUG +VARF(fullscreen, 0, 0, 1, setfullscreen(fullscreen!=0)); +#else +VARF(fullscreen, 0, 1, 1, setfullscreen(fullscreen!=0)); +#endif + +void resetfullscreen() +{ + setfullscreen(false); + setfullscreen(true); +} + +VARF(fullscreendesktop, 0, 0, 1, if(fullscreen) resetfullscreen()); + +void screenres(int w, int h) +{ + scr_w = clamp(w, SCR_MINW, SCR_MAXW); + scr_h = clamp(h, SCR_MINH, SCR_MAXH); + if(screen) + { + if(fullscreendesktop) + { + scr_w = min(scr_w, desktopw); + scr_h = min(scr_h, desktoph); + } + if(SDL_GetWindowFlags(screen) & SDL_WINDOW_FULLSCREEN) + { + if(fullscreendesktop) gl_resize(); + else resetfullscreen(); + initwindowpos = true; + } + else + { + SDL_SetWindowSize(screen, scr_w, scr_h); + SDL_SetWindowPosition(screen, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED); + initwindowpos = false; + } + } + else + { + initwarning("screen resolution"); + } +} + +ICOMMAND(screenres, "ii", (int *w, int *h), screenres(*w, *h)); + +static void setgamma(int val) +{ + if(screen && SDL_SetWindowBrightness(screen, val/100.0f) < 0) conoutf(CON_ERROR, "Could not set gamma: %s", SDL_GetError()); +} + +static int curgamma = 100; +VARFNP(gamma, reqgamma, 30, 100, 300, +{ + if(initing || reqgamma == curgamma) return; + curgamma = reqgamma; + setgamma(curgamma); +}); + +void restoregamma() +{ + if(initing || reqgamma == 100) return; + curgamma = reqgamma; + setgamma(curgamma); +} + +void cleargamma() +{ + if(curgamma != 100 && screen) SDL_SetWindowBrightness(screen, 1.0f); +} + +int curvsync = -1; +void restorevsync() +{ + if(initing || !glcontext) return; + extern int vsync, vsynctear; + if(!SDL_GL_SetSwapInterval(vsync ? (vsynctear ? -1 : 1) : 0)) + curvsync = vsync; +} + +VARFP(vsync, 0, 0, 1, restorevsync()); +VARFP(vsynctear, 0, 0, 1, { if(vsync) restorevsync(); }); + +void setupscreen() +{ + if(glcontext) + { + SDL_GL_DeleteContext(glcontext); + glcontext = NULL; + } + if(screen) + { + SDL_DestroyWindow(screen); + screen = NULL; + } + curvsync = -1; + + SDL_Rect desktop; + if(SDL_GetDisplayBounds(0, &desktop) < 0) fatal("failed querying desktop bounds: %s", SDL_GetError()); + desktopw = desktop.w; + desktoph = desktop.h; + + if(scr_h < 0) scr_h = fullscreen ? desktoph : SCR_DEFAULTH; + if(scr_w < 0) scr_w = (scr_h*desktopw)/desktoph; + scr_w = clamp(scr_w, SCR_MINW, SCR_MAXW); + scr_h = clamp(scr_h, SCR_MINH, SCR_MAXH); + if(fullscreendesktop) + { + scr_w = min(scr_w, desktopw); + scr_h = min(scr_h, desktoph); + } + + int winx = SDL_WINDOWPOS_UNDEFINED, winy = SDL_WINDOWPOS_UNDEFINED, winw = scr_w, winh = scr_h, flags = SDL_WINDOW_RESIZABLE; + if(fullscreen) + { + if(fullscreendesktop) + { + winw = desktopw; + winh = desktoph; + flags |= SDL_WINDOW_FULLSCREEN_DESKTOP; + } + else flags |= SDL_WINDOW_FULLSCREEN; + initwindowpos = true; + } + + SDL_GL_ResetAttributes(); + SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); + #if !defined(WIN32) && !defined(__APPLE__) + SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8); + #endif + static const int configs[] = + { + 0x3, /* try everything */ + 0x2, 0x1, /* try disabling one at a time */ + 0 /* try disabling everything */ + }; + int config = 0; + if(!depthbits) SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); + if(!fsaa) + { + SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 0); + SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 0); + } + loopi(sizeof(configs)/sizeof(configs[0])) + { + config = configs[i]; + if(!depthbits && config&1) continue; + if(fsaa<=0 && config&2) continue; + if(depthbits) SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, config&1 ? depthbits : 24); + if(fsaa>0) + { + SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, config&2 ? 1 : 0); + SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, config&2 ? fsaa : 0); + } + screen = SDL_CreateWindow("Cube 2: Sauerbraten", winx, winy, winw, winh, SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN | SDL_WINDOW_INPUT_FOCUS | SDL_WINDOW_MOUSE_FOCUS | flags); + if(!screen) continue; + + #ifdef __APPLE__ + static const int glversions[] = { 32, 20 }; + #else + static const int glversions[] = { 33, 32, 31, 30, 20 }; + #endif + loopj(sizeof(glversions)/sizeof(glversions[0])) + { + glcompat = glversions[j] <= 30 ? 1 : 0; + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, glversions[j] / 10); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, glversions[j] % 10); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, glversions[j] >= 32 ? SDL_GL_CONTEXT_PROFILE_CORE : 0); + glcontext = SDL_GL_CreateContext(screen); + if(glcontext) break; + } + if(glcontext) break; + } + if(!screen) fatal("failed to create OpenGL window: %s", SDL_GetError()); + else if(!glcontext) fatal("failed to create OpenGL context: %s", SDL_GetError()); + else + { + if(depthbits && (config&1)==0) conoutf(CON_WARN, "%d bit z-buffer not supported - disabling", depthbits); + if(fsaa>0 && (config&2)==0) conoutf(CON_WARN, "%dx anti-aliasing not supported - disabling", fsaa); + } + + SDL_SetWindowMinimumSize(screen, SCR_MINW, SCR_MINH); + SDL_SetWindowMaximumSize(screen, SCR_MAXW, SCR_MAXH); + + SDL_GetWindowSize(screen, &screenw, &screenh); +} + +void resetgl() +{ + clearchanges(CHANGE_GFX); + + renderbackground("resetting OpenGL"); + + extern void cleanupva(); + extern void cleanupparticles(); + extern void cleanupdecals(); + extern void cleanupblobs(); + extern void cleanupsky(); + extern void cleanupmodels(); + extern void cleanupprefabs(); + extern void cleanuplightmaps(); + extern void cleanupblendmap(); + extern void cleanshadowmap(); + extern void cleanreflections(); + extern void cleanupglare(); + extern void cleanupdepthfx(); + recorder::cleanup(); + cleanupva(); + cleanupparticles(); + cleanupdecals(); + cleanupblobs(); + cleanupsky(); + cleanupmodels(); + cleanupprefabs(); + cleanuptextures(); + cleanuplightmaps(); + cleanupblendmap(); + cleanshadowmap(); + cleanreflections(); + cleanupglare(); + cleanupdepthfx(); + cleanupshaders(); + cleanupgl(); + + setupscreen(); + inputgrab(grabinput); + gl_init(); + + inbetweenframes = false; + if(!reloadtexture(*notexture) || + !reloadtexture("data/logo.png") || + !reloadtexture("data/logo_1024.png") || + !reloadtexture("data/background.png") || + !reloadtexture("data/background_detail.png") || + !reloadtexture("data/background_decal.png") || + !reloadtexture("data/mapshot_frame.png") || + !reloadtexture("data/loading_frame.png") || + !reloadtexture("data/loading_bar.png")) + fatal("failed to reload core texture"); + reloadfonts(); + inbetweenframes = true; + renderbackground("initializing..."); + restoregamma(); + restorevsync(); + reloadshaders(); + reloadtextures(); + initlights(); + allchanged(true); +} + +COMMAND(resetgl, ""); + +static queue events; + +static inline bool filterevent(const SDL_Event &event) +{ + switch(event.type) + { + case SDL_MOUSEMOTION: + if(grabinput && !relativemouse && !(SDL_GetWindowFlags(screen) & SDL_WINDOW_FULLSCREEN)) + { + if(event.motion.x == screenw / 2 && event.motion.y == screenh / 2) + return false; // ignore any motion events generated by SDL_WarpMouse + #ifdef __APPLE__ + if(event.motion.y == 0) + return false; // let mac users drag windows via the title bar + #endif + } + break; + } + return true; +} + +template static inline bool pumpevents(queue &events) +{ + while(events.empty()) + { + SDL_PumpEvents(); + databuf buf = events.reserve(events.capacity()); + int n = SDL_PeepEvents(buf.getbuf(), buf.remaining(), SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT); + if(n <= 0) return false; + loopi(n) if(filterevent(buf.buf[i])) buf.put(buf.buf[i]); + events.addbuf(buf); + } + return true; +} + +static int interceptkeysym = 0; + +static int interceptevents(void *data, SDL_Event *event) +{ + switch(event->type) + { + case SDL_MOUSEMOTION: return 0; + case SDL_KEYDOWN: + if(event->key.keysym.sym == interceptkeysym) + { + interceptkeysym = -interceptkeysym; + return 0; + } + break; + } + return 1; +} + +static void clearinterceptkey() +{ + SDL_DelEventWatch(interceptevents, NULL); + interceptkeysym = 0; +} + +bool interceptkey(int sym) +{ + if(!interceptkeysym) + { + interceptkeysym = sym; + SDL_FilterEvents(interceptevents, NULL); + if(interceptkeysym < 0) + { + interceptkeysym = 0; + return true; + } + SDL_AddEventWatch(interceptevents, NULL); + } + else if(abs(interceptkeysym) != sym) interceptkeysym = sym; + SDL_PumpEvents(); + if(interceptkeysym < 0) + { + clearinterceptkey(); + interceptkeysym = sym; + SDL_FilterEvents(interceptevents, NULL); + interceptkeysym = 0; + return true; + } + return false; +} + +static void ignoremousemotion() +{ + SDL_PumpEvents(); + SDL_FlushEvent(SDL_MOUSEMOTION); +} + +static void resetmousemotion() +{ + if(grabinput && !relativemouse && !(SDL_GetWindowFlags(screen) & SDL_WINDOW_FULLSCREEN)) + { + SDL_WarpMouseInWindow(screen, screenw / 2, screenh / 2); + } +} + +static void checkmousemotion(int &dx, int &dy) +{ + while(pumpevents(events)) + { + SDL_Event &event = events.removing(); + if(event.type != SDL_MOUSEMOTION) return; + dx += event.motion.xrel; + dy += event.motion.yrel; + events.remove(); + } +} + +void checkinput() +{ + if(interceptkeysym) clearinterceptkey(); + //int lasttype = 0, lastbut = 0; + bool mousemoved = false; + int focused = 0; + while(pumpevents(events)) + { + SDL_Event &event = events.remove(); + + if(focused && event.type!=SDL_WINDOWEVENT) { if(grabinput != (focused>0)) inputgrab(grabinput = focused>0, shouldgrab); focused = 0; } + + switch(event.type) + { + case SDL_QUIT: + quit(); + return; + + case SDL_TEXTINPUT: + if(textinputmask && int(event.text.timestamp-textinputtime) >= textinputfilter) + { + uchar buf[SDL_TEXTINPUTEVENT_TEXT_SIZE+1]; + size_t len = decodeutf8(buf, sizeof(buf)-1, (const uchar *)event.text.text, strlen(event.text.text)); + if(len > 0) { buf[len] = '\0'; processtextinput((const char *)buf, len); } + } + break; + + case SDL_KEYDOWN: + case SDL_KEYUP: + if(keyrepeatmask || !event.key.repeat) + processkey(event.key.keysym.sym, event.key.state==SDL_PRESSED, event.key.keysym.mod | SDL_GetModState()); + break; + + case SDL_WINDOWEVENT: + switch(event.window.event) + { + case SDL_WINDOWEVENT_CLOSE: + quit(); + break; + + case SDL_WINDOWEVENT_FOCUS_GAINED: + shouldgrab = true; + break; + case SDL_WINDOWEVENT_ENTER: + shouldgrab = false; + focused = 1; + break; + + case SDL_WINDOWEVENT_LEAVE: + case SDL_WINDOWEVENT_FOCUS_LOST: + shouldgrab = false; + focused = -1; + break; + + case SDL_WINDOWEVENT_MINIMIZED: + minimized = true; + break; + + case SDL_WINDOWEVENT_MAXIMIZED: + case SDL_WINDOWEVENT_RESTORED: + minimized = false; + break; + + case SDL_WINDOWEVENT_RESIZED: + break; + + case SDL_WINDOWEVENT_SIZE_CHANGED: + { + SDL_GetWindowSize(screen, &screenw, &screenh); + if(!fullscreendesktop || !(SDL_GetWindowFlags(screen) & SDL_WINDOW_FULLSCREEN)) + { + scr_w = clamp(screenw, SCR_MINW, SCR_MAXW); + scr_h = clamp(screenh, SCR_MINH, SCR_MAXH); + } + gl_resize(); + break; + } + } + break; + + case SDL_MOUSEMOTION: + if(grabinput) + { + int dx = event.motion.xrel, dy = event.motion.yrel; + checkmousemotion(dx, dy); + if(!g3d_movecursor(dx, dy)) mousemove(dx, dy); + mousemoved = true; + } + else if(shouldgrab) inputgrab(grabinput = true); + break; + + case SDL_MOUSEBUTTONDOWN: + case SDL_MOUSEBUTTONUP: + //if(lasttype==event.type && lastbut==event.button.button) break; // why?? get event twice without it + switch(event.button.button) + { + case SDL_BUTTON_LEFT: processkey(-1, event.button.state==SDL_PRESSED); break; + case SDL_BUTTON_MIDDLE: processkey(-2, event.button.state==SDL_PRESSED); break; + case SDL_BUTTON_RIGHT: processkey(-3, event.button.state==SDL_PRESSED); break; + case SDL_BUTTON_X1: processkey(-6, event.button.state==SDL_PRESSED); break; + case SDL_BUTTON_X2: processkey(-7, event.button.state==SDL_PRESSED); break; + } + //lasttype = event.type; + //lastbut = event.button.button; + break; + + case SDL_MOUSEWHEEL: + if(event.wheel.y > 0) { processkey(-4, true); processkey(-4, false); } + else if(event.wheel.y < 0) { processkey(-5, true); processkey(-5, false); } + break; + } + } + if(focused) { if(grabinput != (focused>0)) inputgrab(grabinput = focused>0, shouldgrab); focused = 0; } + if(mousemoved) resetmousemotion(); +} + +void swapbuffers(bool overlay) +{ + recorder::capture(overlay); + gle::disable(); + SDL_GL_SwapWindow(screen); +} + +VAR(menufps, 0, 60, 1000); +VARP(maxfps, 0, 200, 1000); + +void limitfps(int &millis, int curmillis) +{ + int limit = (mainmenu || minimized) && menufps ? (maxfps ? min(maxfps, menufps) : menufps) : maxfps; + if(!limit) return; + static int fpserror = 0; + int delay = 1000/limit - (millis-curmillis); + if(delay < 0) fpserror = 0; + else + { + fpserror += 1000%limit; + if(fpserror >= limit) + { + ++delay; + fpserror -= limit; + } + if(delay > 0) + { + SDL_Delay(delay); + millis += delay; + } + } +} + +#if defined(WIN32) && !defined(_DEBUG) && !defined(__GNUC__) +void stackdumper(unsigned int type, EXCEPTION_POINTERS *ep) +{ + if(!ep) fatal("unknown type"); + EXCEPTION_RECORD *er = ep->ExceptionRecord; + CONTEXT *context = ep->ContextRecord; + char out[512]; + formatstring(out, "Cube 2: Sauerbraten Win32 Exception: 0x%x [0x%x]\n\n", er->ExceptionCode, er->ExceptionCode==EXCEPTION_ACCESS_VIOLATION ? er->ExceptionInformation[1] : -1); + SymInitialize(GetCurrentProcess(), NULL, TRUE); +#ifdef _AMD64_ + STACKFRAME64 sf = {{context->Rip, 0, AddrModeFlat}, {}, {context->Rbp, 0, AddrModeFlat}, {context->Rsp, 0, AddrModeFlat}, 0}; + while(::StackWalk64(IMAGE_FILE_MACHINE_AMD64, GetCurrentProcess(), GetCurrentThread(), &sf, context, NULL, ::SymFunctionTableAccess, ::SymGetModuleBase, NULL)) + { + union { IMAGEHLP_SYMBOL64 sym; char symext[sizeof(IMAGEHLP_SYMBOL64) + sizeof(string)]; }; + sym.SizeOfStruct = sizeof(sym); + sym.MaxNameLength = sizeof(symext) - sizeof(sym); + IMAGEHLP_LINE64 line; + line.SizeOfStruct = sizeof(line); + DWORD64 symoff; + DWORD lineoff; + if(SymGetSymFromAddr64(GetCurrentProcess(), sf.AddrPC.Offset, &symoff, &sym) && SymGetLineFromAddr64(GetCurrentProcess(), sf.AddrPC.Offset, &lineoff, &line)) +#else + STACKFRAME sf = {{context->Eip, 0, AddrModeFlat}, {}, {context->Ebp, 0, AddrModeFlat}, {context->Esp, 0, AddrModeFlat}, 0}; + while(::StackWalk(IMAGE_FILE_MACHINE_I386, GetCurrentProcess(), GetCurrentThread(), &sf, context, NULL, ::SymFunctionTableAccess, ::SymGetModuleBase, NULL)) + { + union { IMAGEHLP_SYMBOL sym; char symext[sizeof(IMAGEHLP_SYMBOL) + sizeof(string)]; }; + sym.SizeOfStruct = sizeof(sym); + sym.MaxNameLength = sizeof(symext) - sizeof(sym); + IMAGEHLP_LINE line; + line.SizeOfStruct = sizeof(line); + DWORD symoff, lineoff; + if(SymGetSymFromAddr(GetCurrentProcess(), sf.AddrPC.Offset, &symoff, &sym) && SymGetLineFromAddr(GetCurrentProcess(), sf.AddrPC.Offset, &lineoff, &line)) +#endif + { + char *del = strrchr(line.FileName, '\\'); + concformatstring(out, "%s - %s [%d]\n", sym.Name, del ? del + 1 : line.FileName, line.LineNumber); + } + } + fatal(out); +} +#endif + +#define MAXFPSHISTORY 60 + +int fpspos = 0, fpshistory[MAXFPSHISTORY]; + +void resetfpshistory() +{ + loopi(MAXFPSHISTORY) fpshistory[i] = 1; + fpspos = 0; +} + +void updatefpshistory(int millis) +{ + fpshistory[fpspos++] = max(1, min(1000, millis)); + if(fpspos>=MAXFPSHISTORY) fpspos = 0; +} + +void getfps(int &fps, int &bestdiff, int &worstdiff) +{ + int total = fpshistory[MAXFPSHISTORY-1], best = total, worst = total; + loopi(MAXFPSHISTORY-1) + { + int millis = fpshistory[i]; + total += millis; + if(millis < best) best = millis; + if(millis > worst) worst = millis; + } + + fps = (1000*MAXFPSHISTORY)/total; + bestdiff = 1000/best-fps; + worstdiff = fps-1000/worst; +} + +void getfps_(int *raw) +{ + int fps, bestdiff, worstdiff; + if(*raw) fps = 1000/fpshistory[(fpspos+MAXFPSHISTORY-1)%MAXFPSHISTORY]; + else getfps(fps, bestdiff, worstdiff); + intret(fps); +} + +COMMANDN(getfps, getfps_, "i"); + +bool inbetweenframes = false, renderedframe = true; + +static bool findarg(int argc, char **argv, const char *str) +{ + for(int i = 1; i0, dedicated>1); // never returns if dedicated + ASSERT(dedicated <= 1); + game::initclient(); + + logoutf("init: video"); + SDL_SetHint(SDL_HINT_GRAB_KEYBOARD, "0"); + #if !defined(WIN32) && !defined(__APPLE__) + SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0"); + #endif + setupscreen(); + SDL_ShowCursor(SDL_FALSE); + SDL_StopTextInput(); // workaround for spurious text-input events getting sent on first text input toggle? + + logoutf("init: gl"); + gl_checkextensions(); + gl_init(); + notexture = textureload("packages/textures/notexture.png"); + if(!notexture) fatal("could not find core textures"); + + logoutf("init: console"); + if(!execfile("data/stdlib.cfg", false)) fatal("cannot find data files (you are running from the wrong folder, try .bat file in the main folder)"); // this is the first file we load. + if(!execfile("data/font.cfg", false)) fatal("cannot find font definitions"); + if(!setfont("default")) fatal("no default font specified"); + + inbetweenframes = true; + renderbackground("initializing..."); + + logoutf("init: world"); + camera1 = player = game::iterdynents(0); + emptymap(0, true, NULL, false); + + logoutf("init: sound"); + initsound(); + + logoutf("init: cfg"); + initing = INIT_LOAD; + execfile("data/keymap.cfg"); + execfile("data/stdedit.cfg"); + execfile("data/sounds.cfg"); + execfile("data/menus.cfg"); + execfile("data/heightmap.cfg"); + execfile("data/blendbrush.cfg"); + defformatstring(gamecfgname, "data/game_%s.cfg", game::gameident()); + execfile(gamecfgname); + if(game::savedservers()) execfile(game::savedservers(), false); + + identflags |= IDF_PERSIST; + + if(!execfile(game::savedconfig(), false)) + { + execfile(game::defaultconfig()); + writecfg(game::restoreconfig()); + } + execfile(game::autoexec(), false); + + identflags &= ~IDF_PERSIST; + + initing = INIT_GAME; + game::loadconfigs(); + + initing = NOT_INITING; + + logoutf("init: render"); + restoregamma(); + restorevsync(); + loadshaders(); + initparticles(); + initdecals(); + + identflags |= IDF_PERSIST; + + logoutf("init: mainloop"); + + if(execfile("once.cfg", false)) remove(findfile("once.cfg", "rb")); + + if(load) + { + logoutf("init: localconnect"); + //localconnect(); + game::changemap(load); + } + + if(initscript) execute(initscript); + + initmumble(); + resetfpshistory(); + + inputgrab(grabinput = true); + ignoremousemotion(); + + for(;;) + { + static int frames = 0; + int millis = getclockmillis(); + limitfps(millis, totalmillis); + elapsedtime = millis - totalmillis; + static int timeerr = 0; + int scaledtime = game::scaletime(elapsedtime) + timeerr; + curtime = scaledtime/100; + timeerr = scaledtime%100; + if(!multiplayer(false) && curtime>200) curtime = 200; + if(game::ispaused()) curtime = 0; + lastmillis += curtime; + totalmillis = millis; + updatetime(); + + checkinput(); + menuprocess(); + tryedit(); + + if(lastmillis) game::updateworld(); + + checksleep(lastmillis); + + serverslice(false, 0); + + if(frames) updatefpshistory(elapsedtime); + frames++; + + // miscellaneous general game effects + recomputecamera(); + updateparticles(); + updatesounds(); + + if(minimized) continue; + + inbetweenframes = false; + if(mainmenu) gl_drawmainmenu(); + else gl_drawframe(); + swapbuffers(); + renderedframe = inbetweenframes = true; + } + + ASSERT(0); + return EXIT_FAILURE; + + #if defined(WIN32) && !defined(_DEBUG) && !defined(__GNUC__) + } __except(stackdumper(0, GetExceptionInformation()), EXCEPTION_CONTINUE_SEARCH) { return 0; } + #endif +} diff --git a/src/engine/master.cpp b/src/engine/master.cpp new file mode 100644 index 0000000..2522e97 --- /dev/null +++ b/src/engine/master.cpp @@ -0,0 +1,718 @@ +#ifdef WIN32 +#define FD_SETSIZE 4096 +#else +#include +#undef __FD_SETSIZE +#define __FD_SETSIZE 4096 +#endif + +#include "cube.h" +#include +#include + +#define INPUT_LIMIT 4096 +#define OUTPUT_LIMIT (64*1024) +#define CLIENT_TIME (3*60*1000) +#define AUTH_TIME (30*1000) +#define AUTH_LIMIT 100 +#define AUTH_THROTTLE 1000 +#define CLIENT_LIMIT 4096 +#define DUP_LIMIT 16 +#define PING_TIME 3000 +#define PING_RETRY 5 +#define KEEPALIVE_TIME (65*60*1000) +#define SERVER_LIMIT 4096 +#define SERVER_DUP_LIMIT 10 + +FILE *logfile = NULL; + +struct userinfo +{ + char *name; + void *pubkey; +}; +hashnameset users; + +void adduser(char *name, char *pubkey) +{ + name = newstring(name); + userinfo &u = users[name]; + u.name = name; + u.pubkey = parsepubkey(pubkey); +} +COMMAND(adduser, "ss"); + +void clearusers() +{ + enumerate(users, userinfo, u, { delete[] u.name; freepubkey(u.pubkey); }); + users.clear(); +} +COMMAND(clearusers, ""); + +vector bans, servbans, gbans; + +void clearbans() +{ + bans.shrink(0); + servbans.shrink(0); + gbans.shrink(0); +} +COMMAND(clearbans, ""); + +void addban(vector &bans, const char *name) +{ + ipmask ban; + ban.parse(name); + bans.add(ban); +} +ICOMMAND(ban, "s", (char *name), addban(bans, name)); +ICOMMAND(servban, "s", (char *name), addban(servbans, name)); +ICOMMAND(gban, "s", (char *name), addban(gbans, name)); + +bool checkban(vector &bans, enet_uint32 host) +{ + loopv(bans) if(bans[i].check(host)) return true; + return false; +} + +struct authreq +{ + enet_uint32 reqtime; + uint id; + void *answer; +}; + +struct gameserver +{ + ENetAddress address; + string ip; + int port, numpings; + enet_uint32 lastping, lastpong; +}; +vector gameservers; + +struct messagebuf +{ + vector &owner; + vector buf; + int refs; + + messagebuf(vector &owner) : owner(owner), refs(0) {} + + const char *getbuf() { return buf.getbuf(); } + int length() { return buf.length(); } + void purge(); + + bool equals(const messagebuf &m) const + { + return buf.length() == m.buf.length() && !memcmp(buf.getbuf(), m.buf.getbuf(), buf.length()); + } + + bool endswith(const messagebuf &m) const + { + return buf.length() >= m.buf.length() && !memcmp(&buf[buf.length() - m.buf.length()], m.buf.getbuf(), m.buf.length()); + } + + void concat(const messagebuf &m) + { + if(buf.length() && buf.last() == '\0') buf.pop(); + buf.put(m.buf.getbuf(), m.buf.length()); + } +}; +vector gameserverlists, gbanlists; +bool updateserverlist = true; + +struct client +{ + ENetAddress address; + ENetSocket socket; + char input[INPUT_LIMIT]; + messagebuf *message; + vector output; + int inputpos, outputpos; + enet_uint32 connecttime, lastinput; + int servport; + enet_uint32 lastauth; + vector authreqs; + bool shouldpurge; + bool registeredserver; + + client() : message(NULL), inputpos(0), outputpos(0), servport(-1), lastauth(0), shouldpurge(false), registeredserver(false) {} +}; +vector clients; + +ENetSocket serversocket = ENET_SOCKET_NULL; + +time_t starttime; +enet_uint32 servtime = 0; + +void fatal(const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + vfprintf(logfile, fmt, args); + fputc('\n', logfile); + va_end(args); + exit(EXIT_FAILURE); +} + +void conoutfv(int type, const char *fmt, va_list args) +{ + vfprintf(logfile, fmt, args); + fputc('\n', logfile); +} + +void purgeclient(int n) +{ + client &c = *clients[n]; + if(c.message) c.message->purge(); + enet_socket_destroy(c.socket); + delete clients[n]; + clients.remove(n); +} + +void output(client &c, const char *msg, int len = 0) +{ + if(!len) len = strlen(msg); + c.output.put(msg, len); +} + +void outputf(client &c, const char *fmt, ...) +{ + string msg; + va_list args; + va_start(args, fmt); + vformatstring(msg, fmt, args); + va_end(args); + + output(c, msg); +} + +ENetSocket pingsocket = ENET_SOCKET_NULL; + +bool setuppingsocket(ENetAddress *address) +{ + if(pingsocket != ENET_SOCKET_NULL) return true; + pingsocket = enet_socket_create(ENET_SOCKET_TYPE_DATAGRAM); + if(pingsocket == ENET_SOCKET_NULL) return false; + if(address && enet_socket_bind(pingsocket, address) < 0) return false; + enet_socket_set_option(pingsocket, ENET_SOCKOPT_NONBLOCK, 1); + return true; +} + +void setupserver(int port, const char *ip = NULL) +{ + ENetAddress address; + address.host = ENET_HOST_ANY; + address.port = port; + + if(ip) + { + if(enet_address_set_host(&address, ip)<0) + fatal("failed to resolve server address: %s", ip); + } + serversocket = enet_socket_create(ENET_SOCKET_TYPE_STREAM); + if(serversocket==ENET_SOCKET_NULL || + enet_socket_set_option(serversocket, ENET_SOCKOPT_REUSEADDR, 1) < 0 || + enet_socket_bind(serversocket, &address) < 0 || + enet_socket_listen(serversocket, -1) < 0) + fatal("failed to create server socket"); + if(enet_socket_set_option(serversocket, ENET_SOCKOPT_NONBLOCK, 1)<0) + fatal("failed to make server socket non-blocking"); + if(!setuppingsocket(&address)) + fatal("failed to create ping socket"); + + enet_time_set(0); + + starttime = time(NULL); + char *ct = ctime(&starttime); + if(strchr(ct, '\n')) *strchr(ct, '\n') = '\0'; + conoutf("*** Starting master server on %s %d at %s ***", ip ? ip : "localhost", port, ct); +} + +void genserverlist() +{ + if(!updateserverlist) return; + while(gameserverlists.length() && gameserverlists.last()->refs<=0) + delete gameserverlists.pop(); + messagebuf *l = new messagebuf(gameserverlists); + loopv(gameservers) + { + gameserver &s = *gameservers[i]; + if(!s.lastpong) continue; + defformatstring(cmd, "addserver %s %d\n", s.ip, s.port); + l->buf.put(cmd, strlen(cmd)); + } + l->buf.add('\0'); + gameserverlists.add(l); + updateserverlist = false; +} + +void gengbanlist() +{ + messagebuf *l = new messagebuf(gbanlists); + const char *header = "cleargbans\n"; + l->buf.put(header, strlen(header)); + string cmd = "addgban "; + int cmdlen = strlen(cmd); + loopv(gbans) + { + ipmask &b = gbans[i]; + l->buf.put(cmd, cmdlen + b.print(&cmd[cmdlen])); + l->buf.add('\n'); + } + if(gbanlists.length() && gbanlists.last()->equals(*l)) + { + delete l; + return; + } + while(gbanlists.length() && gbanlists.last()->refs<=0) + delete gbanlists.pop(); + loopv(gbanlists) + { + messagebuf *m = gbanlists[i]; + if(m->refs > 0 && !m->endswith(*l)) m->concat(*l); + } + gbanlists.add(l); + loopv(clients) + { + client &c = *clients[i]; + if(c.servport >= 0 && !c.message) + { + c.message = l; + c.message->refs++; + } + } +} + +void addgameserver(client &c) +{ + if(gameservers.length() >= SERVER_LIMIT) return; + int dups = 0; + loopv(gameservers) + { + gameserver &s = *gameservers[i]; + if(s.address.host != c.address.host) continue; + ++dups; + if(s.port == c.servport) + { + s.lastping = 0; + s.numpings = 0; + return; + } + } + if(dups >= SERVER_DUP_LIMIT) + { + outputf(c, "failreg too many servers on ip\n"); + return; + } + string hostname; + if(enet_address_get_host_ip(&c.address, hostname, sizeof(hostname)) < 0) + { + outputf(c, "failreg failed resolving ip\n"); + return; + } + gameserver &s = *gameservers.add(new gameserver); + s.address.host = c.address.host; + s.address.port = c.servport+1; + copystring(s.ip, hostname); + s.port = c.servport; + s.numpings = 0; + s.lastping = s.lastpong = 0; +} + +client *findclient(gameserver &s) +{ + loopv(clients) + { + client &c = *clients[i]; + if(s.address.host == c.address.host && s.port == c.servport) + return &c; + } + return NULL; +} + +void servermessage(gameserver &s, const char *msg) +{ + client *c = findclient(s); + if(c) outputf(*c, msg); +} + +void checkserverpongs() +{ + ENetBuffer buf; + ENetAddress addr; + static uchar pong[MAXTRANS]; + for(;;) + { + buf.data = pong; + buf.dataLength = sizeof(pong); + int len = enet_socket_receive(pingsocket, &addr, &buf, 1); + if(len <= 0) break; + loopv(gameservers) + { + gameserver &s = *gameservers[i]; + if(s.address.host == addr.host && s.address.port == addr.port) + { + if(s.lastping && (!s.lastpong || ENET_TIME_GREATER(s.lastping, s.lastpong))) + { + client *c = findclient(s); + if(c) + { + c->registeredserver = true; + outputf(*c, "succreg\n"); + if(!c->message && gbanlists.length()) + { + c->message = gbanlists.last(); + c->message->refs++; + } + } + } + if(!s.lastpong) updateserverlist = true; + s.lastpong = servtime ? servtime : 1; + break; + } + } + } +} + +void bangameservers() +{ + loopvrev(gameservers) if(checkban(servbans, gameservers[i]->address.host)) + { + delete gameservers.remove(i); + updateserverlist = true; + } +} + +void checkgameservers() +{ + ENetBuffer buf; + loopv(gameservers) + { + gameserver &s = *gameservers[i]; + if(s.lastping && s.lastpong && ENET_TIME_LESS_EQUAL(s.lastping, s.lastpong)) + { + if(ENET_TIME_DIFFERENCE(servtime, s.lastpong) > KEEPALIVE_TIME) + { + delete gameservers.remove(i--); + updateserverlist = true; + } + } + else if(!s.lastping || ENET_TIME_DIFFERENCE(servtime, s.lastping) > PING_TIME) + { + if(s.numpings >= PING_RETRY) + { + servermessage(s, "failreg failed pinging server\n"); + delete gameservers.remove(i--); + updateserverlist = true; + } + else + { + static const uchar ping[] = { 1 }; + buf.data = (void *)ping; + buf.dataLength = sizeof(ping); + s.numpings++; + s.lastping = servtime ? servtime : 1; + enet_socket_send(pingsocket, &s.address, &buf, 1); + } + } + } +} + +void messagebuf::purge() +{ + refs = max(refs - 1, 0); + if(refs<=0 && owner.last()!=this) + { + owner.removeobj(this); + delete this; + } +} + +void purgeauths(client &c) +{ + int expired = 0; + loopv(c.authreqs) + { + if(ENET_TIME_DIFFERENCE(servtime, c.authreqs[i].reqtime) >= AUTH_TIME) + { + outputf(c, "failauth %u\n", c.authreqs[i].id); + freechallenge(c.authreqs[i].answer); + expired = i + 1; + } + else break; + } + if(expired > 0) c.authreqs.remove(0, expired); +} + +void reqauth(client &c, uint id, char *name) +{ + if(ENET_TIME_DIFFERENCE(servtime, c.lastauth) < AUTH_THROTTLE) + return; + + c.lastauth = servtime; + + purgeauths(c); + + time_t t = time(NULL); + char *ct = ctime(&t); + if(ct) + { + char *newline = strchr(ct, '\n'); + if(newline) *newline = '\0'; + } + string ip; + if(enet_address_get_host_ip(&c.address, ip, sizeof(ip)) < 0) copystring(ip, "-"); + conoutf("%s: attempting \"%s\" as %u from %s", ct ? ct : "-", name, id, ip); + + userinfo *u = users.access(name); + if(!u) + { + outputf(c, "failauth %u\n", id); + return; + } + + if(c.authreqs.length() >= AUTH_LIMIT) + { + outputf(c, "failauth %u\n", c.authreqs[0].id); + freechallenge(c.authreqs[0].answer); + c.authreqs.remove(0); + } + + authreq &a = c.authreqs.add(); + a.reqtime = servtime; + a.id = id; + uint seed[3] = { uint(starttime), servtime, randomMT() }; + static vector buf; + buf.setsize(0); + a.answer = genchallenge(u->pubkey, seed, sizeof(seed), buf); + + outputf(c, "chalauth %u %s\n", id, buf.getbuf()); +} + +void confauth(client &c, uint id, const char *val) +{ + purgeauths(c); + + loopv(c.authreqs) if(c.authreqs[i].id == id) + { + string ip; + if(enet_address_get_host_ip(&c.address, ip, sizeof(ip)) < 0) copystring(ip, "-"); + if(checkchallenge(val, c.authreqs[i].answer)) + { + outputf(c, "succauth %u\n", id); + conoutf("succeeded %u from %s", id, ip); + } + else + { + outputf(c, "failauth %u\n", id); + conoutf("failed %u from %s", id, ip); + } + freechallenge(c.authreqs[i].answer); + c.authreqs.remove(i--); + return; + } + outputf(c, "failauth %u\n", id); +} + +bool checkclientinput(client &c) +{ + if(c.inputpos<0) return true; + char *end = (char *)memchr(c.input, '\n', c.inputpos); + while(end) + { + *end++ = '\0'; + c.lastinput = servtime; + + int port; + uint id; + string user, val; + if(!strncmp(c.input, "list", 4) && (!c.input[4] || c.input[4] == '\n' || c.input[4] == '\r')) + { + genserverlist(); + if(gameserverlists.empty() || c.message) return false; + c.message = gameserverlists.last(); + c.message->refs++; + c.output.setsize(0); + c.outputpos = 0; + c.shouldpurge = true; + return true; + } + else if(sscanf(c.input, "regserv %d", &port) == 1) + { + if(checkban(servbans, c.address.host)) return false; + if(port < 0 || port > 0xFFFF-1 || (c.servport >= 0 && port != c.servport)) outputf(c, "failreg invalid port\n"); + else + { + c.servport = port; + addgameserver(c); + } + } + else if(sscanf(c.input, "reqauth %u %100s", &id, user) == 2) + { + reqauth(c, id, user); + } + else if(sscanf(c.input, "confauth %u %100s", &id, val) == 2) + { + confauth(c, id, val); + } + c.inputpos = &c.input[c.inputpos] - end; + memmove(c.input, end, c.inputpos); + + end = (char *)memchr(c.input, '\n', c.inputpos); + } + return c.inputpos<(int)sizeof(c.input); +} + +ENetSocketSet readset, writeset; + +void checkclients() +{ + ENetSocketSet readset, writeset; + ENetSocket maxsock = max(serversocket, pingsocket); + ENET_SOCKETSET_EMPTY(readset); + ENET_SOCKETSET_EMPTY(writeset); + ENET_SOCKETSET_ADD(readset, serversocket); + ENET_SOCKETSET_ADD(readset, pingsocket); + loopv(clients) + { + client &c = *clients[i]; + if(c.authreqs.length()) purgeauths(c); + if(c.message || c.output.length()) ENET_SOCKETSET_ADD(writeset, c.socket); + else ENET_SOCKETSET_ADD(readset, c.socket); + maxsock = max(maxsock, c.socket); + } + if(enet_socketset_select(maxsock, &readset, &writeset, 1000)<=0) return; + + if(ENET_SOCKETSET_CHECK(readset, pingsocket)) checkserverpongs(); + if(ENET_SOCKETSET_CHECK(readset, serversocket)) + { + ENetAddress address; + ENetSocket clientsocket = enet_socket_accept(serversocket, &address); + if(clients.length()>=CLIENT_LIMIT || checkban(bans, address.host)) enet_socket_destroy(clientsocket); + else if(clientsocket!=ENET_SOCKET_NULL) + { + int dups = 0, oldest = -1; + loopv(clients) if(clients[i]->address.host == address.host) + { + dups++; + if(oldest<0 || clients[i]->connecttime < clients[oldest]->connecttime) oldest = i; + } + if(dups >= DUP_LIMIT) purgeclient(oldest); + + client *c = new client; + c->address = address; + c->socket = clientsocket; + c->connecttime = servtime; + c->lastinput = servtime; + clients.add(c); + } + } + + loopv(clients) + { + client &c = *clients[i]; + if((c.message || c.output.length()) && ENET_SOCKETSET_CHECK(writeset, c.socket)) + { + const char *data = c.output.length() ? c.output.getbuf() : c.message->getbuf(); + int len = c.output.length() ? c.output.length() : c.message->length(); + ENetBuffer buf; + buf.data = (void *)&data[c.outputpos]; + buf.dataLength = len-c.outputpos; + int res = enet_socket_send(c.socket, NULL, &buf, 1); + if(res>=0) + { + c.outputpos += res; + if(c.outputpos>=len) + { + if(c.output.length()) c.output.setsize(0); + else + { + c.message->purge(); + c.message = NULL; + } + c.outputpos = 0; + if(!c.message && c.output.empty() && c.shouldpurge) + { + purgeclient(i--); + continue; + } + } + } + else { purgeclient(i--); continue; } + } + if(ENET_SOCKETSET_CHECK(readset, c.socket)) + { + ENetBuffer buf; + buf.data = &c.input[c.inputpos]; + buf.dataLength = sizeof(c.input) - c.inputpos; + int res = enet_socket_receive(c.socket, NULL, &buf, 1); + if(res>0) + { + c.inputpos += res; + c.input[min(c.inputpos, (int)sizeof(c.input)-1)] = '\0'; + if(!checkclientinput(c)) { purgeclient(i--); continue; } + } + else { purgeclient(i--); continue; } + } + if(c.output.length() > OUTPUT_LIMIT) { purgeclient(i--); continue; } + if(ENET_TIME_DIFFERENCE(servtime, c.lastinput) >= (c.registeredserver ? KEEPALIVE_TIME : CLIENT_TIME)) { purgeclient(i--); continue; } + } +} + +void banclients() +{ + loopvrev(clients) if(checkban(bans, clients[i]->address.host)) purgeclient(i); +} + +volatile int reloadcfg = 1; + +#ifndef WIN32 +void reloadsignal(int signum) +{ + reloadcfg = 1; +} +#endif + +int main(int argc, char **argv) +{ + if(enet_initialize()<0) fatal("Unable to initialise network module"); + atexit(enet_deinitialize); + + const char *dir = "", *ip = NULL; + int port = 28787; + if(argc>=2) dir = argv[1]; + if(argc>=3) port = atoi(argv[2]); + if(argc>=4) ip = argv[3]; + defformatstring(logname, "%smaster.log", dir); + defformatstring(cfgname, "%smaster.cfg", dir); + path(logname); + path(cfgname); + logfile = fopen(logname, "a"); + if(!logfile) logfile = stdout; + setvbuf(logfile, NULL, _IOLBF, BUFSIZ); +#ifndef WIN32 + signal(SIGUSR1, reloadsignal); +#endif + setupserver(port, ip); + for(;;) + { + if(reloadcfg) + { + conoutf("reloading master.cfg"); + execfile(cfgname); + bangameservers(); + banclients(); + gengbanlist(); + reloadcfg = 0; + } + + servtime = enet_time_get(); + checkclients(); + checkgameservers(); + } + + return EXIT_SUCCESS; +} + diff --git a/src/engine/material.cpp b/src/engine/material.cpp new file mode 100644 index 0000000..59f47f8 --- /dev/null +++ b/src/engine/material.cpp @@ -0,0 +1,886 @@ +#include "engine.h" + +struct QuadNode +{ + int x, y, size; + uint filled; + QuadNode *child[4]; + + QuadNode(int x, int y, int size) : x(x), y(y), size(size), filled(0) { loopi(4) child[i] = 0; } + + void clear() + { + loopi(4) DELETEP(child[i]); + } + + ~QuadNode() + { + clear(); + } + + void insert(int mx, int my, int msize) + { + if(size == msize) + { + filled = 0xF; + return; + } + int csize = size>>1, i = 0; + if(mx >= x+csize) i |= 1; + if(my >= y+csize) i |= 2; + if(csize == msize) + { + filled |= (1 << i); + return; + } + if(!child[i]) child[i] = new QuadNode(i&1 ? x+csize : x, i&2 ? y+csize : y, csize); + child[i]->insert(mx, my, msize); + loopj(4) if(child[j]) + { + if(child[j]->filled == 0xF) + { + DELETEP(child[j]); + filled |= (1 << j); + } + } + } + + void genmatsurf(ushort mat, uchar orient, uchar visible, int x, int y, int z, int size, materialsurface *&matbuf) + { + materialsurface &m = *matbuf++; + m.material = mat; + m.orient = orient; + m.visible = visible; + m.csize = size; + m.rsize = size; + int dim = dimension(orient); + m.o[C[dim]] = x; + m.o[R[dim]] = y; + m.o[dim] = z; + } + + void genmatsurfs(ushort mat, uchar orient, uchar flags, int z, materialsurface *&matbuf) + { + if(filled == 0xF) genmatsurf(mat, orient, flags, x, y, z, size, matbuf); + else if(filled) + { + int csize = size>>1; + loopi(4) if(filled & (1 << i)) + genmatsurf(mat, orient, flags, i&1 ? x+csize : x, i&2 ? y+csize : y, z, csize, matbuf); + } + loopi(4) if(child[i]) child[i]->genmatsurfs(mat, orient, flags, z, matbuf); + } +}; + +static float wfwave; + +static const bvec4 matnormals[6] = +{ + bvec4(0x80, 0, 0), + bvec4(0x7F, 0, 0), + bvec4(0, 0x80, 0), + bvec4(0, 0x7F, 0), + bvec4(0, 0, 0x80), + bvec4(0, 0, 0x7F) +}; + +static void renderwaterfall(const materialsurface &m, float offset) +{ + if(gle::attribbuf.empty()) + { + gle::defvertex(); + gle::defnormal(4, GL_BYTE); + gle::begin(GL_QUADS); + } + float x = m.o.x, y = m.o.y, zmin = m.o.z, zmax = zmin; + if(m.ends&1) zmin += -WATER_OFFSET-WATER_AMPLITUDE; + if(m.ends&2) zmax += wfwave; + int csize = m.csize, rsize = m.rsize; + switch(m.orient) + { + #define GENFACEORIENT(orient, v0, v1, v2, v3) \ + case orient: v0 v1 v2 v3 break; + #define GENFACEVERT(orient, vert, mx,my,mz, sx,sy,sz) \ + { \ + gle::attribf(mx sx, my sy, mz sz); \ + gle::attrib(matnormals[orient]); \ + } + GENFACEVERTSXY(x, x, y, y, zmin, zmax, /**/, + csize, /**/, + rsize, + offset, - offset) + #undef GENFACEORIENT + #undef GENFACEVERT + } +} + +static void drawmaterial(const materialsurface &m, float offset, const bvec4 &color) +{ + if(gle::attribbuf.empty()) + { + gle::defvertex(); + gle::defcolor(4, GL_UNSIGNED_BYTE); + gle::begin(GL_QUADS); + } + float x = m.o.x, y = m.o.y, z = m.o.z, csize = m.csize, rsize = m.rsize; + switch(m.orient) + { + #define GENFACEORIENT(orient, v0, v1, v2, v3) \ + case orient: v0 v1 v2 v3 break; + #define GENFACEVERT(orient, vert, mx,my,mz, sx,sy,sz) \ + { \ + gle::attribf(mx sx, my sy, mz sz); \ + gle::attrib(color); \ + } + GENFACEVERTS(x, x, y, y, z, z, /**/, + csize, /**/, + rsize, + offset, - offset) + #undef GENFACEORIENT + #undef GENFACEVERT + } +} + +const struct material +{ + const char *name; + ushort id; +} materials[] = +{ + {"air", MAT_AIR}, + {"water", MAT_WATER}, {"water1", MAT_WATER}, {"water2", MAT_WATER+1}, {"water3", MAT_WATER+2}, {"water4", MAT_WATER+3}, + {"glass", MAT_GLASS}, {"glass1", MAT_GLASS}, {"glass2", MAT_GLASS+1}, {"glass3", MAT_GLASS+2}, {"glass4", MAT_GLASS+3}, + {"lava", MAT_LAVA}, {"lava1", MAT_LAVA}, {"lava2", MAT_LAVA+1}, {"lava3", MAT_LAVA+2}, {"lava4", MAT_LAVA+3}, + {"clip", MAT_CLIP}, + {"noclip", MAT_NOCLIP}, + {"gameclip", MAT_GAMECLIP}, + {"death", MAT_DEATH}, + {"alpha", MAT_ALPHA} +}; + +int findmaterial(const char *name) +{ + loopi(sizeof(materials)/sizeof(material)) + { + if(!strcmp(materials[i].name, name)) return materials[i].id; + } + return -1; +} + +const char *findmaterialname(int mat) +{ + loopi(sizeof(materials)/sizeof(materials[0])) if(materials[i].id == mat) return materials[i].name; + return NULL; +} + +const char *getmaterialdesc(int mat, const char *prefix) +{ + static const ushort matmasks[] = { MATF_VOLUME|MATF_INDEX, MATF_CLIP, MAT_DEATH, MAT_ALPHA }; + static string desc; + desc[0] = '\0'; + loopi(sizeof(matmasks)/sizeof(matmasks[0])) if(mat&matmasks[i]) + { + const char *matname = findmaterialname(mat&matmasks[i]); + if(matname) + { + concatstring(desc, desc[0] ? ", " : prefix); + concatstring(desc, matname); + } + } + return desc; +} + +int visiblematerial(const cube &c, int orient, const ivec &co, int size, ushort matmask) +{ + ushort mat = c.material&matmask; + switch(mat) + { + case MAT_AIR: + break; + + case MAT_LAVA: + case MAT_WATER: + if(visibleface(c, orient, co, size, mat, MAT_AIR, matmask)) + return (orient != O_BOTTOM ? MATSURF_VISIBLE : MATSURF_EDIT_ONLY); + break; + + case MAT_GLASS: + if(visibleface(c, orient, co, size, MAT_GLASS, MAT_AIR, matmask)) + return MATSURF_VISIBLE; + break; + + default: + if(visibleface(c, orient, co, size, mat, MAT_AIR, matmask)) + return MATSURF_EDIT_ONLY; + break; + } + return MATSURF_NOT_VISIBLE; +} + +void genmatsurfs(const cube &c, const ivec &co, int size, vector &matsurfs) +{ + loopi(6) + { + static const ushort matmasks[] = { MATF_VOLUME|MATF_INDEX, MATF_CLIP, MAT_DEATH, MAT_ALPHA }; + loopj(sizeof(matmasks)/sizeof(matmasks[0])) + { + int matmask = matmasks[j]; + int vis = visiblematerial(c, i, co, size, matmask&~MATF_INDEX); + if(vis != MATSURF_NOT_VISIBLE) + { + materialsurface m; + m.material = c.material&matmask; + m.orient = i; + m.visible = vis; + m.o = co; + m.csize = m.rsize = size; + if(dimcoord(i)) m.o[dimension(i)] += size; + matsurfs.add(m); + break; + } + } + } +} + +static inline bool mergematcmp(const materialsurface &x, const materialsurface &y) +{ + int dim = dimension(x.orient), c = C[dim], r = R[dim]; + if(x.o[r] + x.rsize < y.o[r] + y.rsize) return true; + if(x.o[r] + x.rsize > y.o[r] + y.rsize) return false; + return x.o[c] < y.o[c]; +} + +static int mergematr(materialsurface *m, int sz, materialsurface &n) +{ + int dim = dimension(n.orient), c = C[dim], r = R[dim]; + for(int i = sz-1; i >= 0; --i) + { + if(m[i].o[r] + m[i].rsize < n.o[r]) break; + if(m[i].o[r] + m[i].rsize == n.o[r] && m[i].o[c] == n.o[c] && m[i].csize == n.csize) + { + n.o[r] = m[i].o[r]; + n.rsize += m[i].rsize; + memmove(&m[i], &m[i+1], (sz - (i+1)) * sizeof(materialsurface)); + return 1; + } + } + return 0; +} + +static int mergematc(materialsurface &m, materialsurface &n) +{ + int dim = dimension(n.orient), c = C[dim], r = R[dim]; + if(m.o[r] == n.o[r] && m.rsize == n.rsize && m.o[c] + m.csize == n.o[c]) + { + n.o[c] = m.o[c]; + n.csize += m.csize; + return 1; + } + return 0; +} + +static int mergemat(materialsurface *m, int sz, materialsurface &n) +{ + for(bool merged = false; sz; merged = true) + { + int rmerged = mergematr(m, sz, n); + sz -= rmerged; + if(!rmerged && merged) break; + if(!sz) break; + int cmerged = mergematc(m[sz-1], n); + sz -= cmerged; + if(!cmerged) break; + } + m[sz++] = n; + return sz; +} + +static int mergemats(materialsurface *m, int sz) +{ + quicksort(m, sz, mergematcmp); + + int nsz = 0; + loopi(sz) nsz = mergemat(m, nsz, m[i]); + return nsz; +} + +static inline bool optmatcmp(const materialsurface &x, const materialsurface &y) +{ + if(x.material < y.material) return true; + if(x.material > y.material) return false; + if(x.orient > y.orient) return true; + if(x.orient < y.orient) return false; + int dim = dimension(x.orient); + return x.o[dim] < y.o[dim]; +} + +VARF(optmats, 0, 1, 1, allchanged()); + +int optimizematsurfs(materialsurface *matbuf, int matsurfs) +{ + quicksort(matbuf, matsurfs, optmatcmp); + if(!optmats) return matsurfs; + materialsurface *cur = matbuf, *end = matbuf+matsurfs; + while(cur < end) + { + materialsurface *start = cur++; + int dim = dimension(start->orient); + while(cur < end && + cur->material == start->material && + cur->orient == start->orient && + cur->visible == start->visible && + cur->o[dim] == start->o[dim]) + ++cur; + if(!isliquid(start->material&MATF_VOLUME) || start->orient != O_TOP || !vertwater) + { + if(start!=matbuf) memmove(matbuf, start, (cur-start)*sizeof(materialsurface)); + matbuf += mergemats(matbuf, cur-start); + } + else if(cur-start>=4) + { + QuadNode vmats(0, 0, worldsize); + loopi(cur-start) vmats.insert(start[i].o[C[dim]], start[i].o[R[dim]], start[i].csize); + vmats.genmatsurfs(start->material, start->orient, start->visible, start->o[dim], matbuf); + } + else + { + if(start!=matbuf) memmove(matbuf, start, (cur-start)*sizeof(materialsurface)); + matbuf += cur-start; + } + } + return matsurfs - (end-matbuf); +} + +struct waterinfo +{ + materialsurface *m; + double depth, area; +}; + +void setupmaterials(int start, int len) +{ + int hasmat = 0; + vector water; + unionfind uf; + if(!len) len = valist.length(); + for(int i = start; i < len; i++) + { + vtxarray *va = valist[i]; + materialsurface *skip = NULL; + loopj(va->matsurfs) + { + materialsurface &m = va->matbuf[j]; + int matvol = m.material&MATF_VOLUME; + if(matvol==MAT_WATER && m.orient==O_TOP) + { + m.index = water.length(); + loopvk(water) + { + materialsurface &n = *water[k].m; + if(m.material!=n.material || m.o.z!=n.o.z) continue; + if(n.o.x+n.rsize==m.o.x || m.o.x+m.rsize==n.o.x) + { + if(n.o.y+n.csize>m.o.y && n.o.ym.o.x && n.o.xmaterial && m.orient == skip->orient && skip->skip < 0xFFFF) + skip->skip++; + else + skip = &m; + } + } + loopv(water) + { + int root = uf.find(i); + if(i==root) continue; + materialsurface &m = *water[i].m, &n = *water[root].m; + if(m.light && (!m.light->attr1 || !n.light || (n.light->attr1 && m.light->attr1 > n.light->attr1))) n.light = m.light; + water[root].depth += water[i].depth; + water[root].area += water[i].area; + } + loopv(water) + { + int root = uf.find(i); + water[i].m->light = water[root].m->light; + water[i].m->depth = (short)(water[root].depth/water[root].area); + } + if(hasmat&(0xF< ymin && ymax > xmin) continue; + int c = sortorigin[dim]; + if(c > xmin && c < xmax) return sortedit; + if(c > ymin && c < ymax) return !sortedit; + xmin = abs(xmin - c); + xmax = abs(xmax - c); + ymin = abs(ymin - c); + ymax = abs(ymax - c); + if(max(xmin, xmax) <= min(ymin, ymax)) return sortedit; + else if(max(ymin, ymax) <= min(xmin, xmax)) return !sortedit; + } + if(x.material < y.material) return sortedit; + if(x.material > y.material) return !sortedit; + return false; +} + +void sortmaterials(vector &vismats) +{ + sortorigin = ivec(camera1->o); + if(reflecting) sortorigin.z = int(reflectz - (camera1->o.z - reflectz)); + vec dir; + vecfromyawpitch(camera1->yaw, reflecting ? -camera1->pitch : camera1->pitch, 1, 0, dir); + loopi(3) { dir[i] = fabs(dir[i]); sortdim[i] = i; } + if(dir[sortdim[2]] > dir[sortdim[1]]) swap(sortdim[2], sortdim[1]); + if(dir[sortdim[1]] > dir[sortdim[0]]) swap(sortdim[1], sortdim[0]); + if(dir[sortdim[2]] > dir[sortdim[1]]) swap(sortdim[2], sortdim[1]); + + for(vtxarray *va = reflecting ? reflectedva : visibleva; va; va = reflecting ? va->rnext : va->next) + { + if(!va->matsurfs || va->occluded >= OCCLUDE_BB) continue; + if(reflecting || refracting>0 ? va->o.z+va->size <= reflectz : va->o.z >= reflectz) continue; + loopi(va->matsurfs) + { + materialsurface &m = va->matbuf[i]; + if(!editmode || !showmat || drawtex) + { + int matvol = m.material&MATF_VOLUME; + if(matvol==MAT_WATER && (m.orient==O_TOP || (refracting<0 && reflectz>worldsize))) { i += m.skip; continue; } + if(m.visible == MATSURF_EDIT_ONLY) { i += m.skip; continue; } + if(glaring && matvol!=MAT_LAVA) { i += m.skip; continue; } + } + else if(glaring) continue; + vismats.add(&m); + } + } + sortedit = editmode && showmat && !drawtex; + vismats.sort(vismatcmp); +} + +void rendermatgrid(vector &vismats) +{ + enablepolygonoffset(GL_POLYGON_OFFSET_LINE); + glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + int lastmat = -1; + bvec4 color(0, 0, 0, 0); + loopvrev(vismats) + { + materialsurface &m = *vismats[i]; + if(m.material != lastmat) + { + switch(m.material&~MATF_INDEX) + { + case MAT_WATER: color = bvec4( 0, 0, 85, 255); break; // blue + case MAT_CLIP: color = bvec4(85, 0, 0, 255); break; // red + case MAT_GLASS: color = bvec4( 0, 85, 85, 255); break; // cyan + case MAT_NOCLIP: color = bvec4( 0, 85, 0, 255); break; // green + case MAT_LAVA: color = bvec4(85, 40, 0, 255); break; // orange + case MAT_GAMECLIP: color = bvec4(85, 85, 0, 255); break; // yellow + case MAT_DEATH: color = bvec4(40, 40, 40, 255); break; // black + case MAT_ALPHA: color = bvec4(85, 0, 85, 255); break; // pink + default: continue; + } + lastmat = m.material; + } + drawmaterial(m, -0.1f, color); + } + xtraverts += gle::end(); + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + disablepolygonoffset(GL_POLYGON_OFFSET_LINE); +} + +#define GLASSVARS(name) \ + bvec name##color(0x20, 0x80, 0xC0); \ + HVARFR(name##colour, 0, 0x2080C0, 0xFFFFFF, \ + { \ + if(!name##colour) name##colour = 0x2080C0; \ + name##color = bvec((name##colour>>16)&0xFF, (name##colour>>8)&0xFF, name##colour&0xFF); \ + }); + +GLASSVARS(glass) +GLASSVARS(glass2) +GLASSVARS(glass3) +GLASSVARS(glass4) + +GETMATIDXVAR(glass, colour, int) +GETMATIDXVAR(glass, color, const bvec &) + +VARP(glassenv, 0, 1, 1); + +static void drawglass(const materialsurface &m, float offset) +{ + if(gle::attribbuf.empty()) + { + gle::defvertex(); + gle::defnormal(4, GL_BYTE); + gle::begin(GL_QUADS); + } + float x = m.o.x, y = m.o.y, z = m.o.z, csize = m.csize, rsize = m.rsize; + switch(m.orient) + { + #define GENFACEORIENT(orient, v0, v1, v2, v3) \ + case orient: v0 v1 v2 v3 break; + #define GENFACEVERT(orient, vert, mx,my,mz, sx,sy,sz) \ + { \ + gle::attribf(mx sx, my sy, mz sz); \ + gle::attrib(matnormals[orient]); \ + } + GENFACEVERTS(x, x, y, y, z, z, /**/, + csize, /**/, + rsize, + offset, - offset) + #undef GENFACEORIENT + #undef GENFACEVERT + } +} + +VARFP(waterfallenv, 0, 1, 1, preloadwatershaders()); + +static inline void changematerial(int mat, int orient) +{ + switch(mat&~MATF_INDEX) + { + case MAT_LAVA: + if(orient==O_TOP) flushlava(); + else xtraverts += gle::end(); + break; + default: + xtraverts += gle::end(); + break; + } +} + +void rendermaterials() +{ + vector vismats; + sortmaterials(vismats); + if(vismats.empty()) return; + + glDisable(GL_CULL_FACE); + + MSlot *mslot = NULL; + int lastorient = -1, lastmat = -1, usedwaterfall = -1; + bool depth = true, blended = false; + ushort envmapped = EMID_NONE; + + GLOBALPARAM(camera, camera1->o); + + int lastfogtype = 1; + if(editmode && showmat && !drawtex) + { + glBlendFunc(GL_ZERO, GL_ONE_MINUS_SRC_COLOR); + glEnable(GL_BLEND); blended = true; + foggednotextureshader->set(); + zerofogcolor(); lastfogtype = 0; + bvec4 color(0, 0, 0, 0); + loopv(vismats) + { + const materialsurface &m = *vismats[i]; + if(lastmat!=m.material) + { + switch(m.material&~MATF_INDEX) + { + case MAT_WATER: color = bvec4(255, 128, 0, 255); break; // blue + case MAT_CLIP: color = bvec4( 0, 255, 255, 255); break; // red + case MAT_GLASS: color = bvec4(255, 0, 0, 255); break; // cyan + case MAT_NOCLIP: color = bvec4(255, 0, 255, 255); break; // green + case MAT_LAVA: color = bvec4( 0, 128, 255, 255); break; // orange + case MAT_GAMECLIP: color = bvec4( 0, 0, 255, 255); break; // yellow + case MAT_DEATH: color = bvec4(192, 192, 192, 255); break; // black + case MAT_ALPHA: color = bvec4( 0, 255, 0, 255); break; // pink + default: continue; + } + lastmat = m.material; + } + drawmaterial(m, -0.1f, color); + } + xtraverts += gle::end(); + } + else loopv(vismats) + { + const materialsurface &m = *vismats[i]; + int matvol = m.material&~MATF_INDEX; + if(lastmat!=m.material || lastorient!=m.orient || (matvol==MAT_GLASS && envmapped && m.envmap != envmapped)) + { + int fogtype = lastfogtype; + switch(matvol) + { + case MAT_WATER: + if(m.orient == O_TOP) continue; + if(lastmat == m.material) break; + mslot = &lookupmaterialslot(m.material, false); + if(!mslot->loaded || !mslot->sts.inrange(1)) continue; + else + { + changematerial(lastmat, lastorient); + glBindTexture(GL_TEXTURE_2D, mslot->sts[1].t->id); + + bvec wfcol = getwaterfallcolor(m.material); + if(wfcol.iszero()) wfcol = getwatercolor(m.material); + gle::color(wfcol, 192); + + int wfog = getwaterfog(m.material); + if(!wfog && !waterfallenv) + { + foggednotextureshader->set(); + fogtype = 1; + if(blended) { glDisable(GL_BLEND); blended = false; } + if(!depth) { glDepthMask(GL_TRUE); depth = true; } + } + else if((!waterfallrefract || reflecting || refracting) && !waterfallenv) + { + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_COLOR); + SETSHADER(waterfall); + fogtype = 0; + if(!blended) { glEnable(GL_BLEND); blended = true; } + if(depth) { glDepthMask(GL_FALSE); depth = false; } + } + else + { + fogtype = 1; + + if(waterfallrefract && wfog && !reflecting && !refracting) + { + if(waterfallenv) SETSHADER(waterfallenvrefract); + else SETSHADER(waterfallrefract); + if(blended) { glDisable(GL_BLEND); blended = false; } + if(!depth) { glDepthMask(GL_TRUE); depth = true; } + } + else + { + SETSHADER(waterfallenv); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + if(wfog) + { + if(!blended) { glEnable(GL_BLEND); blended = true; } + if(depth) { glDepthMask(GL_FALSE); depth = false; } + } + else + { + if(blended) { glDisable(GL_BLEND); blended = false; } + if(!depth) { glDepthMask(GL_TRUE); depth = true; } + } + } + + if(usedwaterfall != m.material) + { + Texture *dudv = mslot->sts.inrange(5) ? mslot->sts[5].t : notexture; + float scale = TEX_SCALE/(dudv->ys*mslot->scale); + LOCALPARAMF(dudvoffset, 0, scale*16*lastmillis/1000.0f); + + glActiveTexture_(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, mslot->sts.inrange(4) ? mslot->sts[4].t->id : notexture->id); + glActiveTexture_(GL_TEXTURE2); + glBindTexture(GL_TEXTURE_2D, mslot->sts.inrange(5) ? mslot->sts[5].t->id : notexture->id); + if(waterfallenv) + { + glActiveTexture_(GL_TEXTURE3); + glBindTexture(GL_TEXTURE_CUBE_MAP, lookupenvmap(*mslot)); + } + if(waterfallrefract && (!reflecting || !refracting) && usedwaterfall < 0) + { + glActiveTexture_(GL_TEXTURE4); + extern void setupwaterfallrefract(); + setupwaterfallrefract(); + } + glActiveTexture_(GL_TEXTURE0); + + usedwaterfall = m.material; + } + } + float angle = fmod(float(lastmillis/600.0f/(2*M_PI)), 1.0f), + s = angle - int(angle) - 0.5f; + s *= 8 - fabs(s)*16; + wfwave = vertwater ? WATER_AMPLITUDE*s-WATER_OFFSET : -WATER_OFFSET; + float scroll = 16.0f*lastmillis/1000.0f; + float xscale = TEX_SCALE/(mslot->sts[1].t->xs*mslot->scale); + float yscale = -TEX_SCALE/(mslot->sts[1].t->ys*mslot->scale); + LOCALPARAMF(waterfalltexgen, xscale, yscale, 0.0f, scroll); + } + break; + + case MAT_LAVA: + if(lastmat==m.material && lastorient!=O_TOP && m.orient!=O_TOP) break; + mslot = &lookupmaterialslot(m.material, false); + if(!mslot->loaded) continue; + else + { + int subslot = m.orient==O_TOP ? 0 : 1; + if(!mslot->sts.inrange(subslot)) continue; + changematerial(lastmat, lastorient); + glBindTexture(GL_TEXTURE_2D, mslot->sts[subslot].t->id); + } + if(lastmat!=m.material) + { + if(!depth) { glDepthMask(GL_TRUE); depth = true; } + if(blended) { glDisable(GL_BLEND); blended = false; } + float t = lastmillis/2000.0f; + t -= floor(t); + t = 1.0f - 2*fabs(t-0.5f); + extern int glare; + if(glare) t = 0.625f + 0.075f*t; + else t = 0.5f + 0.5f*t; + gle::colorf(t, t, t); + if(glaring) SETSHADER(lavaglare); else SETSHADER(lava); + fogtype = 1; + } + if(m.orient!=O_TOP) + { + float angle = fmod(float(lastmillis/2000.0f/(2*M_PI)), 1.0f), + s = angle - int(angle) - 0.5f; + s *= 8 - fabs(s)*16; + wfwave = vertwater ? WATER_AMPLITUDE*s-WATER_OFFSET : -WATER_OFFSET; + float scroll = 16.0f*lastmillis/3000.0f; + float xscale = TEX_SCALE/(mslot->sts[1].t->xs*mslot->scale); + float yscale = -TEX_SCALE/(mslot->sts[1].t->ys*mslot->scale); + LOCALPARAMF(lavatexgen, xscale, yscale, 0.0f, scroll); + } + else setuplava(mslot->sts[0].t, mslot->scale); + break; + + case MAT_GLASS: + if((m.envmap==EMID_NONE || !glassenv || envmapped==m.envmap) && lastmat==m.material) break; + changematerial(lastmat, lastorient); + if(m.envmap!=EMID_NONE && glassenv && envmapped!=m.envmap) + { + glBindTexture(GL_TEXTURE_CUBE_MAP, lookupenvmap(m.envmap)); + envmapped = m.envmap; + } + if(lastmat!=m.material) + { + if(!blended) { glEnable(GL_BLEND); blended = true; } + if(depth) { glDepthMask(GL_FALSE); depth = false; } + const bvec &gcol = getglasscolor(m.material); + if(m.envmap!=EMID_NONE && glassenv) + { + glBlendFunc(GL_ONE, GL_SRC_ALPHA); + gle::color(gcol); + SETSHADER(glass); + } + else + { + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + gle::color(gcol, 40); + foggednotextureshader->set(); + fogtype = 1; + } + } + break; + + default: continue; + } + lastmat = m.material; + lastorient = m.orient; + if(fogtype!=lastfogtype) + { + if(fogtype) resetfogcolor(); + else zerofogcolor(); + lastfogtype = fogtype; + } + } + switch(matvol) + { + case MAT_WATER: + renderwaterfall(m, 0.1f); + break; + + case MAT_LAVA: + if(m.orient==O_TOP) renderlava(m); + else renderwaterfall(m, 0.1f); + break; + + case MAT_GLASS: + drawglass(m, 0.1f); + break; + } + } + + if(lastorient >= 0) changematerial(lastmat, lastorient); + + if(!depth) glDepthMask(GL_TRUE); + if(blended) glDisable(GL_BLEND); + if(!lastfogtype) resetfogcolor(); + extern int wireframe; + if(editmode && showmat && !drawtex && !wireframe) + { + foggednotextureshader->set(); + rendermatgrid(vismats); + } + + glEnable(GL_CULL_FACE); +} + diff --git a/src/engine/md3.h b/src/engine/md3.h new file mode 100644 index 0000000..d560255 --- /dev/null +++ b/src/engine/md3.h @@ -0,0 +1,183 @@ +struct md3; + +struct md3frame +{ + vec bbmin, bbmax, origin; + float radius; + uchar name[16]; +}; + +struct md3tag +{ + char name[64]; + float translation[3]; + float rotation[3][3]; +}; + +struct md3vertex +{ + short vertex[3]; + short normal; +}; + +struct md3triangle +{ + int vertexindices[3]; +}; + +struct md3header +{ + char id[4]; + int version; + char name[64]; + int flags; + int numframes, numtags, nummeshes, numskins; + int ofs_frames, ofs_tags, ofs_meshes, ofs_eof; // offsets +}; + +struct md3meshheader +{ + char id[4]; + char name[64]; + int flags; + int numframes, numshaders, numvertices, numtriangles; + int ofs_triangles, ofs_shaders, ofs_uv, ofs_vertices, meshsize; // offsets +}; + +struct md3 : vertloader +{ + md3(const char *name) : vertloader(name) {} + + static const char *formatname() { return "md3"; } + bool flipy() const { return true; } + int type() const { return MDL_MD3; } + + struct md3meshgroup : vertmeshgroup + { + bool load(const char *path) + { + stream *f = openfile(path, "rb"); + if(!f) return false; + md3header header; + f->read(&header, sizeof(md3header)); + lilswap(&header.version, 1); + lilswap(&header.flags, 9); + if(strncmp(header.id, "IDP3", 4) != 0 || header.version != 15) // header check + { + delete f; + conoutf(CON_ERROR, "md3: corrupted header"); + return false; + } + + name = newstring(path); + + numframes = header.numframes; + + int mesh_offset = header.ofs_meshes; + loopi(header.nummeshes) + { + vertmesh &m = *new vertmesh; + m.group = this; + meshes.add(&m); + + md3meshheader mheader; + f->seek(mesh_offset, SEEK_SET); + f->read(&mheader, sizeof(md3meshheader)); + lilswap(&mheader.flags, 10); + + m.name = newstring(mheader.name); + + m.numtris = mheader.numtriangles; + m.tris = new tri[m.numtris]; + f->seek(mesh_offset + mheader.ofs_triangles, SEEK_SET); + loopj(m.numtris) + { + md3triangle tri; + f->read(&tri, sizeof(md3triangle)); // read the triangles + lilswap(tri.vertexindices, 3); + loopk(3) m.tris[j].vert[k] = (ushort)tri.vertexindices[k]; + } + + m.numverts = mheader.numvertices; + m.tcverts = new tcvert[m.numverts]; + f->seek(mesh_offset + mheader.ofs_uv , SEEK_SET); + f->read(m.tcverts, m.numverts*2*sizeof(float)); // read the UV data + lilswap(&m.tcverts[0].tc.x, 2*m.numverts); + + m.verts = new vert[numframes*m.numverts]; + f->seek(mesh_offset + mheader.ofs_vertices, SEEK_SET); + loopj(numframes*m.numverts) + { + md3vertex v; + f->read(&v, sizeof(md3vertex)); // read the vertices + lilswap(v.vertex, 4); + + m.verts[j].pos = vec(v.vertex[0]/64.0f, -v.vertex[1]/64.0f, v.vertex[2]/64.0f); + + float lng = (v.normal&0xFF)*2*M_PI/255.0f; // decode vertex normals + float lat = ((v.normal>>8)&0xFF)*2*M_PI/255.0f; + m.verts[j].norm = vec(cosf(lat)*sinf(lng), -sinf(lat)*sinf(lng), cosf(lng)); + } + + mesh_offset += mheader.meshsize; + } + + numtags = header.numtags; + if(numtags) + { + tags = new tag[numframes*numtags]; + f->seek(header.ofs_tags, SEEK_SET); + md3tag tag; + + loopi(header.numframes*header.numtags) + { + f->read(&tag, sizeof(md3tag)); + lilswap(tag.translation, 12); + if(tag.name[0] && iload(name)) { delete group; return NULL; } + return group; + } + + bool loaddefaultparts() + { + const char *pname = parentdir(name); + part &mdl = addpart(); + defformatstring(name1, "packages/models/%s/tris.md3", name); + mdl.meshes = sharemeshes(path(name1)); + if(!mdl.meshes) + { + defformatstring(name2, "packages/models/%s/tris.md3", pname); // try md3 in parent folder (vert sharing) + mdl.meshes = sharemeshes(path(name2)); + if(!mdl.meshes) return false; + } + Texture *tex, *masks; + loadskin(name, pname, tex, masks); + mdl.initskins(tex, masks); + if(tex==notexture) conoutf(CON_ERROR, "could not load model skin for %s", name1); + return true; + } +}; + +vertcommands md3commands; + diff --git a/src/engine/md5.h b/src/engine/md5.h new file mode 100644 index 0000000..0d59586 --- /dev/null +++ b/src/engine/md5.h @@ -0,0 +1,419 @@ +struct md5; + +struct md5joint +{ + vec pos; + quat orient; +}; + +struct md5weight +{ + int joint; + float bias; + vec pos; +}; + +struct md5vert +{ + vec2 tc; + ushort start, count; +}; + +struct md5hierarchy +{ + string name; + int parent, flags, start; +}; + +struct md5 : skelloader +{ + md5(const char *name) : skelloader(name) {} + + static const char *formatname() { return "md5"; } + int type() const { return MDL_MD5; } + + struct md5mesh : skelmesh + { + md5weight *weightinfo; + int numweights; + md5vert *vertinfo; + + md5mesh() : weightinfo(NULL), numweights(0), vertinfo(NULL) + { + } + + ~md5mesh() + { + cleanup(); + } + + void cleanup() + { + DELETEA(weightinfo); + DELETEA(vertinfo); + } + + void buildverts(vector &joints) + { + loopi(numverts) + { + md5vert &v = vertinfo[i]; + vec pos(0, 0, 0); + loopk(v.count) + { + md5weight &w = weightinfo[v.start+k]; + md5joint &j = joints[w.joint]; + vec wpos = j.orient.rotate(w.pos); + wpos.add(j.pos); + wpos.mul(w.bias); + pos.add(wpos); + } + vert &vv = verts[i]; + vv.pos = pos; + vv.tc = v.tc; + + blendcombo c; + int sorted = 0; + loopj(v.count) + { + md5weight &w = weightinfo[v.start+j]; + sorted = c.addweight(sorted, w.bias, w.joint); + } + c.finalize(sorted); + vv.blend = addblendcombo(c); + } + } + + void load(stream *f, char *buf, size_t bufsize) + { + md5weight w; + md5vert v; + tri t; + int index; + + while(f->getline(buf, bufsize) && buf[0]!='}') + { + if(strstr(buf, "// meshes:")) + { + char *start = strchr(buf, ':')+1; + if(*start==' ') start++; + char *end = start + strlen(start)-1; + while(end >= start && isspace(*end)) end--; + name = newstring(start, end+1-start); + } + else if(strstr(buf, "shader")) + { + char *start = strchr(buf, '"'), *end = start ? strchr(start+1, '"') : NULL; + if(start && end) + { + char *texname = newstring(start+1, end-(start+1)); + part *p = loading->parts.last(); + p->initskins(notexture, notexture, group->meshes.length()); + skin &s = p->skins.last(); + s.tex = textureload(makerelpath(dir, texname), 0, true, false); + delete[] texname; + } + } + else if(sscanf(buf, " numverts %d", &numverts)==1) + { + numverts = max(numverts, 0); + if(numverts) + { + vertinfo = new md5vert[numverts]; + verts = new vert[numverts]; + } + } + else if(sscanf(buf, " numtris %d", &numtris)==1) + { + numtris = max(numtris, 0); + if(numtris) tris = new tri[numtris]; + } + else if(sscanf(buf, " numweights %d", &numweights)==1) + { + numweights = max(numweights, 0); + if(numweights) weightinfo = new md5weight[numweights]; + } + else if(sscanf(buf, " vert %d ( %f %f ) %hu %hu", &index, &v.tc.x, &v.tc.y, &v.start, &v.count)==5) + { + if(index>=0 && index=0 && index=0 && index basejoints; + while(f->getline(buf, sizeof(buf))) + { + int tmp; + if(sscanf(buf, " MD5Version %d", &tmp)==1) + { + if(tmp!=10) { delete f; return false; } + } + else if(sscanf(buf, " numJoints %d", &tmp)==1) + { + if(tmp<1) { delete f; return false; } + if(skel->numbones>0) continue; + skel->numbones = tmp; + skel->bones = new boneinfo[skel->numbones]; + } + else if(sscanf(buf, " numMeshes %d", &tmp)==1) + { + if(tmp<1) { delete f; return false; } + } + else if(strstr(buf, "joints {")) + { + string name; + int parent; + md5joint j; + while(f->getline(buf, sizeof(buf)) && buf[0]!='}') + { + char *curbuf = buf, *curname = name; + bool allowspace = false; + while(*curbuf && isspace(*curbuf)) curbuf++; + if(*curbuf == '"') { curbuf++; allowspace = true; } + while(*curbuf && curname < &name[sizeof(name)-1]) + { + char c = *curbuf++; + if(c == '"') break; + if(isspace(c) && !allowspace) break; + *curname++ = c; + } + *curname = '\0'; + if(sscanf(curbuf, " %d ( %f %f %f ) ( %f %f %f )", + &parent, &j.pos.x, &j.pos.y, &j.pos.z, + &j.orient.x, &j.orient.y, &j.orient.z)==7) + { + j.pos.y = -j.pos.y; + j.orient.x = -j.orient.x; + j.orient.z = -j.orient.z; + if(basejoints.length()numbones) + { + if(!skel->bones[basejoints.length()].name) + skel->bones[basejoints.length()].name = newstring(name); + skel->bones[basejoints.length()].parent = parent; + } + j.orient.restorew(); + basejoints.add(j); + } + } + if(basejoints.length()!=skel->numbones) { delete f; return false; } + } + else if(strstr(buf, "mesh {")) + { + md5mesh *m = new md5mesh; + m->group = this; + meshes.add(m); + m->load(f, buf, sizeof(buf)); + if(!m->numtris || !m->numverts) + { + conoutf(CON_WARN, "empty mesh in %s", filename); + meshes.removeobj(m); + delete m; + } + } + } + + if(skel->shared <= 1) + { + skel->linkchildren(); + loopv(basejoints) + { + boneinfo &b = skel->bones[i]; + b.base = dualquat(basejoints[i].orient, basejoints[i].pos); + (b.invbase = b.base).invert(); + } + } + + loopv(meshes) + { + md5mesh &m = *(md5mesh *)meshes[i]; + m.buildverts(basejoints); + if(smooth <= 1) m.smoothnorms(smooth); + else m.buildnorms(); + m.cleanup(); + } + + sortblendcombos(); + + delete f; + return true; + } + + skelanimspec *loadanim(const char *filename) + { + skelanimspec *sa = skel->findskelanim(filename); + if(sa) return sa; + + stream *f = openfile(filename, "r"); + if(!f) return NULL; + + vector hierarchy; + vector basejoints; + int animdatalen = 0, animframes = 0; + float *animdata = NULL; + dualquat *animbones = NULL; + char buf[512]; + while(f->getline(buf, sizeof(buf))) + { + int tmp; + if(sscanf(buf, " MD5Version %d", &tmp)==1) + { + if(tmp!=10) { delete f; return NULL; } + } + else if(sscanf(buf, " numJoints %d", &tmp)==1) + { + if(tmp!=skel->numbones) { delete f; return NULL; } + } + else if(sscanf(buf, " numFrames %d", &animframes)==1) + { + if(animframes<1) { delete f; return NULL; } + } + else if(sscanf(buf, " frameRate %d", &tmp)==1); + else if(sscanf(buf, " numAnimatedComponents %d", &animdatalen)==1) + { + if(animdatalen>0) animdata = new float[animdatalen]; + } + else if(strstr(buf, "bounds {")) + { + while(f->getline(buf, sizeof(buf)) && buf[0]!='}'); + } + else if(strstr(buf, "hierarchy {")) + { + while(f->getline(buf, sizeof(buf)) && buf[0]!='}') + { + md5hierarchy h; + if(sscanf(buf, " %100s %d %d %d", h.name, &h.parent, &h.flags, &h.start)==4) + hierarchy.add(h); + } + } + else if(strstr(buf, "baseframe {")) + { + while(f->getline(buf, sizeof(buf)) && buf[0]!='}') + { + md5joint j; + if(sscanf(buf, " ( %f %f %f ) ( %f %f %f )", &j.pos.x, &j.pos.y, &j.pos.z, &j.orient.x, &j.orient.y, &j.orient.z)==6) + { + j.pos.y = -j.pos.y; + j.orient.x = -j.orient.x; + j.orient.z = -j.orient.z; + j.orient.restorew(); + basejoints.add(j); + } + } + if(basejoints.length()!=skel->numbones) { delete f; if(animdata) delete[] animdata; return NULL; } + animbones = new dualquat[(skel->numframes+animframes)*skel->numbones]; + if(skel->framebones) + { + memcpy(animbones, skel->framebones, skel->numframes*skel->numbones*sizeof(dualquat)); + delete[] skel->framebones; + } + skel->framebones = animbones; + animbones += skel->numframes*skel->numbones; + + sa = &skel->addskelanim(filename); + sa->frame = skel->numframes; + sa->range = animframes; + + skel->numframes += animframes; + } + else if(sscanf(buf, " frame %d", &tmp)==1) + { + for(int numdata = 0; f->getline(buf, sizeof(buf)) && buf[0]!='}';) + { + for(char *src = buf, *next = src; numdata < animdatalen; numdata++, src = next) + { + animdata[numdata] = strtod(src, &next); + if(next <= src) break; + } + } + dualquat *frame = &animbones[tmp*skel->numbones]; + loopv(basejoints) + { + md5hierarchy &h = hierarchy[i]; + md5joint j = basejoints[i]; + if(h.start < animdatalen && h.flags) + { + float *jdata = &animdata[h.start]; + if(h.flags&1) j.pos.x = *jdata++; + if(h.flags&2) j.pos.y = -*jdata++; + if(h.flags&4) j.pos.z = *jdata++; + if(h.flags&8) j.orient.x = -*jdata++; + if(h.flags&16) j.orient.y = *jdata++; + if(h.flags&32) j.orient.z = -*jdata++; + j.orient.restorew(); + } + frame[i] = dualquat(j.orient, j.pos); + if(adjustments.inrange(i)) adjustments[i].adjust(frame[i]); + frame[i].mul(skel->bones[i].invbase); + if(h.parent >= 0) frame[i].mul(skel->bones[h.parent].base, dualquat(frame[i])); + frame[i].fixantipodal(skel->framebones[i]); + } + } + } + + if(animdata) delete[] animdata; + delete f; + + return sa; + } + + bool load(const char *meshfile, float smooth) + { + name = newstring(meshfile); + + if(!loadmesh(meshfile, smooth)) return false; + + return true; + } + }; + + meshgroup *loadmeshes(const char *name, va_list args) + { + md5meshgroup *group = new md5meshgroup; + group->shareskeleton(va_arg(args, char *)); + if(!group->load(name, va_arg(args, double))) { delete group; return NULL; } + return group; + } + + bool loaddefaultparts() + { + skelpart &mdl = addpart(); + mdl.pitchscale = mdl.pitchoffset = mdl.pitchmin = mdl.pitchmax = 0; + adjustments.setsize(0); + const char *fname = name + strlen(name); + do --fname; while(fname >= name && *fname!='/' && *fname!='\\'); + fname++; + defformatstring(meshname, "packages/models/%s/%s.md5mesh", name, fname); + mdl.meshes = sharemeshes(path(meshname), NULL, 2.0); + if(!mdl.meshes) return false; + mdl.initanimparts(); + mdl.initskins(); + defformatstring(animname, "packages/models/%s/%s.md5anim", name, fname); + ((md5meshgroup *)mdl.meshes)->loadanim(path(animname)); + return true; + } +}; + +skelcommands md5commands; + diff --git a/src/engine/menus.cpp b/src/engine/menus.cpp new file mode 100644 index 0000000..f2d8e3b --- /dev/null +++ b/src/engine/menus.cpp @@ -0,0 +1,783 @@ +// menus.cpp: ingame menu system (also used for scores and serverlist) + +#include "engine.h" + +#define GUI_TITLE_COLOR 0xFFDD88 +#define GUI_BUTTON_COLOR 0xFFFFFF +#define GUI_TEXT_COLOR 0xDDFFDD + +static vec menupos; +static int menustart = 0; +static g3d_gui *cgui = NULL; + +VAR(guitabnum, 1, 0, 0); + +struct menu : g3d_callback +{ + char *name, *header; + uint *contents, *init, *onclear; + bool showtab, keeptab; + int menutab; + + menu() : name(NULL), header(NULL), contents(NULL), init(NULL), onclear(NULL), showtab(true), keeptab(false), menutab(1) {} + + void gui(g3d_gui &g, bool firstpass) + { + cgui = &g; + guitabnum = menutab; + cgui->start(menustart, 0.03f, showtab ? &menutab : NULL); + if(showtab) cgui->tab(header ? header : name, GUI_TITLE_COLOR); + execute(contents); + cgui->end(); + cgui = NULL; + guitabnum = 0; + } + + virtual void clear() + { + if(onclear) { freecode(onclear); onclear = NULL; } + } +}; + +struct delayedupdate +{ + enum + { + INT, + FLOAT, + STRING, + ACTION + } type; + ident *id; + union + { + int i; + float f; + char *s; + } val; + delayedupdate() : type(ACTION), id(NULL) { val.s = NULL; } + ~delayedupdate() { if(type == STRING || type == ACTION) DELETEA(val.s); } + + void schedule(const char *s) { type = ACTION; val.s = newstring(s); } + void schedule(ident *var, int i) { type = INT; id = var; val.i = i; } + void schedule(ident *var, float f) { type = FLOAT; id = var; val.f = f; } + void schedule(ident *var, char *s) { type = STRING; id = var; val.s = newstring(s); } + + int getint() const + { + switch(type) + { + case INT: return val.i; + case FLOAT: return int(val.f); + case STRING: return int(strtol(val.s, NULL, 0)); + default: return 0; + } + } + + float getfloat() const + { + switch(type) + { + case INT: return float(val.i); + case FLOAT: return val.f; + case STRING: return float(parsefloat(val.s)); + default: return 0; + } + } + + const char *getstring() const + { + switch(type) + { + case INT: return intstr(val.i); + case FLOAT: return intstr(int(floor(val.f))); + case STRING: return val.s; + default: return ""; + } + } + + void run() + { + if(type == ACTION) { if(val.s) execute(val.s); } + else if(id) switch(id->type) + { + case ID_VAR: setvarchecked(id, getint()); break; + case ID_FVAR: setfvarchecked(id, getfloat()); break; + case ID_SVAR: setsvarchecked(id, getstring()); break; + case ID_ALIAS: alias(id->name, getstring()); break; + } + } +}; + +static hashnameset guis; +static vector guistack; +static vector updatelater; +static bool shouldclearmenu = true, clearlater = false; + +VARP(menudistance, 16, 40, 256); +VARP(menuautoclose, 32, 120, 4096); + +vec menuinfrontofplayer() +{ + vec dir; + vecfromyawpitch(camera1->yaw, 0, 1, 0, dir); + dir.mul(menudistance).add(camera1->o); + dir.z -= player->eyeheight-1; + return dir; +} + +void popgui() +{ + menu *m = guistack.pop(); + m->clear(); +} + +void removegui(menu *m) +{ + loopv(guistack) if(guistack[i]==m) + { + guistack.remove(i); + m->clear(); + return; + } +} + +void pushgui(menu *m, int pos = -1) +{ + if(guistack.empty()) + { + menupos = menuinfrontofplayer(); + g3d_resetcursor(); + } + if(pos < 0) guistack.add(m); + else guistack.insert(pos, m); + if(pos < 0 || pos==guistack.length()-1) + { + if(!m->keeptab) m->menutab = 1; + menustart = totalmillis; + } + if(m->init) execute(m->init); +} + +void restoregui(int pos) +{ + int clear = guistack.length()-pos-1; + loopi(clear) popgui(); + menustart = totalmillis; +} + +void showgui(const char *name) +{ + menu *m = guis.access(name); + if(!m) return; + int pos = guistack.find(m); + if(pos<0) pushgui(m); + else restoregui(pos); +} + +void hidegui(const char *name) +{ + menu *m = guis.access(name); + if(m) removegui(m); +} + +int cleargui(int n) +{ + int clear = guistack.length(); + if(mainmenu && !isconnected(true) && clear > 0 && guistack[0]->name && !strcmp(guistack[0]->name, "main")) + { + clear--; + if(!clear) return 1; + } + if(n>0) clear = min(clear, n); + loopi(clear) popgui(); + if(!guistack.empty()) restoregui(guistack.length()-1); + return clear; +} + +void clearguis(int level = -1) +{ + if(level < 0) level = guistack.length(); + loopvrev(guistack) + { + menu *m = guistack[i]; + if(m->onclear) + { + uint *action = m->onclear; + m->onclear = NULL; + execute(action); + freecode(action); + } + } + cleargui(level); +} + +void guionclear(char *action) +{ + if(guistack.empty()) return; + menu *m = guistack.last(); + if(m->onclear) { freecode(m->onclear); m->onclear = NULL; } + if(action[0]) m->onclear = compilecode(action); +} + +void guistayopen(uint *contents) +{ + bool oldclearmenu = shouldclearmenu; + shouldclearmenu = false; + execute(contents); + shouldclearmenu = oldclearmenu; +} + +void guinoautotab(uint *contents) +{ + if(!cgui) return; + bool oldval = cgui->allowautotab(false); + execute(contents); + cgui->allowautotab(oldval); +} + +void guimerge(uint *contents) +{ + if(!cgui) return; + bool oldval = cgui->mergehits(true); + execute(contents); + cgui->mergehits(oldval); +} + +//@DOC name and icon are optional +void guibutton(char *name, char *action, char *icon) +{ + if(!cgui) return; + bool hideicon = !strcmp(icon, "0"); + int ret = cgui->button(name, GUI_BUTTON_COLOR, hideicon ? NULL : (icon[0] ? icon : (strstr(action, "showgui") ? "menu" : "action"))); + if(ret&G3D_UP) + { + updatelater.add().schedule(action[0] ? action : name); + if(shouldclearmenu) clearlater = true; + } + else if(ret&G3D_ROLLOVER) + { + alias("guirollovername", name); + alias("guirolloveraction", action); + } +} + +void guiimage(char *path, char *action, float *scale, int *overlaid, char *alt, char *title) +{ + if(!cgui) return; + Texture *t = textureload(path, 0, true, false); + if(t==notexture) + { + if(alt[0]) t = textureload(alt, 0, true, false); + if(t==notexture) return; + } + int ret = cgui->image(t, *scale, *overlaid!=0 ? title : NULL); + if(ret&G3D_UP) + { + if(*action) + { + updatelater.add().schedule(action); + if(shouldclearmenu) clearlater = true; + } + } + else if(ret&G3D_ROLLOVER) + { + alias("guirolloverimgpath", path); + alias("guirolloverimgaction", action); + } +} + +void guicolor(int *color) +{ + if(cgui) + { + defformatstring(desc, "0x%06X", *color); + cgui->text(desc, *color, NULL); + } +} + +void guitextbox(char *text, int *width, int *height, int *color) +{ + if(cgui && text[0]) cgui->textbox(text, *width ? *width : 12, *height ? *height : 1, *color ? *color : 0xFFFFFF); +} + +void guitext(char *name, char *icon) +{ + bool hideicon = !strcmp(icon, "0"); + if(cgui) cgui->text(name, !hideicon && icon[0] ? GUI_BUTTON_COLOR : GUI_TEXT_COLOR, hideicon ? NULL : (icon[0] ? icon : "info")); +} + +void guititle(char *name) +{ + if(cgui) cgui->title(name, GUI_TITLE_COLOR); +} + +void guitab(char *name) +{ + if(cgui) cgui->tab(name, GUI_TITLE_COLOR); +} + +void guibar() +{ + if(cgui) cgui->separator(); +} + +void guistrut(float *strut, int *alt) +{ + if(cgui) + { + if(*alt) cgui->strut(*strut); else cgui->space(*strut); + } +} + +void guispring(int *weight) +{ + if(cgui) cgui->spring(max(*weight, 1)); +} + +void guicolumn(int *col) +{ + if(cgui) cgui->column(*col); +} + +template static void updateval(char *var, T val, char *onchange) +{ + ident *id = writeident(var); + updatelater.add().schedule(id, val); + if(onchange[0]) updatelater.add().schedule(onchange); +} + +static int getval(char *var) +{ + ident *id = readident(var); + if(!id) return 0; + switch(id->type) + { + case ID_VAR: return *id->storage.i; + case ID_FVAR: return int(*id->storage.f); + case ID_SVAR: return parseint(*id->storage.s); + case ID_ALIAS: return id->getint(); + default: return 0; + } +} + +static float getfval(char *var) +{ + ident *id = readident(var); + if(!id) return 0; + switch(id->type) + { + case ID_VAR: return *id->storage.i; + case ID_FVAR: return *id->storage.f; + case ID_SVAR: return parsefloat(*id->storage.s); + case ID_ALIAS: return id->getfloat(); + default: return 0; + } +} + +static const char *getsval(char *var) +{ + ident *id = readident(var); + if(!id) return ""; + switch(id->type) + { + case ID_VAR: return intstr(*id->storage.i); + case ID_FVAR: return floatstr(*id->storage.f); + case ID_SVAR: return *id->storage.s; + case ID_ALIAS: return id->getstr(); + default: return ""; + } +} + +void guislider(char *var, int *min, int *max, char *onchange) +{ + if(!cgui) return; + int oldval = getval(var), val = oldval, vmin = *max > INT_MIN ? *min : getvarmin(var), vmax = *max > INT_MIN ? *max : getvarmax(var); + cgui->slider(val, vmin, vmax, GUI_TITLE_COLOR); + if(val != oldval) updateval(var, val, onchange); +} + +void guilistslider(char *var, char *list, char *onchange) +{ + if(!cgui) return; + vector vals; + list += strspn(list, "\n\t "); + while(*list) + { + vals.add(parseint(list)); + list += strcspn(list, "\n\t \0"); + list += strspn(list, "\n\t "); + } + if(vals.empty()) return; + int val = getval(var), oldoffset = vals.length()-1, offset = oldoffset; + loopv(vals) if(val <= vals[i]) { oldoffset = offset = i; break; } + cgui->slider(offset, 0, vals.length()-1, GUI_TITLE_COLOR, intstr(val)); + if(offset != oldoffset) updateval(var, vals[offset], onchange); +} + +void guinameslider(char *var, char *names, char *list, char *onchange) +{ + if(!cgui) return; + vector vals; + list += strspn(list, "\n\t "); + while(*list) + { + vals.add(parseint(list)); + list += strcspn(list, "\n\t \0"); + list += strspn(list, "\n\t "); + } + if(vals.empty()) return; + int val = getval(var), oldoffset = vals.length()-1, offset = oldoffset; + loopv(vals) if(val <= vals[i]) { oldoffset = offset = i; break; } + char *label = indexlist(names, offset); + cgui->slider(offset, 0, vals.length()-1, GUI_TITLE_COLOR, label); + if(offset != oldoffset) updateval(var, vals[offset], onchange); + delete[] label; +} + +void guicheckbox(char *name, char *var, float *on, float *off, char *onchange) +{ + bool enabled = getfval(var)!=*off; + if(cgui && cgui->button(name, GUI_BUTTON_COLOR, enabled ? "checkbox_on" : "checkbox_off")&G3D_UP) + { + updateval(var, enabled ? *off : (*on || *off ? *on : 1.0f), onchange); + } +} + +void guiradio(char *name, char *var, float *n, char *onchange) +{ + bool enabled = getfval(var)==*n; + if(cgui && cgui->button(name, GUI_BUTTON_COLOR, enabled ? "radio_on" : "radio_off")&G3D_UP) + { + if(!enabled) updateval(var, *n, onchange); + } +} + +void guibitfield(char *name, char *var, int *mask, char *onchange) +{ + int val = getval(var); + bool enabled = (val & *mask) != 0; + if(cgui && cgui->button(name, GUI_BUTTON_COLOR, enabled ? "checkbox_on" : "checkbox_off")&G3D_UP) + { + updateval(var, enabled ? val & ~*mask : val | *mask, onchange); + } +} + +//-ve length indicates a wrapped text field of any (approx 260 chars) length, |length| is the field width +void guifield(char *var, int *maxlength, char *onchange) +{ + if(!cgui) return; + const char *initval = getsval(var); + char *result = cgui->field(var, GUI_BUTTON_COLOR, *maxlength ? *maxlength : 12, 0, initval); + if(result) updateval(var, result, onchange); +} + +//-ve maxlength indicates a wrapped text field of any (approx 260 chars) length, |maxlength| is the field width +void guieditor(char *name, int *maxlength, int *height, int *mode) +{ + if(!cgui) return; + cgui->field(name, GUI_BUTTON_COLOR, *maxlength ? *maxlength : 12, *height, NULL, *mode<=0 ? EDITORFOREVER : *mode); + //returns a non-NULL pointer (the currentline) when the user commits, could then manipulate via text* commands +} + +//-ve length indicates a wrapped text field of any (approx 260 chars) length, |length| is the field width +void guikeyfield(char *var, int *maxlength, char *onchange) +{ + if(!cgui) return; + const char *initval = getsval(var); + char *result = cgui->keyfield(var, GUI_BUTTON_COLOR, *maxlength ? *maxlength : -8, 0, initval); + if(result) updateval(var, result, onchange); +} + +//use text to do more... + + +void guilist(uint *contents) +{ + if(!cgui) return; + cgui->pushlist(); + execute(contents); + cgui->poplist(); +} + +void guialign(int *align, uint *contents) +{ + if(!cgui) return; + cgui->pushlist(); + if(*align >= 0) cgui->spring(); + execute(contents); + if(*align == 0) cgui->spring(); + cgui->poplist(); +} + +void newgui(char *name, char *contents, char *header, char *init) +{ + menu *m = guis.access(name); + if(!m) + { + name = newstring(name); + m = &guis[name]; + m->name = name; + } + else + { + DELETEA(m->header); + freecode(m->contents); + freecode(m->init); + } + if(header && header[0]) + { + char *end = NULL; + int val = strtol(header, &end, 0); + if(end && !*end) + { + m->header = NULL; + m->showtab = val != 0; + } + else + { + m->header = newstring(header); + m->showtab = true; + } + } + else + { + m->header = NULL; + m->showtab = true; + } + m->contents = compilecode(contents); + m->init = init && init[0] ? compilecode(init) : NULL; +} + +menu *guiserversmenu = NULL; + +void guiservers(uint *header, int *pagemin, int *pagemax) +{ + extern const char *showservers(g3d_gui *cgui, uint *header, int pagemin, int pagemax); + if(cgui) + { + const char *command = showservers(cgui, header, *pagemin, *pagemax > 0 ? *pagemax : INT_MAX); + if(command) + { + updatelater.add().schedule(command); + if(shouldclearmenu) clearlater = true; + guiserversmenu = clearlater || guistack.empty() ? NULL : guistack.last(); + } + } +} + +void notifywelcome() +{ + if(guiserversmenu) + { + if(guistack.length() && guistack.last() == guiserversmenu) clearguis(); + guiserversmenu = NULL; + } +} + +COMMAND(newgui, "ssss"); +COMMAND(guibutton, "sss"); +COMMAND(guitext, "ss"); +COMMAND(guiservers, "eii"); +ICOMMAND(cleargui, "i", (int *n), intret(cleargui(*n))); +COMMAND(showgui, "s"); +COMMAND(hidegui, "s"); +COMMAND(guionclear, "s"); +COMMAND(guistayopen, "e"); +COMMAND(guinoautotab, "e"); +COMMAND(guimerge, "e"); +ICOMMAND(guikeeptab, "b", (int *keeptab), if(guistack.length()) guistack.last()->keeptab = *keeptab!=0); +COMMAND(guilist, "e"); +COMMAND(guialign, "ie"); +COMMAND(guititle, "s"); +COMMAND(guibar,""); +COMMAND(guistrut,"fi"); +COMMAND(guispring, "i"); +COMMAND(guicolumn, "i"); +COMMAND(guiimage,"ssfiss"); +COMMAND(guislider,"sbbs"); +COMMAND(guilistslider, "sss"); +COMMAND(guinameslider, "ssss"); +COMMAND(guiradio,"ssfs"); +COMMAND(guibitfield, "ssis"); +COMMAND(guicheckbox, "ssffs"); +COMMAND(guitab, "s"); +COMMAND(guifield, "sis"); +COMMAND(guikeyfield, "sis"); +COMMAND(guieditor, "siii"); +COMMAND(guicolor, "i"); +COMMAND(guitextbox, "siii"); + +void guiplayerpreview(int *model, int *team, int *weap, char *action, float *scale, int *overlaid, char *title) +{ + if(!cgui) return; + int ret = cgui->playerpreview(*model, *team, *weap, *scale, *overlaid!=0 ? title : NULL); + if(ret&G3D_UP) + { + if(*action) + { + updatelater.add().schedule(action); + if(shouldclearmenu) clearlater = true; + } + } +} +COMMAND(guiplayerpreview, "iiisfis"); + +void guimodelpreview(char *model, char *animspec, char *action, float *scale, int *overlaid, char *title, int *throttle) +{ + if(!cgui) return; + int anim = ANIM_ALL; + if(animspec[0]) + { + if(isdigit(animspec[0])) + { + anim = parseint(animspec); + if(anim >= 0) anim %= ANIM_INDEX; + else anim = ANIM_ALL; + } + else + { + vector anims; + findanims(animspec, anims); + if(anims.length()) anim = anims[0]; + } + } + int ret = cgui->modelpreview(model, anim|ANIM_LOOP, *scale, *overlaid!=0 ? title : NULL, *throttle!=0); + if(ret&G3D_UP) + { + if(*action) + { + updatelater.add().schedule(action); + if(shouldclearmenu) clearlater = true; + } + } + else if(ret&G3D_ROLLOVER) + { + alias("guirolloverpreviewname", model); + alias("guirolloverpreviewaction", action); + } +} +COMMAND(guimodelpreview, "sssfisi"); + +void guiprefabpreview(char *prefab, int *color, char *action, float *scale, int *overlaid, char *title, int *throttle) +{ + if(!cgui) return; + int ret = cgui->prefabpreview(prefab, vec::hexcolor(*color), *scale, *overlaid!=0 ? title : NULL, *throttle!=0); + if(ret&G3D_UP) + { + if(*action) + { + updatelater.add().schedule(action); + if(shouldclearmenu) clearlater = true; + } + } + else if(ret&G3D_ROLLOVER) + { + alias("guirolloverpreviewname", prefab); + alias("guirolloverpreviewaction", action); + } +} +COMMAND(guiprefabpreview, "sisfisi"); + +struct change +{ + int type; + const char *desc; + + change() {} + change(int type, const char *desc) : type(type), desc(desc) {} +}; +static vector needsapply; + +static struct applymenu : menu +{ + void gui(g3d_gui &g, bool firstpass) + { + if(guistack.empty()) return; + g.start(menustart, 0.03f); + g.text("the following settings have changed:", GUI_TEXT_COLOR, "info"); + loopv(needsapply) g.text(needsapply[i].desc, GUI_TEXT_COLOR, "info"); + g.separator(); + g.text("apply changes now?", GUI_TEXT_COLOR, "info"); + if(g.button("yes", GUI_BUTTON_COLOR, "action")&G3D_UP) + { + int changetypes = 0; + loopv(needsapply) changetypes |= needsapply[i].type; + if(changetypes&CHANGE_GFX) updatelater.add().schedule("resetgl"); + if(changetypes&CHANGE_SOUND) updatelater.add().schedule("resetsound"); + clearlater = true; + } + if(g.button("no", GUI_BUTTON_COLOR, "action")&G3D_UP) + clearlater = true; + g.end(); + } + + void clear() + { + menu::clear(); + needsapply.shrink(0); + } +} applymenu; + +VARP(applydialog, 0, 1, 1); + +static bool processingmenu = false; + +void addchange(const char *desc, int type) +{ + if(!applydialog) return; + loopv(needsapply) if(!strcmp(needsapply[i].desc, desc)) return; + needsapply.add(change(type, desc)); + if(needsapply.length() && guistack.find(&applymenu) < 0) + pushgui(&applymenu, processingmenu ? max(guistack.length()-1, 0) : -1); +} + +void clearchanges(int type) +{ + loopv(needsapply) + { + if(needsapply[i].type&type) + { + needsapply[i].type &= ~type; + if(!needsapply[i].type) needsapply.remove(i--); + } + } + if(needsapply.empty()) removegui(&applymenu); +} + +void menuprocess() +{ + processingmenu = true; + int wasmain = mainmenu, level = guistack.length(); + loopv(updatelater) updatelater[i].run(); + updatelater.shrink(0); + if(wasmain > mainmenu || clearlater) + { + if(wasmain > mainmenu || level==guistack.length()) clearguis(level); + clearlater = false; + } + if(mainmenu && !isconnected(true) && guistack.empty()) showgui("main"); + processingmenu = false; +} + +VAR(mainmenu, 1, 1, 0); + +void clearmainmenu() +{ + if(mainmenu && isconnected()) + { + mainmenu = 0; + if(!processingmenu) cleargui(); + } +} + +void g3d_mainmenu() +{ + if(!guistack.empty()) + { + extern int usegui2d; + if(!mainmenu && !usegui2d && camera1->o.dist(menupos) > menuautoclose) cleargui(); + else g3d_addgui(guistack.last(), menupos, GUI_2D | GUI_FOLLOW); + } +} + diff --git a/src/engine/model.h b/src/engine/model.h new file mode 100644 index 0000000..97264bd --- /dev/null +++ b/src/engine/model.h @@ -0,0 +1,90 @@ +enum { MDL_MD2 = 0, MDL_MD3, MDL_MD5, MDL_OBJ, MDL_SMD, MDL_IQM, NUMMODELTYPES }; + +struct model +{ + char *name; + float spinyaw, spinpitch, offsetyaw, offsetpitch; + bool collide, ellipsecollide, shadow, alphadepth, depthoffset; + float scale; + vec translate; + BIH *bih; + vec bbcenter, bbradius, bbextend, collidecenter, collideradius; + float rejectradius, eyeheight, collidexyradius, collideheight; + int batch; + + model(const char *name) : name(name ? newstring(name) : NULL), spinyaw(0), spinpitch(0), offsetyaw(0), offsetpitch(0), collide(true), ellipsecollide(false), shadow(true), alphadepth(true), depthoffset(false), scale(1.0f), translate(0, 0, 0), bih(0), bbcenter(0, 0, 0), bbradius(-1, -1, -1), bbextend(0, 0, 0), collidecenter(0, 0, 0), collideradius(-1, -1, -1), rejectradius(-1), eyeheight(0.9f), collidexyradius(0), collideheight(0), batch(-1) {} + virtual ~model() { DELETEA(name); DELETEP(bih); } + virtual void calcbb(vec ¢er, vec &radius) = 0; + virtual void render(int anim, int basetime, int basetime2, const vec &o, float yaw, float pitch, dynent *d, modelattach *a = NULL, const vec &color = vec(0, 0, 0), const vec &dir = vec(0, 0, 0), float transparent = 1) = 0; + virtual bool load() = 0; + virtual int type() const = 0; + virtual BIH *setBIH() { return 0; } + virtual bool envmapped() { return false; } + virtual bool skeletal() const { return false; } + + virtual void setshader(Shader *shader) {} + virtual void setenvmap(float envmapmin, float envmapmax, Texture *envmap) {} + virtual void setspec(float spec) {} + virtual void setambient(float ambient) {} + virtual void setglow(float glow, float glowdelta, float glowpulse) {} + virtual void setglare(float specglare, float glowglare) {} + virtual void setalphatest(float alpha) {} + virtual void setalphablend(bool blend) {} + virtual void setfullbright(float fullbright) {} + virtual void setcullface(bool cullface) {} + + virtual void preloadBIH() { if(!bih) setBIH(); } + virtual void preloadshaders(bool force = false) {} + virtual void preloadmeshes() {} + virtual void cleanup() {} + + virtual void startrender() {} + virtual void endrender() {} + + void boundbox(vec ¢er, vec &radius) + { + if(bbradius.x < 0) + { + calcbb(bbcenter, bbradius); + bbradius.add(bbextend); + } + center = bbcenter; + radius = bbradius; + } + + float collisionbox(vec ¢er, vec &radius) + { + if(collideradius.x < 0) + { + boundbox(collidecenter, collideradius); + if(collidexyradius) + { + collidecenter.x = collidecenter.y = 0; + collideradius.x = collideradius.y = collidexyradius; + } + if(collideheight) + { + collidecenter.z = collideradius.z = collideheight/2; + } + rejectradius = vec(collidecenter).abs().add(collideradius).magnitude(); + } + center = collidecenter; + radius = collideradius; + return rejectradius; + } + + float boundsphere(vec ¢er) + { + vec radius; + boundbox(center, radius); + return radius.magnitude(); + } + + float above() + { + vec center, radius; + boundbox(center, radius); + return center.z+radius.z; + } +}; + diff --git a/src/engine/movie.cpp b/src/engine/movie.cpp new file mode 100644 index 0000000..25cb491 --- /dev/null +++ b/src/engine/movie.cpp @@ -0,0 +1,1157 @@ +// Feedback on playing videos: +// quicktime - ok +// vlc - ok +// xine - ok +// mplayer - ok +// totem - ok +// avidemux - ok - 3Apr09-RockKeyman:had to swap UV channels as it showed up blue +// kino - ok + +#include "engine.h" +#include "SDL_mixer.h" + +VAR(dbgmovie, 0, 0, 1); + +struct aviindexentry +{ + int frame, type, size; + uint offset; + + aviindexentry() {} + aviindexentry(int frame, int type, int size, uint offset) : frame(frame), type(type), size(size), offset(offset) {} +}; + +struct avisegmentinfo +{ + stream::offset offset, videoindexoffset, soundindexoffset; + int firstindex; + uint videoindexsize, soundindexsize, indexframes, videoframes, soundframes; + + avisegmentinfo() {} + avisegmentinfo(stream::offset offset, int firstindex) : offset(offset), videoindexoffset(0), soundindexoffset(0), firstindex(firstindex), videoindexsize(0), soundindexsize(0), indexframes(0), videoframes(0), soundframes(0) {} +}; + +struct aviwriter +{ + stream *f; + uchar *yuv; + uint videoframes; + stream::offset totalsize; + const uint videow, videoh, videofps; + string filename; + + int soundfrequency, soundchannels; + Uint16 soundformat; + + vector index; + vector segments; + + stream::offset fileframesoffset, fileextframesoffset, filevideooffset, filesoundoffset, superindexvideooffset, superindexsoundoffset; + + enum { MAX_CHUNK_DEPTH = 16, MAX_SUPER_INDEX = 1024 }; + stream::offset chunkoffsets[MAX_CHUNK_DEPTH]; + int chunkdepth; + + aviindexentry &addindex(int frame, int type, int size) + { + avisegmentinfo &seg = segments.last(); + int i = index.length(); + while(--i >= seg.firstindex) + { + aviindexentry &e = index[i]; + if(frame > e.frame || (frame == e.frame && type <= e.type)) break; + } + return index.insert(i + 1, aviindexentry(frame, type, size, uint(totalsize - chunkoffsets[chunkdepth]))); + } + + double filespaceguess() + { + return double(totalsize); + } + + void startchunk(const char *fcc, uint size = 0) + { + f->write(fcc, 4); + f->putlil(size); + totalsize += 4 + 4; + chunkoffsets[++chunkdepth] = totalsize; + totalsize += size; + } + + void listchunk(const char *fcc, const char *lfcc) + { + startchunk(fcc); + f->write(lfcc, 4); + totalsize += 4; + } + + void endchunk() + { + ASSERT(chunkdepth >= 0); + --chunkdepth; + } + + void endlistchunk() + { + ASSERT(chunkdepth >= 0); + int size = int(totalsize - chunkoffsets[chunkdepth]); + f->seek(-4 - size, SEEK_CUR); + f->putlil(size); + f->seek(0, SEEK_END); + if(size & 1) { f->putchar(0x00); totalsize++; } + endchunk(); + } + + void writechunk(const char *fcc, const void *data, uint len) // simplify startchunk()/endchunk() to avoid f->seek() + { + f->write(fcc, 4); + f->putlil(len); + f->write(data, len); + totalsize += 4 + 4 + len; + if(len & 1) { f->putchar(0x00); totalsize++; } + } + + void close() + { + if(!f) return; + flushsegment(); + + uint soundindexes = 0, videoindexes = 0, soundframes = 0, videoframes = 0, indexframes = 0; + loopv(segments) + { + avisegmentinfo &seg = segments[i]; + if(seg.soundindexsize) soundindexes++; + videoindexes++; + soundframes += seg.soundframes; + videoframes += seg.videoframes; + indexframes += seg.indexframes; + } + if(dbgmovie) conoutf(CON_DEBUG, "fileframes: sound=%d, video=%d+%d(dups)\n", soundframes, videoframes, indexframes-videoframes); + f->seek(fileframesoffset, SEEK_SET); + f->putlil(segments[0].indexframes); + f->seek(filevideooffset, SEEK_SET); + f->putlil(segments[0].videoframes); + if(segments[0].soundframes > 0) + { + f->seek(filesoundoffset, SEEK_SET); + f->putlil(segments[0].soundframes); + } + f->seek(fileextframesoffset, SEEK_SET); + f->putlil(indexframes); // total video frames + + f->seek(superindexvideooffset + 2 + 2, SEEK_SET); + f->putlil(videoindexes); + f->seek(superindexvideooffset + 2 + 2 + 4 + 4 + 4 + 4 + 4, SEEK_SET); + loopv(segments) + { + avisegmentinfo &seg = segments[i]; + f->putlil(seg.videoindexoffset&stream::offset(0xFFFFFFFFU)); + f->putlil(seg.videoindexoffset>>32); + f->putlil(seg.videoindexsize); + f->putlil(seg.indexframes); + } + + if(soundindexes > 0) + { + f->seek(superindexsoundoffset + 2 + 2, SEEK_SET); + f->putlil(soundindexes); + f->seek(superindexsoundoffset + 2 + 2 + 4 + 4 + 4 + 4 + 4, SEEK_SET); + loopv(segments) + { + avisegmentinfo &seg = segments[i]; + if(!seg.soundindexsize) continue; + f->putlil(seg.soundindexoffset&stream::offset(0xFFFFFFFFU)); + f->putlil(seg.soundindexoffset>>32); + f->putlil(seg.soundindexsize); + f->putlil(seg.soundframes); + } + } + + f->seek(0, SEEK_END); + + DELETEP(f); + } + + aviwriter(const char *name, uint w, uint h, uint fps, bool sound) : f(NULL), yuv(NULL), videoframes(0), totalsize(0), videow(w&~1), videoh(h&~1), videofps(fps), soundfrequency(0),soundchannels(0),soundformat(0) + { + copystring(filename, name); + path(filename); + if(!strrchr(filename, '.')) concatstring(filename, ".avi"); + + extern bool nosound; // sound.cpp + if(sound && !nosound) + { + Mix_QuerySpec(&soundfrequency, &soundformat, &soundchannels); + const char *desc; + switch(soundformat) + { + case AUDIO_U8: desc = "u8"; break; + case AUDIO_S8: desc = "s8"; break; + case AUDIO_U16LSB: desc = "u16l"; break; + case AUDIO_U16MSB: desc = "u16b"; break; + case AUDIO_S16LSB: desc = "s16l"; break; + case AUDIO_S16MSB: desc = "s16b"; break; + default: desc = "unkn"; + } + if(dbgmovie) conoutf(CON_DEBUG, "soundspec: %dhz %s x %d", soundfrequency, desc, soundchannels); + } + } + + ~aviwriter() + { + close(); + if(yuv) delete [] yuv; + } + + bool open() + { + f = openfile(filename, "wb"); + if(!f) return false; + + chunkdepth = -1; + + listchunk("RIFF", "AVI "); + + listchunk("LIST", "hdrl"); + + startchunk("avih", 56); + f->putlil(1000000 / videofps); // microsecsperframe + f->putlil(0); // maxbytespersec + f->putlil(0); // reserved + f->putlil(0x10 | 0x20); // flags - hasindex|mustuseindex + fileframesoffset = f->tell(); + f->putlil(0); // totalvideoframes + f->putlil(0); // initialframes + f->putlil(soundfrequency > 0 ? 2 : 1); // streams + f->putlil(0); // buffersize + f->putlil(videow); // video width + f->putlil(videoh); // video height + loopi(4) f->putlil(0); // reserved + endchunk(); // avih + + listchunk("LIST", "strl"); + + startchunk("strh", 56); + f->write("vids", 4); // fcctype + f->write("I420", 4); // fcchandler + f->putlil(0); // flags + f->putlil(0); // priority + f->putlil(0); // initialframes + f->putlil(1); // scale + f->putlil(videofps); // rate + f->putlil(0); // start + filevideooffset = f->tell(); + f->putlil(0); // length + f->putlil(videow*videoh*3/2); // suggested buffersize + f->putlil(0); // quality + f->putlil(0); // samplesize + f->putlil(0); // frame left + f->putlil(0); // frame top + f->putlil(videow); // frame right + f->putlil(videoh); // frame bottom + endchunk(); // strh + + startchunk("strf", 40); + f->putlil(40); //headersize + f->putlil(videow); // width + f->putlil(videoh); // height + f->putlil(3); // planes + f->putlil(12); // bitcount + f->write("I420", 4); // compression + f->putlil(videow*videoh*3/2); // imagesize + f->putlil(0); // xres + f->putlil(0); // yres; + f->putlil(0); // colorsused + f->putlil(0); // colorsrequired + endchunk(); // strf + + startchunk("indx", 24 + 16*MAX_SUPER_INDEX); + superindexvideooffset = f->tell(); + f->putlil(4); // longs per entry + f->putlil(0); // index of indexes + f->putlil(0); // entries in use + f->write("00dc", 4); // chunk id + f->putlil(0); // reserved 1 + f->putlil(0); // reserved 2 + f->putlil(0); // reserved 3 + loopi(MAX_SUPER_INDEX) + { + f->putlil(0); // offset low + f->putlil(0); // offset high + f->putlil(0); // size + f->putlil(0); // duration + } + endchunk(); // indx + + startchunk("vprp", 68); + f->putlil(0); // video format token + f->putlil(0); // video standard + f->putlil(videofps); // vertical refresh rate + f->putlil(videow); // horizontal total + f->putlil(videoh); // vertical total + int gcd = screenw, rem = screenh; + while(rem > 0) { gcd %= rem; swap(gcd, rem); } + f->putlil(screenh/gcd); // aspect denominator + f->putlil(screenw/gcd); // aspect numerator + f->putlil(videow); // frame width + f->putlil(videoh); // frame height + f->putlil(1); // fields per frame + f->putlil(videoh); // compressed bitmap height + f->putlil(videow); // compressed bitmap width + f->putlil(videoh); // valid bitmap height + f->putlil(videow); // valid bitmap width + f->putlil(0); // valid bitmap x offset + f->putlil(0); // valid bitmap y offset + f->putlil(0); // video x offset + f->putlil(0); // video y start + endchunk(); // vprp + + endlistchunk(); // LIST strl + + if(soundfrequency > 0) + { + const int bps = (soundformat==AUDIO_U8 || soundformat == AUDIO_S8) ? 1 : 2; + + listchunk("LIST", "strl"); + + startchunk("strh", 56); + f->write("auds", 4); // fcctype + f->putlil(1); // fcchandler - normally 4cc, but audio is a special case + f->putlil(0); // flags + f->putlil(0); // priority + f->putlil(0); // initialframes + f->putlil(1); // scale + f->putlil(soundfrequency); // rate + f->putlil(0); // start + filesoundoffset = f->tell(); + f->putlil(0); // length + f->putlil(soundfrequency*bps*soundchannels/2); // suggested buffer size (this is a half second) + f->putlil(0); // quality + f->putlil(bps*soundchannels); // samplesize + f->putlil(0); // frame left + f->putlil(0); // frame top + f->putlil(0); // frame right + f->putlil(0); // frame bottom + endchunk(); // strh + + startchunk("strf", 18); + f->putlil(1); // format (uncompressed PCM) + f->putlil(soundchannels); // channels + f->putlil(soundfrequency); // sampleframes per second + f->putlil(soundfrequency*bps*soundchannels); // average bytes per second + f->putlil(bps*soundchannels); // block align <-- guess + f->putlil(bps*8); // bits per sample + f->putlil(0); // size + endchunk(); //strf + + startchunk("indx", 24 + 16*MAX_SUPER_INDEX); + superindexsoundoffset = f->tell(); + f->putlil(4); // longs per entry + f->putlil(0); // index of indexes + f->putlil(0); // entries in use + f->write("01wb", 4); // chunk id + f->putlil(0); // reserved 1 + f->putlil(0); // reserved 2 + f->putlil(0); // reserved 3 + loopi(MAX_SUPER_INDEX) + { + f->putlil(0); // offset low + f->putlil(0); // offset high + f->putlil(0); // size + f->putlil(0); // duration + } + endchunk(); // indx + + endlistchunk(); // LIST strl + } + + listchunk("LIST", "odml"); + startchunk("dmlh", 4); + fileextframesoffset = f->tell(); + f->putlil(0); + endchunk(); // dmlh + endlistchunk(); // LIST odml + + listchunk("LIST", "INFO"); + const char *software = "Cube 2: Sauerbraten"; + writechunk("ISFT", software, strlen(software)+1); + endlistchunk(); // LIST INFO + + endlistchunk(); // LIST hdrl + + nextsegment(); + + return true; + } + + static inline void boxsample(const uchar *src, const uint stride, + const uint area, const uint w, uint h, + const uint xlow, const uint xhigh, const uint ylow, const uint yhigh, + uint &bdst, uint &gdst, uint &rdst) + { + const uchar *end = &src[w<<2]; + uint bt = 0, gt = 0, rt = 0; + for(const uchar *cur = &src[4]; cur < end; cur += 4) + { + bt += cur[0]; + gt += cur[1]; + rt += cur[2]; + } + bt = ylow*(bt + ((src[0]*xlow + end[0]*xhigh)>>12)); + gt = ylow*(gt + ((src[1]*xlow + end[1]*xhigh)>>12)); + rt = ylow*(rt + ((src[2]*xlow + end[2]*xhigh)>>12)); + if(h) + { + for(src += stride, end += stride; --h; src += stride, end += stride) + { + uint b = 0, g = 0, r = 0; + for(const uchar *cur = &src[4]; cur < end; cur += 4) + { + b += cur[0]; + g += cur[1]; + r += cur[2]; + } + bt += (b<<12) + src[0]*xlow + end[0]*xhigh; + gt += (g<<12) + src[1]*xlow + end[1]*xhigh; + rt += (r<<12) + src[2]*xlow + end[2]*xhigh; + } + uint b = 0, g = 0, r = 0; + for(const uchar *cur = &src[4]; cur < end; cur += 4) + { + b += cur[0]; + g += cur[1]; + r += cur[2]; + } + bt += yhigh*(b + ((src[0]*xlow + end[0]*xhigh)>>12)); + gt += yhigh*(g + ((src[1]*xlow + end[1]*xhigh)>>12)); + rt += yhigh*(r + ((src[2]*xlow + end[2]*xhigh)>>12)); + } + bdst = (bt*area)>>24; + gdst = (gt*area)>>24; + rdst = (rt*area)>>24; + } + + void scaleyuv(const uchar *pixels, uint srcw, uint srch) + { + const int flip = -1; + const uint planesize = videow * videoh; + if(!yuv) yuv = new uchar[(planesize*3)/2]; + uchar *yplane = yuv, *uplane = yuv + planesize, *vplane = yuv + planesize + planesize/4; + const int ystride = flip*int(videow), uvstride = flip*int(videow)/2; + if(flip < 0) { yplane -= int(videoh-1)*ystride; uplane -= int(videoh/2-1)*uvstride; vplane -= int(videoh/2-1)*uvstride; } + + const uint stride = srcw<<2; + srcw &= ~1; + srch &= ~1; + const uint wfrac = (srcw<<12)/videow, hfrac = (srch<<12)/videoh, + area = ((ullong)planesize<<12)/(srcw*srch + 1), + dw = videow*wfrac, dh = videoh*hfrac; + + for(uint y = 0; y < dh;) + { + uint yn = y + hfrac - 1, yi = y>>12, h = (yn>>12) - yi, ylow = ((yn|(-int(h)>>24))&0xFFFU) + 1 - (y&0xFFFU), yhigh = (yn&0xFFFU) + 1; + y += hfrac; + uint y2n = y + hfrac - 1, y2i = y>>12, h2 = (y2n>>12) - y2i, y2low = ((y2n|(-int(h2)>>24))&0xFFFU) + 1 - (y&0xFFFU), y2high = (y2n&0xFFFU) + 1; + y += hfrac; + + const uchar *src = &pixels[yi*stride], *src2 = &pixels[y2i*stride]; + uchar *ydst = yplane, *ydst2 = yplane + ystride, *udst = uplane, *vdst = vplane; + for(uint x = 0; x < dw;) + { + uint xn = x + wfrac - 1, xi = x>>12, w = (xn>>12) - xi, xlow = ((w+0xFFFU)&0x1000U) - (x&0xFFFU), xhigh = (xn&0xFFFU) + 1; + x += wfrac; + uint x2n = x + wfrac - 1, x2i = x>>12, w2 = (x2n>>12) - x2i, x2low = ((w2+0xFFFU)&0x1000U) - (x&0xFFFU), x2high = (x2n&0xFFFU) + 1; + x += wfrac; + + uint b1, g1, r1, b2, g2, r2, b3, g3, r3, b4, g4, r4; + boxsample(&src[xi<<2], stride, area, w, h, xlow, xhigh, ylow, yhigh, b1, g1, r1); + boxsample(&src[x2i<<2], stride, area, w2, h, x2low, x2high, ylow, yhigh, b2, g2, r2); + boxsample(&src2[xi<<2], stride, area, w, h2, xlow, xhigh, y2low, y2high, b3, g3, r3); + boxsample(&src2[x2i<<2], stride, area, w2, h2, x2low, x2high, y2low, y2high, b4, g4, r4); + + + // Y = 16 + 65.481*R + 128.553*G + 24.966*B + // Cb = 128 - 37.797*R - 74.203*G + 112.0*B + // Cr = 128 + 112.0*R - 93.786*G - 18.214*B + *ydst++ = ((16<<12) + 1052*r1 + 2065*g1 + 401*b1)>>12; + *ydst++ = ((16<<12) + 1052*r2 + 2065*g2 + 401*b2)>>12; + *ydst2++ = ((16<<12) + 1052*r3 + 2065*g3 + 401*b3)>>12;; + *ydst2++ = ((16<<12) + 1052*r4 + 2065*g4 + 401*b4)>>12;; + + const uint b = b1 + b2 + b3 + b4, + g = g1 + g2 + g3 + g4, + r = r1 + r2 + r3 + r4; + // note: weights here are scaled by 1<<10, as opposed to 1<<12, since r/g/b are already *4 + *udst++ = ((128<<12) - 152*r - 298*g + 450*b)>>12; + *vdst++ = ((128<<12) + 450*r - 377*g - 73*b)>>12; + } + + yplane += 2*ystride; + uplane += uvstride; + vplane += uvstride; + } + } + + void encodeyuv(const uchar *pixels) + { + const int flip = -1; + const uint planesize = videow * videoh; + if(!yuv) yuv = new uchar[(planesize*3)/2]; + uchar *yplane = yuv, *uplane = yuv + planesize, *vplane = yuv + planesize + planesize/4; + const int ystride = flip*int(videow), uvstride = flip*int(videow)/2; + if(flip < 0) { yplane -= int(videoh-1)*ystride; uplane -= int(videoh/2-1)*uvstride; vplane -= int(videoh/2-1)*uvstride; } + + const uint stride = videow<<2; + const uchar *src = pixels, *yend = src + videoh*stride; + while(src < yend) + { + const uchar *src2 = src + stride, *xend = src2; + uchar *ydst = yplane, *ydst2 = yplane + ystride, *udst = uplane, *vdst = vplane; + while(src < xend) + { + const uint b1 = src[0], g1 = src[1], r1 = src[2], + b2 = src[4], g2 = src[5], r2 = src[6], + b3 = src2[0], g3 = src2[1], r3 = src2[2], + b4 = src2[4], g4 = src2[5], r4 = src2[6]; + + // Y = 16 + 65.481*R + 128.553*G + 24.966*B + // Cb = 128 - 37.797*R - 74.203*G + 112.0*B + // Cr = 128 + 112.0*R - 93.786*G - 18.214*B + *ydst++ = ((16<<12) + 1052*r1 + 2065*g1 + 401*b1)>>12; + *ydst++ = ((16<<12) + 1052*r2 + 2065*g2 + 401*b2)>>12; + *ydst2++ = ((16<<12) + 1052*r3 + 2065*g3 + 401*b3)>>12;; + *ydst2++ = ((16<<12) + 1052*r4 + 2065*g4 + 401*b4)>>12;; + + const uint b = b1 + b2 + b3 + b4, + g = g1 + g2 + g3 + g4, + r = r1 + r2 + r3 + r4; + // note: weights here are scaled by 1<<10, as opposed to 1<<12, since r/g/b are already *4 + *udst++ = ((128<<12) - 152*r - 298*g + 450*b)>>12; + *vdst++ = ((128<<12) + 450*r - 377*g - 73*b)>>12; + + src += 8; + src2 += 8; + } + src = src2; + yplane += 2*ystride; + uplane += uvstride; + vplane += uvstride; + } + } + + void compressyuv(const uchar *pixels) + { + const int flip = -1; + const uint planesize = videow * videoh; + if(!yuv) yuv = new uchar[(planesize*3)/2]; + uchar *yplane = yuv, *uplane = yuv + planesize, *vplane = yuv + planesize + planesize/4; + const int ystride = flip*int(videow), uvstride = flip*int(videow)/2; + if(flip < 0) { yplane -= int(videoh-1)*ystride; uplane -= int(videoh/2-1)*uvstride; vplane -= int(videoh/2-1)*uvstride; } + + const uint stride = videow<<2; + const uchar *src = pixels, *yend = src + videoh*stride; + while(src < yend) + { + const uchar *src2 = src + stride, *xend = src2; + uchar *ydst = yplane, *ydst2 = yplane + ystride, *udst = uplane, *vdst = vplane; + while(src < xend) + { + *ydst++ = src[0]; + *ydst++ = src[4]; + *ydst2++ = src2[0]; + *ydst2++ = src2[4]; + + *udst++ = (uint(src[1]) + uint(src[5]) + uint(src2[1]) + uint(src2[5])) >> 2; + *vdst++ = (uint(src[2]) + uint(src[6]) + uint(src2[2]) + uint(src2[6])) >> 2; + + src += 8; + src2 += 8; + } + src = src2; + yplane += 2*ystride; + uplane += uvstride; + vplane += uvstride; + } + } + + bool writesound(uchar *data, uint framesize, uint frame) + { + // do conversion in-place to little endian format + // note that xoring by half the range yields the same bit pattern as subtracting the range regardless of signedness + // ... so can toggle signedness just by xoring the high byte with 0x80 + switch(soundformat) + { + case AUDIO_U8: + for(uchar *dst = data, *end = &data[framesize]; dst < end; dst++) *dst ^= 0x80; + break; + case AUDIO_S8: + break; + case AUDIO_U16LSB: + for(uchar *dst = &data[1], *end = &data[framesize]; dst < end; dst += 2) *dst ^= 0x80; + break; + case AUDIO_U16MSB: + for(ushort *dst = (ushort *)data, *end = (ushort *)&data[framesize]; dst < end; dst++) +#if SDL_BYTEORDER == SDL_BIG_ENDIAN + *dst = endianswap(*dst) ^ 0x0080; +#else + *dst = endianswap(*dst) ^ 0x8000; +#endif + break; + case AUDIO_S16LSB: + break; + case AUDIO_S16MSB: + endianswap((short *)data, framesize/2); + break; + } + + if(totalsize - segments.last().offset + framesize > 1000*1000*1000 && !nextsegment()) return false; + + addindex(frame, 1, framesize); + + writechunk("01wb", data, framesize); + + return true; + } + + + enum + { + VID_RGB = 0, + VID_YUV, + VID_YUV420 + }; + + void flushsegment() + { + endlistchunk(); // LIST movi + + avisegmentinfo &seg = segments.last(); + + uint indexframes = 0, videoframes = 0, soundframes = 0; + for(int i = seg.firstindex; i < index.length(); i++) + { + aviindexentry &e = index[i]; + if(e.type) soundframes++; + else + { + if(i == seg.firstindex || e.offset != index[i-1].offset) + videoframes++; + indexframes++; + } + } + + if(segments.length() == 1) + { + startchunk("idx1", index.length()*16); + loopv(index) + { + aviindexentry &entry = index[i]; + // printf("%3d %s %08x\n", i, (entry.type==1)?"s":"v", entry.offset); + f->write(entry.type ? "01wb" : "00dc", 4); // chunkid + f->putlil(0x10); // flags - KEYFRAME + f->putlil(entry.offset); // offset (relative to movi) + f->putlil(entry.size); // size + } + endchunk(); + } + + seg.videoframes = videoframes; + seg.videoindexoffset = totalsize; + startchunk("ix00", 24 + indexframes*8); + f->putlil(2); // longs per entry + f->putlil(0x0100); // index of chunks + f->putlil(indexframes); // entries in use + f->write("00dc", 4); // chunk id + f->putlil(seg.offset&stream::offset(0xFFFFFFFFU)); // offset low + f->putlil(seg.offset>>32); // offset high + f->putlil(0); // reserved 3 + for(int i = seg.firstindex; i < index.length(); i++) + { + aviindexentry &e = index[i]; + if(e.type) continue; + f->putlil(e.offset + 4 + 4); + f->putlil(e.size); + } + endchunk(); // ix00 + seg.videoindexsize = uint(totalsize - seg.videoindexoffset); + + if(soundframes) + { + seg.soundframes = soundframes; + seg.soundindexoffset = totalsize; + startchunk("ix01", 24 + soundframes*8); + f->putlil(2); // longs per entry + f->putlil(0x0100); // index of chunks + f->putlil(soundframes); // entries in use + f->write("01wb", 4); // chunk id + f->putlil(seg.offset&stream::offset(0xFFFFFFFFU)); // offset low + f->putlil(seg.offset>>32); // offset high + f->putlil(0); // reserved 3 + for(int i = seg.firstindex; i < index.length(); i++) + { + aviindexentry &e = index[i]; + if(!e.type) continue; + f->putlil(e.offset + 4 + 4); + f->putlil(e.size); + } + endchunk(); // ix01 + seg.soundindexsize = uint(totalsize - seg.soundindexoffset); + } + + endlistchunk(); // RIFF AVI/AVIX + } + + bool nextsegment() + { + if(segments.length()) + { + if(segments.length() >= MAX_SUPER_INDEX) return false; + flushsegment(); + listchunk("RIFF", "AVIX"); + } + listchunk("LIST", "movi"); + segments.add(avisegmentinfo(chunkoffsets[chunkdepth], index.length())); + return true; + } + + bool writevideoframe(const uchar *pixels, uint srcw, uint srch, int format, uint frame) + { + if(frame < videoframes) return true; + + switch(format) + { + case VID_RGB: + if(srcw != videow || srch != videoh) scaleyuv(pixels, srcw, srch); + else encodeyuv(pixels); + break; + case VID_YUV: + compressyuv(pixels); + break; + } + + const uint framesize = (videow * videoh * 3) / 2; + if(totalsize - segments.last().offset + framesize > 1000*1000*1000 && !nextsegment()) return false; + + while(videoframes <= frame) addindex(videoframes++, 0, framesize); + + writechunk("00dc", format == VID_YUV420 ? pixels : yuv, framesize); + + return true; + } + +}; + +VAR(movieaccelblit, 0, 0, 1); +VAR(movieaccelyuv, 0, 1, 1); +VARP(movieaccel, 0, 1, 1); +VARP(moviesync, 0, 0, 1); +FVARP(movieminquality, 0, 0, 1); + +namespace recorder +{ + static enum { REC_OK = 0, REC_USERHALT, REC_TOOSLOW, REC_FILERROR } state = REC_OK; + + static aviwriter *file = NULL; + static int starttime = 0; + + static int stats[1000]; + static int statsindex = 0; + static uint dps = 0; // dropped frames per sample + + enum { MAXSOUNDBUFFERS = 128 }; // sounds queue up until there is a video frame, so at low fps you'll need a bigger queue + struct soundbuffer + { + uchar *sound; + uint size, maxsize; + uint frame; + + soundbuffer() : sound(NULL), maxsize(0) {} + ~soundbuffer() { cleanup(); } + + void load(uchar *stream, uint len, uint fnum) + { + if(len > maxsize) + { + DELETEA(sound); + sound = new uchar[len]; + maxsize = len; + } + size = len; + frame = fnum; + memcpy(sound, stream, len); + } + + void cleanup() { DELETEA(sound); maxsize = 0; } + }; + static queue soundbuffers; + static SDL_mutex *soundlock = NULL; + + enum { MAXVIDEOBUFFERS = 2 }; // double buffer + struct videobuffer + { + uchar *video; + uint w, h, bpp, frame; + int format; + + videobuffer() : video(NULL){} + ~videobuffer() { cleanup(); } + + void init(int nw, int nh, int nbpp) + { + DELETEA(video); + w = nw; + h = nh; + bpp = nbpp; + video = new uchar[w*h*bpp]; + format = -1; + } + + void cleanup() { DELETEA(video); } + }; + static queue videobuffers; + static uint lastframe = ~0U; + + static GLuint scalefb = 0, scaletex[2] = { 0, 0 }; + static uint scalew = 0, scaleh = 0; + static GLuint encodefb = 0, encoderb = 0; + + static SDL_Thread *thread = NULL; + static SDL_mutex *videolock = NULL; + static SDL_cond *shouldencode = NULL, *shouldread = NULL; + + bool isrecording() { return file != NULL; } + + float calcquality() + { + return 1.0f - float(dps)/float(dps+file->videofps); // strictly speaking should lock to read dps - 1.0=perfect, 0.5=half of frames are beingdropped + } + + int gettime() + { + return inbetweenframes ? getclockmillis() : totalmillis; + } + + int videoencoder(void *data) // runs on a separate thread + { + for(int numvid = 0, numsound = 0;;) + { + SDL_LockMutex(videolock); + for(; numvid > 0; numvid--) videobuffers.remove(); + SDL_CondSignal(shouldread); + while(videobuffers.empty() && state == REC_OK) SDL_CondWait(shouldencode, videolock); + if(state != REC_OK) { SDL_UnlockMutex(videolock); break; } + videobuffer &m = videobuffers.removing(); + numvid++; + SDL_UnlockMutex(videolock); + + if(file->soundfrequency > 0) + { + // chug data from lock protected buffer to avoid holding lock while writing to file + SDL_LockMutex(soundlock); + for(; numsound > 0; numsound--) soundbuffers.remove(); + for(; numsound < soundbuffers.length(); numsound++) + { + soundbuffer &s = soundbuffers.removing(numsound); + if(s.frame > m.frame) break; // sync with video + } + SDL_UnlockMutex(soundlock); + loopi(numsound) + { + soundbuffer &s = soundbuffers.removing(i); + if(!file->writesound(s.sound, s.size, s.frame)) state = REC_FILERROR; + } + } + + int duplicates = m.frame - (int)file->videoframes + 1; + if(duplicates > 0) // determine how many frames have been dropped over the sample window + { + dps -= stats[statsindex]; + stats[statsindex] = duplicates-1; + dps += stats[statsindex]; + statsindex = (statsindex+1)%file->videofps; + } + //printf("frame %d->%d (%d dps): sound = %d bytes\n", file->videoframes, nextframenum, dps, m.soundlength); + if(calcquality() < movieminquality) state = REC_TOOSLOW; + else if(!file->writevideoframe(m.video, m.w, m.h, m.format, m.frame)) state = REC_FILERROR; + + m.frame = ~0U; + } + + return 0; + } + + void soundencoder(void *udata, Uint8 *stream, int len) // callback occurs on a separate thread + { + SDL_LockMutex(soundlock); + if(soundbuffers.full()) + { + if(movieminquality >= 1) state = REC_TOOSLOW; + } + else if(state == REC_OK) + { + uint nextframe = (max(gettime() - starttime, 0)*file->videofps)/1000; + soundbuffer &s = soundbuffers.add(); + s.load((uchar *)stream, len, nextframe); + } + SDL_UnlockMutex(soundlock); + } + + void start(const char *filename, int videofps, int videow, int videoh, bool sound) + { + if(file) return; + + useshaderbyname("moviergb"); + useshaderbyname("movieyuv"); + useshaderbyname("moviey"); + useshaderbyname("movieu"); + useshaderbyname("moviev"); + + int fps, bestdiff, worstdiff; + getfps(fps, bestdiff, worstdiff); + if(videofps > fps) conoutf(CON_WARN, "frame rate may be too low to capture at %d fps", videofps); + + if(videow%2) videow += 1; + if(videoh%2) videoh += 1; + + file = new aviwriter(filename, videow, videoh, videofps, sound); + if(!file->open()) + { + conoutf(CON_ERROR, "unable to create file %s", filename); + DELETEP(file); + return; + } + conoutf("movie recording to: %s %dx%d @ %dfps%s", file->filename, file->videow, file->videoh, file->videofps, (file->soundfrequency>0)?" + sound":""); + + starttime = gettime(); + loopi(file->videofps) stats[i] = 0; + statsindex = 0; + dps = 0; + + lastframe = ~0U; + videobuffers.clear(); + loopi(MAXVIDEOBUFFERS) + { + uint w = screenw, h = screenw; + videobuffers.data[i].init(w, h, 4); + videobuffers.data[i].frame = ~0U; + } + + soundbuffers.clear(); + + soundlock = SDL_CreateMutex(); + videolock = SDL_CreateMutex(); + shouldencode = SDL_CreateCond(); + shouldread = SDL_CreateCond(); + thread = SDL_CreateThread(videoencoder, "video encoder", NULL); + if(file->soundfrequency > 0) Mix_SetPostMix(soundencoder, NULL); + } + + void cleanup() + { + if(scalefb) { glDeleteFramebuffers_(1, &scalefb); scalefb = 0; } + if(scaletex[0] || scaletex[1]) { glDeleteTextures(2, scaletex); memset(scaletex, 0, sizeof(scaletex)); } + scalew = scaleh = 0; + if(encodefb) { glDeleteFramebuffers_(1, &encodefb); encodefb = 0; } + if(encoderb) { glDeleteRenderbuffers_(1, &encoderb); encoderb = 0; } + } + + void stop() + { + if(!file) return; + if(state == REC_OK) state = REC_USERHALT; + if(file->soundfrequency > 0) Mix_SetPostMix(NULL, NULL); + + SDL_LockMutex(videolock); // wakeup thread enough to kill it + SDL_CondSignal(shouldencode); + SDL_UnlockMutex(videolock); + + SDL_WaitThread(thread, NULL); // block until thread is finished + + cleanup(); + + loopi(MAXVIDEOBUFFERS) videobuffers.data[i].cleanup(); + loopi(MAXSOUNDBUFFERS) soundbuffers.data[i].cleanup(); + + SDL_DestroyMutex(soundlock); + SDL_DestroyMutex(videolock); + SDL_DestroyCond(shouldencode); + SDL_DestroyCond(shouldread); + + soundlock = videolock = NULL; + shouldencode = shouldread = NULL; + thread = NULL; + + static const char * const mesgs[] = { "ok", "stopped", "computer too slow", "file error"}; + conoutf("movie recording halted: %s, %d frames", mesgs[state], file->videoframes); + + DELETEP(file); + state = REC_OK; + } + + void readbuffer(videobuffer &m, uint nextframe) + { + bool accelyuv = movieaccelyuv && !(m.w%8), + usefbo = movieaccel && file->videow <= (uint)screenw && file->videoh <= (uint)screenh && (accelyuv || file->videow < (uint)screenw || file->videoh < (uint)screenh); + uint w = screenw, h = screenh; + if(usefbo) { w = file->videow; h = file->videoh; } + if(w != m.w || h != m.h) m.init(w, h, 4); + m.format = aviwriter::VID_RGB; + m.frame = nextframe; + + glPixelStorei(GL_PACK_ALIGNMENT, texalign(m.video, m.w, 4)); + if(usefbo) + { + uint tw = screenw, th = screenh; + if(hasFBB && movieaccelblit) { tw = max(tw/2, m.w); th = max(th/2, m.h); } + if(tw != scalew || th != scaleh) + { + if(!scalefb) glGenFramebuffers_(1, &scalefb); + loopi(2) + { + if(!scaletex[i]) glGenTextures(1, &scaletex[i]); + createtexture(scaletex[i], tw, th, NULL, 3, 1, GL_RGB); + } + scalew = tw; + scaleh = th; + } + if(accelyuv && (!encodefb || !encoderb)) + { + if(!encodefb) glGenFramebuffers_(1, &encodefb); + glBindFramebuffer_(GL_FRAMEBUFFER, encodefb); + if(!encoderb) glGenRenderbuffers_(1, &encoderb); + glBindRenderbuffer_(GL_RENDERBUFFER, encoderb); + glRenderbufferStorage_(GL_RENDERBUFFER, GL_RGBA, (m.w*3)/8, m.h); + glFramebufferRenderbuffer_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, encoderb); + glBindRenderbuffer_(GL_RENDERBUFFER, 0); + glBindFramebuffer_(GL_FRAMEBUFFER, 0); + } + + if(tw < (uint)screenw || th < (uint)screenh) + { + glBindFramebuffer_(GL_READ_FRAMEBUFFER, 0); + glBindFramebuffer_(GL_DRAW_FRAMEBUFFER, scalefb); + glFramebufferTexture2D_(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, scaletex[0], 0); + glBlitFramebuffer_(0, 0, screenw, screenh, 0, 0, tw, th, GL_COLOR_BUFFER_BIT, GL_LINEAR); + glBindFramebuffer_(GL_DRAW_FRAMEBUFFER, 0); + } + else + { + glBindTexture(GL_TEXTURE_2D, scaletex[0]); + glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, screenw, screenh); + } + + GLOBALPARAMF(moviescale, 1.0f/scalew, 1.0f/scaleh); + if(tw > m.w || th > m.h || (!accelyuv && tw >= m.w && th >= m.h)) + { + glBindFramebuffer_(GL_FRAMEBUFFER, scalefb); + do + { + uint dw = max(tw/2, m.w), dh = max(th/2, m.h); + glFramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, scaletex[1], 0); + glViewport(0, 0, dw, dh); + glBindTexture(GL_TEXTURE_2D, scaletex[0]); + if(dw == m.w && dh == m.h && !accelyuv) { SETSHADER(movieyuv); m.format = aviwriter::VID_YUV; } + else SETSHADER(moviergb); + screenquad(tw/float(scalew), th/float(scaleh)); + tw = dw; + th = dh; + swap(scaletex[0], scaletex[1]); + } while(tw > m.w || th > m.h); + } + if(accelyuv) + { + glBindFramebuffer_(GL_FRAMEBUFFER, encodefb); + glBindTexture(GL_TEXTURE_2D, scaletex[0]); + glViewport(0, 0, m.w/4, m.h); SETSHADER(moviey); screenquadflipped(m.w/float(scalew), m.h/float(scaleh)); + glViewport(m.w/4, 0, m.w/8, m.h/2); SETSHADER(movieu); screenquadflipped(m.w/float(scalew), m.h/float(scaleh)); + glViewport(m.w/4, m.h/2, m.w/8, m.h/2); SETSHADER(moviev); screenquadflipped(m.w/float(scalew), m.h/float(scaleh)); + const uint planesize = m.w * m.h; + glPixelStorei(GL_PACK_ALIGNMENT, texalign(m.video, m.w/4, 4)); + glReadPixels(0, 0, m.w/4, m.h, GL_BGRA, GL_UNSIGNED_BYTE, m.video); + glPixelStorei(GL_PACK_ALIGNMENT, texalign(&m.video[planesize], m.w/8, 4)); + glReadPixels(m.w/4, 0, m.w/8, m.h/2, GL_BGRA, GL_UNSIGNED_BYTE, &m.video[planesize]); + glPixelStorei(GL_PACK_ALIGNMENT, texalign(&m.video[planesize + planesize/4], m.w/8, 4)); + glReadPixels(m.w/4, m.h/2, m.w/8, m.h/2, GL_BGRA, GL_UNSIGNED_BYTE, &m.video[planesize + planesize/4]); + m.format = aviwriter::VID_YUV420; + } + else + { + glBindFramebuffer_(GL_FRAMEBUFFER, scalefb); + glReadPixels(0, 0, m.w, m.h, GL_BGRA, GL_UNSIGNED_BYTE, m.video); + } + glBindFramebuffer_(GL_FRAMEBUFFER, 0); + glViewport(0, 0, screenw, screenh); + + } + else glReadPixels(0, 0, m.w, m.h, GL_BGRA, GL_UNSIGNED_BYTE, m.video); + } + + bool readbuffer() + { + if(!file) return false; + if(state != REC_OK) + { + stop(); + return false; + } + SDL_LockMutex(videolock); + if(moviesync && videobuffers.full()) SDL_CondWait(shouldread, videolock); + uint nextframe = (max(gettime() - starttime, 0)*file->videofps)/1000; + if(!videobuffers.full() && (lastframe == ~0U || nextframe > lastframe)) + { + videobuffer &m = videobuffers.adding(); + SDL_UnlockMutex(videolock); + readbuffer(m, nextframe); + SDL_LockMutex(videolock); + lastframe = nextframe; + videobuffers.add(); + SDL_CondSignal(shouldencode); + } + SDL_UnlockMutex(videolock); + return true; + } + + void drawhud() + { + int w = screenw, h = screenh; + if(forceaspect) w = int(ceil(h*forceaspect)); + gettextres(w, h); + + hudmatrix.ortho(0, w, h, 0, -1, 1); + hudmatrix.scale(1/3.0f, 1/3.0f, 1); + resethudmatrix(); + hudshader->set(); + + glEnable(GL_BLEND); + + double totalsize = file->filespaceguess(); + const char *unit = "KB"; + if(totalsize >= 1e9) { totalsize /= 1e9; unit = "GB"; } + else if(totalsize >= 1e6) { totalsize /= 1e6; unit = "MB"; } + else totalsize /= 1e3; + + draw_textf("recorded %.1f%s %d%%", w*3-10*FONTH, h*3-FONTH-FONTH*3/2, totalsize, unit, int(calcquality()*100)); + + glDisable(GL_BLEND); + } + + void capture(bool overlay) + { + if(readbuffer() && overlay) drawhud(); + } +} + +VARP(moview, 0, 320, 10000); +VARP(movieh, 0, 240, 10000); +VARP(moviefps, 1, 24, 1000); +VARP(moviesound, 0, 1, 1); + +void movie(char *name) +{ + if(name[0] == '\0') recorder::stop(); + else if(!recorder::isrecording()) recorder::start(name, moviefps, moview ? moview : screenw, movieh ? movieh : screenh, moviesound!=0); +} + +COMMAND(movie, "s"); +ICOMMAND(movierecording, "", (), intret(recorder::isrecording() ? 1 : 0)); + diff --git a/src/engine/mpr.h b/src/engine/mpr.h new file mode 100644 index 0000000..b4cfb59 --- /dev/null +++ b/src/engine/mpr.h @@ -0,0 +1,575 @@ +// This code is based off the Minkowski Portal Refinement algorithm by Gary Snethen in XenoCollide & Game Programming Gems 7. + +namespace mpr +{ + struct CubePlanes + { + const clipplanes &p; + + CubePlanes(const clipplanes &p) : p(p) {} + + vec center() const { return p.o; } + + vec supportpoint(const vec &n) const + { + int besti = 7; + float bestd = n.dot(p.v[7]); + loopi(7) + { + float d = n.dot(p.v[i]); + if(d > bestd) { besti = i; bestd = d; } + } + return p.v[besti]; + } + }; + + struct SolidCube + { + vec o; + int size; + + SolidCube(float x, float y, float z, int size) : o(x, y, z), size(size) {} + SolidCube(const vec &o, int size) : o(o), size(size) {} + SolidCube(const ivec &o, int size) : o(o), size(size) {} + + vec center() const { return vec(o).add(size/2); } + + vec supportpoint(const vec &n) const + { + vec p(o); + if(n.x > 0) p.x += size; + if(n.y > 0) p.y += size; + if(n.z > 0) p.z += size; + return p; + } + }; + + struct Ent + { + physent *ent; + + Ent(physent *ent) : ent(ent) {} + + vec center() const { return vec(ent->o.x, ent->o.y, ent->o.z + (ent->aboveeye - ent->eyeheight)/2); } + }; + + struct EntOBB : Ent + { + matrix3 orient; + float zmargin; + + EntOBB(physent *ent, float zmargin = 0) : Ent(ent), zmargin(zmargin) + { + orient.setyaw(ent->yaw*RAD); + } + + vec center() const { return vec(ent->o.x, ent->o.y, ent->o.z + (ent->aboveeye - ent->eyeheight - zmargin)/2); } + + vec contactface(const vec &wn, const vec &wdir) const + { + vec n = orient.transform(wn).div(vec(ent->xradius, ent->yradius, (ent->aboveeye + ent->eyeheight + zmargin)/2)), + dir = orient.transform(wdir), + an(fabs(n.x), fabs(n.y), dir.z ? fabs(n.z) : 0), + fn(0, 0, 0); + if(an.x > an.y) + { + if(an.x > an.z) fn.x = n.x*dir.x < 0 ? (n.x > 0 ? 1 : -1) : 0; + else if(an.z > 0) fn.z = n.z*dir.z < 0 ? (n.z > 0 ? 1 : -1) : 0; + } + else if(an.y > an.z) fn.y = n.y*dir.y < 0 ? (n.y > 0 ? 1 : -1) : 0; + else if(an.z > 0) fn.z = n.z*dir.z < 0 ? (n.z > 0 ? 1 : -1) : 0; + return orient.transposedtransform(fn); + } + + vec localsupportpoint(const vec &ln) const + { + return vec(ln.x > 0 ? ent->xradius : -ent->xradius, + ln.y > 0 ? ent->yradius : -ent->yradius, + ln.z > 0 ? ent->aboveeye : -ent->eyeheight - zmargin); + } + + vec supportpoint(const vec &n) const + { + return orient.transposedtransform(localsupportpoint(orient.transform(n))).add(ent->o); + } + + float supportcoordneg(float a, float b, float c) const + { + return localsupportpoint(vec(-a, -b, -c)).dot(vec(a, b, c)); + } + float supportcoord(float a, float b, float c) const + { + return localsupportpoint(vec(a, b, c)).dot(vec(a, b, c)); + } + + float left() const { return supportcoordneg(orient.a.x, orient.b.x, orient.c.x) + ent->o.x; } + float right() const { return supportcoord(orient.a.x, orient.b.x, orient.c.x) + ent->o.x; } + float back() const { return supportcoordneg(orient.a.y, orient.b.y, orient.c.y) + ent->o.y; } + float front() const { return supportcoord(orient.a.y, orient.b.y, orient.c.y) + ent->o.y; } + float bottom() const { return ent->o.z - ent->eyeheight - zmargin; } + float top() const { return ent->o.z + ent->aboveeye; } + }; + + struct EntFuzzy : Ent + { + EntFuzzy(physent *ent) : Ent(ent) {} + + float left() const { return ent->o.x - ent->radius; } + float right() const { return ent->o.x + ent->radius; } + float back() const { return ent->o.y - ent->radius; } + float front() const { return ent->o.y + ent->radius; } + float bottom() const { return ent->o.z - ent->eyeheight; } + float top() const { return ent->o.z + ent->aboveeye; } + }; + + struct EntCylinder : EntFuzzy + { + float zmargin; + + EntCylinder(physent *ent, float zmargin = 0) : EntFuzzy(ent), zmargin(zmargin) {} + + vec center() const { return vec(ent->o.x, ent->o.y, ent->o.z + (ent->aboveeye - ent->eyeheight - zmargin)/2); } + + float bottom() const { return ent->o.z - ent->eyeheight - zmargin; } + + vec contactface(const vec &n, const vec &dir) const + { + float dxy = n.dot2(n)/(ent->radius*ent->radius), dz = n.z*n.z*4/(ent->aboveeye + ent->eyeheight + zmargin); + vec fn(0, 0, 0); + if(dz > dxy && dir.z) fn.z = n.z*dir.z < 0 ? (n.z > 0 ? 1 : -1) : 0; + else if(n.dot2(dir) < 0) + { + fn.x = n.x; + fn.y = n.y; + fn.normalize(); + } + return fn; + } + + vec supportpoint(const vec &n) const + { + vec p(ent->o); + if(n.z > 0) p.z += ent->aboveeye; + else p.z -= ent->eyeheight + zmargin; + if(n.x || n.y) + { + float r = ent->radius / n.magnitude2(); + p.x += n.x*r; + p.y += n.y*r; + } + return p; + } + }; + + struct EntCapsule : EntFuzzy + { + EntCapsule(physent *ent) : EntFuzzy(ent) {} + + vec supportpoint(const vec &n) const + { + vec p(ent->o); + if(n.z > 0) p.z += ent->aboveeye - ent->radius; + else p.z -= ent->eyeheight - ent->radius; + p.add(vec(n).mul(ent->radius / n.magnitude())); + return p; + } + }; + + struct EntEllipsoid : EntFuzzy + { + EntEllipsoid(physent *ent) : EntFuzzy(ent) {} + + vec supportpoint(const vec &dir) const + { + vec p(ent->o), n = vec(dir).normalize(); + p.x += ent->radius*n.x; + p.y += ent->radius*n.y; + p.z += (ent->aboveeye + ent->eyeheight)/2*(1 + n.z) - ent->eyeheight; + return p; + } + }; + + struct Model + { + vec o, radius; + matrix3 orient; + + Model(const vec &ent, const vec ¢er, const vec &radius, int yaw) : o(ent), radius(radius) + { + orient.setyaw(yaw*RAD); + o.add(orient.transposedtransform(center)); + } + + vec center() const { return o; } + }; + + struct ModelOBB : Model + { + ModelOBB(const vec &ent, const vec ¢er, const vec &radius, int yaw) : + Model(ent, center, radius, yaw) + {} + + vec contactface(const vec &wn, const vec &wdir) const + { + vec n = orient.transform(wn).div(radius), dir = orient.transform(wdir), + an(fabs(n.x), fabs(n.y), dir.z ? fabs(n.z) : 0), + fn(0, 0, 0); + if(an.x > an.y) + { + if(an.x > an.z) fn.x = n.x*dir.x < 0 ? (n.x > 0 ? 1 : -1) : 0; + else if(an.z > 0) fn.z = n.z*dir.z < 0 ? (n.z > 0 ? 1 : -1) : 0; + } + else if(an.y > an.z) fn.y = n.y*dir.y < 0 ? (n.y > 0 ? 1 : -1) : 0; + else if(an.z > 0) fn.z = n.z*dir.z < 0 ? (n.z > 0 ? 1 : -1) : 0; + return orient.transposedtransform(fn); + } + + vec supportpoint(const vec &n) const + { + vec ln = orient.transform(n), p(0, 0, 0); + if(ln.x > 0) p.x += radius.x; + else p.x -= radius.x; + if(ln.y > 0) p.y += radius.y; + else p.y -= radius.y; + if(ln.z > 0) p.z += radius.z; + else p.z -= radius.z; + return orient.transposedtransform(p).add(o); + } + }; + + struct ModelEllipse : Model + { + ModelEllipse(const vec &ent, const vec ¢er, const vec &radius, int yaw) : + Model(ent, center, radius, yaw) + {} + + vec contactface(const vec &wn, const vec &wdir) const + { + vec n = orient.transform(wn).div(radius), dir = orient.transform(wdir); + float dxy = n.dot2(n), dz = n.z*n.z; + vec fn(0, 0, 0); + if(dz > dxy && dir.z) fn.z = n.z*dir.z < 0 ? (n.z > 0 ? 1 : -1) : 0; + else if(n.dot2(dir) < 0) + { + fn.x = n.x*radius.y; + fn.y = n.y*radius.x; + fn.normalize(); + } + return orient.transposedtransform(fn); + } + + vec supportpoint(const vec &n) const + { + vec ln = orient.transform(n), p(0, 0, 0); + if(ln.z > 0) p.z += radius.z; + else p.z -= radius.z; + if(ln.x || ln.y) + { + float r = ln.magnitude2(); + p.x += ln.x*radius.x/r; + p.y += ln.y*radius.y/r; + } + return orient.transposedtransform(p).add(o); + } + }; + + const float boundarytolerance = 1e-3f; + + template + bool collide(const T &p1, const U &p2) + { + // v0 = center of Minkowski difference + vec v0 = p2.center().sub(p1.center()); + if(v0.iszero()) return true; // v0 and origin overlap ==> hit + + // v1 = support in direction of origin + vec n = vec(v0).neg(); + vec v1 = p2.supportpoint(n).sub(p1.supportpoint(vec(n).neg())); + if(v1.dot(n) <= 0) return false; // origin outside v1 support plane ==> miss + + // v2 = support perpendicular to plane containing origin, v0 and v1 + n.cross(v1, v0); + if(n.iszero()) return true; // v0, v1 and origin colinear (and origin inside v1 support plane) == > hit + vec v2 = p2.supportpoint(n).sub(p1.supportpoint(vec(n).neg())); + if(v2.dot(n) <= 0) return false; // origin outside v2 support plane ==> miss + + // v3 = support perpendicular to plane containing v0, v1 and v2 + n.cross(v0, v1, v2); + + // If the origin is on the - side of the plane, reverse the direction of the plane + if(n.dot(v0) > 0) + { + swap(v1, v2); + n.neg(); + } + + /// + // Phase One: Find a valid portal + + loopi(100) + { + // Obtain the next support point + vec v3 = p2.supportpoint(n).sub(p1.supportpoint(vec(n).neg())); + if(v3.dot(n) <= 0) return false; // origin outside v3 support plane ==> miss + + // If origin is outside (v1,v0,v3), then portal is invalid -- eliminate v2 and find new support outside face + vec v3xv0; + v3xv0.cross(v3, v0); + if(v1.dot(v3xv0) < 0) + { + v2 = v3; + n.cross(v0, v1, v3); + continue; + } + + // If origin is outside (v3,v0,v2), then portal is invalid -- eliminate v1 and find new support outside face + if(v2.dot(v3xv0) > 0) + { + v1 = v3; + n.cross(v0, v3, v2); + continue; + } + + /// + // Phase Two: Refine the portal + + for(int j = 0;; j++) + { + // Compute outward facing normal of the portal + n.cross(v1, v2, v3); + + // If the origin is inside the portal, we have a hit + if(n.dot(v1) >= 0) return true; + + n.normalize(); + + // Find the support point in the direction of the portal's normal + vec v4 = p2.supportpoint(n).sub(p1.supportpoint(vec(n).neg())); + + // If the origin is outside the support plane or the boundary is thin enough, we have a miss + if(v4.dot(n) <= 0 || vec(v4).sub(v3).dot(n) <= boundarytolerance || j > 100) return false; + + // Test origin against the three planes that separate the new portal candidates: (v1,v4,v0) (v2,v4,v0) (v3,v4,v0) + // Note: We're taking advantage of the triple product identities here as an optimization + // (v1 % v4) * v0 == v1 * (v4 % v0) > 0 if origin inside (v1, v4, v0) + // (v2 % v4) * v0 == v2 * (v4 % v0) > 0 if origin inside (v2, v4, v0) + // (v3 % v4) * v0 == v3 * (v4 % v0) > 0 if origin inside (v3, v4, v0) + vec v4xv0; + v4xv0.cross(v4, v0); + if(v1.dot(v4xv0) > 0) + { + if(v2.dot(v4xv0) > 0) v1 = v4; // Inside v1 & inside v2 ==> eliminate v1 + else v3 = v4; // Inside v1 & outside v2 ==> eliminate v3 + } + else + { + if(v3.dot(v4xv0) > 0) v2 = v4; // Outside v1 & inside v3 ==> eliminate v2 + else v1 = v4; // Outside v1 & outside v3 ==> eliminate v1 + } + } + } + return false; + } + + template + bool collide(const T &p1, const U &p2, vec *contactnormal, vec *contactpoint1, vec *contactpoint2) + { + // v0 = center of Minkowski sum + vec v01 = p1.center(); + vec v02 = p2.center(); + vec v0 = vec(v02).sub(v01); + + // Avoid case where centers overlap -- any direction is fine in this case + if(v0.iszero()) v0 = vec(0, 0, 1e-5f); + + // v1 = support in direction of origin + vec n = vec(v0).neg(); + vec v11 = p1.supportpoint(vec(n).neg()); + vec v12 = p2.supportpoint(n); + vec v1 = vec(v12).sub(v11); + if(v1.dot(n) <= 0) + { + if(contactnormal) *contactnormal = n; + return false; + } + + // v2 - support perpendicular to v1,v0 + n.cross(v1, v0); + if(n.iszero()) + { + n = vec(v1).sub(v0); + n.normalize(); + if(contactnormal) *contactnormal = n; + if(contactpoint1) *contactpoint1 = v11; + if(contactpoint2) *contactpoint2 = v12; + return true; + } + vec v21 = p1.supportpoint(vec(n).neg()); + vec v22 = p2.supportpoint(n); + vec v2 = vec(v22).sub(v21); + if(v2.dot(n) <= 0) + { + if(contactnormal) *contactnormal = n; + return false; + } + + // Determine whether origin is on + or - side of plane (v1,v0,v2) + n.cross(v0, v1, v2); + ASSERT( !n.iszero() ); + // If the origin is on the - side of the plane, reverse the direction of the plane + if(n.dot(v0) > 0) + { + swap(v1, v2); + swap(v11, v21); + swap(v12, v22); + n.neg(); + } + + /// + // Phase One: Identify a portal + + loopi(100) + { + // Obtain the support point in a direction perpendicular to the existing plane + // Note: This point is guaranteed to lie off the plane + vec v31 = p1.supportpoint(vec(n).neg()); + vec v32 = p2.supportpoint(n); + vec v3 = vec(v32).sub(v31); + if(v3.dot(n) <= 0) + { + if(contactnormal) *contactnormal = n; + return false; + } + + // If origin is outside (v1,v0,v3), then eliminate v2 and loop + vec v3xv0; + v3xv0.cross(v3, v0); + if(v1.dot(v3xv0) < 0) + { + v2 = v3; + v21 = v31; + v22 = v32; + n.cross(v0, v1, v3); + continue; + } + + // If origin is outside (v3,v0,v2), then eliminate v1 and loop + if(v2.dot(v3xv0) > 0) + { + v1 = v3; + v11 = v31; + v12 = v32; + n.cross(v0, v3, v2); + continue; + } + + bool hit = false; + + /// + // Phase Two: Refine the portal + + // We are now inside of a wedge... + for(int j = 0;; j++) + { + // Compute normal of the wedge face + n.cross(v1, v2, v3); + + // Can this happen??? Can it be handled more cleanly? + if(n.iszero()) + { + ASSERT(0); + return true; + } + + n.normalize(); + + // If the origin is inside the wedge, we have a hit + if(n.dot(v1) >= 0 && !hit) + { + if(contactnormal) *contactnormal = n; + + // Compute the barycentric coordinates of the origin + if(contactpoint1 || contactpoint2) + { + float b0 = v3.scalartriple(v1, v2), + b1 = v0.scalartriple(v3, v2), + b2 = v3.scalartriple(v0, v1), + b3 = v0.scalartriple(v2, v1), + sum = b0 + b1 + b2 + b3; + if(sum <= 0) + { + b0 = 0; + b1 = n.scalartriple(v2, v3); + b2 = n.scalartriple(v3, v1); + b3 = n.scalartriple(v1, v2); + sum = b1 + b2 + b3; + } + if(contactpoint1) + *contactpoint1 = (vec(v01).mul(b0).add(vec(v11).mul(b1)).add(vec(v21).mul(b2)).add(vec(v31).mul(b3))).mul(1.0f/sum); + if(contactpoint2) + *contactpoint2 = (vec(v02).mul(b0).add(vec(v12).mul(b1)).add(vec(v22).mul(b2)).add(vec(v32).mul(b3))).mul(1.0f/sum); + } + + // HIT!!! + hit = true; + } + + // Find the support point in the direction of the wedge face + vec v41 = p1.supportpoint(vec(n).neg()); + vec v42 = p2.supportpoint(n); + vec v4 = vec(v42).sub(v41); + + // If the boundary is thin enough or the origin is outside the support plane for the newly discovered vertex, then we can terminate + if(v4.dot(n) <= 0 || vec(v4).sub(v3).dot(n) <= boundarytolerance || j > 100) + { + if(contactnormal) *contactnormal = n; + return hit; + } + + // Test origin against the three planes that separate the new portal candidates: (v1,v4,v0) (v2,v4,v0) (v3,v4,v0) + // Note: We're taking advantage of the triple product identities here as an optimization + // (v1 % v4) * v0 == v1 * (v4 % v0) > 0 if origin inside (v1, v4, v0) + // (v2 % v4) * v0 == v2 * (v4 % v0) > 0 if origin inside (v2, v4, v0) + // (v3 % v4) * v0 == v3 * (v4 % v0) > 0 if origin inside (v3, v4, v0) + vec v4xv0; + v4xv0.cross(v4, v0); + if(v1.dot(v4xv0) > 0) // Compute the tetrahedron dividing face d1 = (v4,v0,v1) + { + if(v2.dot(v4xv0) > 0) // Compute the tetrahedron dividing face d2 = (v4,v0,v2) + { + // Inside d1 & inside d2 ==> eliminate v1 + v1 = v4; + v11 = v41; + v12 = v42; + } + else + { + // Inside d1 & outside d2 ==> eliminate v3 + v3 = v4; + v31 = v41; + v32 = v42; + } + } + else + { + if(v3.dot(v4xv0) > 0) // Compute the tetrahedron dividing face d3 = (v4,v0,v3) + { + // Outside d1 & inside d3 ==> eliminate v2 + v2 = v4; + v21 = v41; + v22 = v42; + } + else + { + // Outside d1 & outside d3 ==> eliminate v1 + v1 = v4; + v11 = v41; + v12 = v42; + } + } + } + } + return false; + } +} + diff --git a/src/engine/normal.cpp b/src/engine/normal.cpp new file mode 100644 index 0000000..d8641ab --- /dev/null +++ b/src/engine/normal.cpp @@ -0,0 +1,383 @@ +#include "engine.h" + +struct normalgroup +{ + vec pos; + int flat, normals, tnormals; + + normalgroup() : flat(0), normals(-1), tnormals(-1) {} + normalgroup(const vec &pos) : pos(pos), flat(0), normals(-1), tnormals(-1) {} +}; + +static inline bool htcmp(const vec &v, const normalgroup &n) { return v == n.pos; } + +struct normal +{ + int next; + vec surface; +}; + +struct tnormal +{ + int next; + float offset; + int normals[2]; + normalgroup *groups[2]; +}; + +hashset normalgroups(1<<16); +vector normals; +vector tnormals; + +VARR(lerpangle, 0, 44, 180); + +static float lerpthreshold = 0; +static bool usetnormals = true; + +static int addnormal(const vec &key, const vec &surface) +{ + normalgroup &g = normalgroups.access(key, key); + normal &n = normals.add(); + n.next = g.normals; + n.surface = surface; + return g.normals = normals.length()-1; +} + +static void addtnormal(const vec &key, float offset, int normal1, int normal2, normalgroup *group1, normalgroup *group2) +{ + normalgroup &g = normalgroups.access(key, key); + tnormal &n = tnormals.add(); + n.next = g.tnormals; + n.offset = offset; + n.normals[0] = normal1; + n.normals[1] = normal2; + n.groups[0] = group1; + n.groups[1] = group2; + g.tnormals = tnormals.length()-1; +} + +static int addnormal(const vec &key, int axis) +{ + normalgroup &g = normalgroups.access(key, key); + g.flat += 1<<(4*axis); + return axis - 6; +} + +static inline void findnormal(const normalgroup &g, const vec &surface, vec &v) +{ + v = vec(0, 0, 0); + int total = 0; + if(surface.x >= lerpthreshold) { int n = (g.flat>>4)&0xF; v.x += n; total += n; } + else if(surface.x <= -lerpthreshold) { int n = g.flat&0xF; v.x -= n; total += n; } + if(surface.y >= lerpthreshold) { int n = (g.flat>>12)&0xF; v.y += n; total += n; } + else if(surface.y <= -lerpthreshold) { int n = (g.flat>>8)&0xF; v.y -= n; total += n; } + if(surface.z >= lerpthreshold) { int n = (g.flat>>20)&0xF; v.z += n; total += n; } + else if(surface.z <= -lerpthreshold) { int n = (g.flat>>16)&0xF; v.z -= n; total += n; } + for(int cur = g.normals; cur >= 0;) + { + normal &o = normals[cur]; + if(o.surface.dot(surface) >= lerpthreshold) + { + v.add(o.surface); + total++; + } + cur = o.next; + } + if(total > 1) v.normalize(); + else if(!total) v = surface; +} + +static inline bool findtnormal(const normalgroup &g, const vec &surface, vec &v) +{ + float bestangle = lerpthreshold; + tnormal *bestnorm = NULL; + for(int cur = g.tnormals; cur >= 0;) + { + tnormal &o = tnormals[cur]; + static const vec flats[6] = { vec(-1, 0, 0), vec(1, 0, 0), vec(0, -1, 0), vec(0, 1, 0), vec(0, 0, -1), vec(0, 0, 1) }; + vec n1 = o.normals[0] < 0 ? flats[o.normals[0]+6] : normals[o.normals[0]].surface, + n2 = o.normals[1] < 0 ? flats[o.normals[1]+6] : normals[o.normals[1]].surface, + nt; + nt.lerp(n1, n2, o.offset).normalize(); + float tangle = nt.dot(surface); + if(tangle >= bestangle) + { + bestangle = tangle; + bestnorm = &o; + } + cur = o.next; + } + if(!bestnorm) return false; + vec n1, n2; + findnormal(*bestnorm->groups[0], surface, n1); + findnormal(*bestnorm->groups[1], surface, n2); + v.lerp(n1, n2, bestnorm->offset).normalize(); + return true; +} + +void findnormal(const vec &key, const vec &surface, vec &v) +{ + const normalgroup *g = normalgroups.access(key); + if(!g) v = surface; + else if(g->tnormals < 0 || !findtnormal(*g, surface, v)) + findnormal(*g, surface, v); +} + +VARR(lerpsubdiv, 0, 2, 4); +VARR(lerpsubdivsize, 4, 4, 128); + +static uint progress = 0; + +void show_addnormals_progress() +{ + float bar1 = float(progress) / float(allocnodes); + renderprogress(bar1, "computing normals..."); +} + +void addnormals(cube &c, const ivec &o, int size) +{ + CHECK_CALCLIGHT_PROGRESS(return, show_addnormals_progress); + + if(c.children) + { + progress++; + size >>= 1; + loopi(8) addnormals(c.children[i], ivec(i, o, size), size); + return; + } + else if(isempty(c)) return; + + vec pos[MAXFACEVERTS]; + int norms[MAXFACEVERTS]; + int tj = usetnormals && c.ext ? c.ext->tjoints : -1, vis; + loopi(6) if((vis = visibletris(c, i, o, size))) + { + CHECK_CALCLIGHT_PROGRESS(return, show_addnormals_progress); + if(c.texture[i] == DEFAULT_SKY) continue; + + vec planes[2]; + int numverts = c.ext ? c.ext->surfaces[i].numverts&MAXFACEVERTS : 0, convex = 0, numplanes = 0; + if(numverts) + { + vertinfo *verts = c.ext->verts() + c.ext->surfaces[i].verts; + vec vo(ivec(o).mask(~0xFFF)); + loopj(numverts) + { + vertinfo &v = verts[j]; + pos[j] = vec(v.x, v.y, v.z).mul(1.0f/8).add(vo); + } + if(!(c.merged&(1<= 0 && tjoints[tj].edge < i*(MAXFACEVERTS+1)) tj = tjoints[tj].next; + while(tj >= 0 && tjoints[tj].edge < (i+1)*(MAXFACEVERTS+1)) + { + int edge = tjoints[tj].edge, e1 = edge%(MAXFACEVERTS+1), e2 = (e1+1)%numverts; + const vec &v1 = pos[e1], &v2 = pos[e2]; + ivec d(vec(v2).sub(v1).mul(8)); + int axis = abs(d.x) > abs(d.y) ? (abs(d.x) > abs(d.z) ? 0 : 2) : (abs(d.y) > abs(d.z) ? 1 : 2); + if(d[axis] < 0) d.neg(); + reduceslope(d); + int origin = int(min(v1[axis], v2[axis])*8)&~0x7FFF, + offset1 = (int(v1[axis]*8) - origin) / d[axis], + offset2 = (int(v2[axis]*8) - origin) / d[axis]; + vec o = vec(v1).sub(vec(d).mul(offset1/8.0f)), n1, n2; + float doffset = 1.0f / (offset2 - offset1); + + while(tj >= 0) + { + tjoint &t = tjoints[tj]; + if(t.edge != edge) break; + float offset = (t.offset - offset1) * doffset; + vec tpos = vec(d).mul(t.offset/8.0f).add(o); + addtnormal(tpos, offset, norms[e1], norms[e2], normalgroups.access(v1), normalgroups.access(v2)); + tj = t.next; + } + } + } +} + +void calcnormals(bool lerptjoints) +{ + if(!lerpangle) return; + usetnormals = lerptjoints; + if(usetnormals) findtjoints(); + lerpthreshold = cos(lerpangle*RAD) - 1e-5f; + progress = 1; + loopi(8) addnormals(worldroot[i], ivec(i, ivec(0, 0, 0), worldsize/2), worldsize/2); +} + +void clearnormals() +{ + normalgroups.clear(); + normals.setsize(0); + tnormals.setsize(0); +} + +void calclerpverts(const vec2 *c, const vec *n, lerpvert *lv, int &numv) +{ + int i = 0; + loopj(numv) + { + if(j) + { + if(c[j] == c[j-1] && n[j] == n[j-1]) continue; + if(j == numv-1 && c[j] == c[0] && n[j] == n[0]) continue; + } + lv[i].normal = n[j]; + lv[i].tc = c[j]; + i++; + } + numv = i; +} + +void setlerpstep(float v, lerpbounds &bounds) +{ + if(bounds.min->tc.y + 1 > bounds.max->tc.y) + { + bounds.nstep = vec(0, 0, 0); + bounds.normal = bounds.min->normal; + if(bounds.min->normal != bounds.max->normal) + { + bounds.normal.add(bounds.max->normal); + bounds.normal.normalize(); + } + bounds.ustep = 0; + bounds.u = bounds.min->tc.x; + return; + } + + bounds.nstep = bounds.max->normal; + bounds.nstep.sub(bounds.min->normal); + bounds.nstep.div(bounds.max->tc.y-bounds.min->tc.y); + + bounds.normal = bounds.nstep; + bounds.normal.mul(v - bounds.min->tc.y); + bounds.normal.add(bounds.min->normal); + + bounds.ustep = (bounds.max->tc.x-bounds.min->tc.x) / (bounds.max->tc.y-bounds.min->tc.y); + bounds.u = bounds.ustep * (v-bounds.min->tc.y) + bounds.min->tc.x; +} + +void initlerpbounds(float u, float v, const lerpvert *lv, int numv, lerpbounds &start, lerpbounds &end) +{ + const lerpvert *first = &lv[0], *second = NULL; + loopi(numv-1) + { + if(lv[i+1].tc.y < first->tc.y) { second = first; first = &lv[i+1]; } + else if(!second || lv[i+1].tc.y < second->tc.y) second = &lv[i+1]; + } + + if(int(first->tc.y) < int(second->tc.y)) { start.min = end.min = first; } + else if(first->tc.x > second->tc.x) { start.min = second; end.min = first; } + else { start.min = first; end.min = second; } + + if((lv[1].tc.x - lv->tc.x)*(lv[2].tc.y - lv->tc.y) > (lv[1].tc.y - lv->tc.y)*(lv[2].tc.x - lv->tc.x)) + { + start.winding = end.winding = 1; + start.max = (start.min == lv ? &lv[numv-1] : start.min-1); + end.max = (end.min == &lv[numv-1] ? lv : end.min+1); + } + else + { + start.winding = end.winding = -1; + start.max = (start.min == &lv[numv-1] ? lv : start.min+1); + end.max = (end.min == lv ? &lv[numv-1] : end.min-1); + } + + setlerpstep(v, start); + setlerpstep(v, end); +} + +void updatelerpbounds(float v, const lerpvert *lv, int numv, lerpbounds &start, lerpbounds &end) +{ + if(v >= start.max->tc.y) + { + const lerpvert *next = start.winding > 0 ? + (start.max == lv ? &lv[numv-1] : start.max-1) : + (start.max == &lv[numv-1] ? lv : start.max+1); + if(next->tc.y > start.max->tc.y) + { + start.min = start.max; + start.max = next; + setlerpstep(v, start); + } + } + if(v >= end.max->tc.y) + { + const lerpvert *next = end.winding > 0 ? + (end.max == &lv[numv-1] ? lv : end.max+1) : + (end.max == lv ? &lv[numv-1] : end.max-1); + if(next->tc.y > end.max->tc.y) + { + end.min = end.max; + end.max = next; + setlerpstep(v, end); + } + } +} + +void lerpnormal(float u, float v, const lerpvert *lv, int numv, lerpbounds &start, lerpbounds &end, vec &normal, vec &nstep) +{ + updatelerpbounds(v, lv, numv, start, end); + + if(start.u + 1 > end.u) + { + nstep = vec(0, 0, 0); + normal = start.normal; + normal.add(end.normal); + normal.normalize(); + } + else + { + vec nstart(start.normal), nend(end.normal); + nstart.normalize(); + nend.normalize(); + + nstep = nend; + nstep.sub(nstart); + nstep.div(end.u-start.u); + + normal = nstep; + normal.mul(u-start.u); + normal.add(nstart); + normal.normalize(); + } + + start.normal.add(start.nstep); + start.u += start.ustep; + + end.normal.add(end.nstep); + end.u += end.ustep; +} + diff --git a/src/engine/obj.h b/src/engine/obj.h new file mode 100644 index 0000000..9cdf46a --- /dev/null +++ b/src/engine/obj.h @@ -0,0 +1,191 @@ +struct obj; + +struct obj : vertloader +{ + obj(const char *name) : vertloader(name) {} + + static const char *formatname() { return "obj"; } + static bool animated() { return false; } + bool flipy() const { return true; } + int type() const { return MDL_OBJ; } + + struct objmeshgroup : vertmeshgroup + { + void parsevert(char *s, vector &out) + { + vec &v = out.add(vec(0, 0, 0)); + while(isalpha(*s)) s++; + loopi(3) + { + v[i] = strtod(s, &s); + while(isspace(*s)) s++; + if(!*s) break; + } + } + + bool load(const char *filename, float smooth) + { + int len = strlen(filename); + if(len < 4 || strcasecmp(&filename[len-4], ".obj")) return false; + + stream *file = openfile(filename, "rb"); + if(!file) return false; + + name = newstring(filename); + + numframes = 1; + + vector attrib[3]; + char buf[512]; + + hashtable verthash; + vector verts; + vector tcverts; + vector tris; + + #define STARTMESH do { \ + vertmesh &m = *new vertmesh; \ + m.group = this; \ + m.name = meshname[0] ? newstring(meshname) : NULL; \ + meshes.add(&m); \ + curmesh = &m; \ + verthash.clear(); \ + verts.setsize(0); \ + tcverts.setsize(0); \ + tris.setsize(0); \ + } while(0) + + #define FLUSHMESH do { \ + curmesh->numverts = verts.length(); \ + if(verts.length()) \ + { \ + curmesh->verts = new vert[verts.length()]; \ + memcpy(curmesh->verts, verts.getbuf(), verts.length()*sizeof(vert)); \ + curmesh->tcverts = new tcvert[verts.length()]; \ + memcpy(curmesh->tcverts, tcverts.getbuf(), tcverts.length()*sizeof(tcvert)); \ + } \ + curmesh->numtris = tris.length(); \ + if(tris.length()) \ + { \ + curmesh->tris = new tri[tris.length()]; \ + memcpy(curmesh->tris, tris.getbuf(), tris.length()*sizeof(tri)); \ + } \ + if(attrib[2].empty()) \ + { \ + if(smooth <= 1) curmesh->smoothnorms(smooth); \ + else curmesh->buildnorms(); \ + } \ + } while(0) + + string meshname = ""; + vertmesh *curmesh = NULL; + while(file->getline(buf, sizeof(buf))) + { + char *c = buf; + while(isspace(*c)) c++; + switch(*c) + { + case '#': continue; + case 'v': + if(isspace(c[1])) parsevert(c, attrib[0]); + else if(c[1]=='t') parsevert(c, attrib[1]); + else if(c[1]=='n') parsevert(c, attrib[2]); + break; + case 'g': + { + while(isalpha(*c)) c++; + while(isspace(*c)) c++; + char *name = c; + size_t namelen = strlen(name); + while(namelen > 0 && isspace(name[namelen-1])) namelen--; + copystring(meshname, name, min(namelen+1, sizeof(meshname))); + + if(curmesh) FLUSHMESH; + curmesh = NULL; + break; + } + case 'f': + { + if(!curmesh) STARTMESH; + int v0 = -1, v1 = -1; + while(isalpha(*c)) c++; + for(;;) + { + while(isspace(*c)) c++; + if(!*c) break; + ivec vkey(-1, -1, -1); + loopi(3) + { + vkey[i] = strtol(c, &c, 10); + if(vkey[i] < 0) vkey[i] = attrib[i].length() + vkey[i]; + else vkey[i]--; + if(!attrib[i].inrange(vkey[i])) vkey[i] = -1; + if(*c!='/') break; + c++; + } + int *index = verthash.access(vkey); + if(!index) + { + index = &verthash[vkey]; + *index = verts.length(); + vert &v = verts.add(); + v.pos = vkey.x < 0 ? vec(0, 0, 0) : attrib[0][vkey.x]; + v.pos = vec(v.pos.z, -v.pos.x, v.pos.y); + v.norm = vkey.z < 0 ? vec(0, 0, 0) : attrib[2][vkey.z]; + v.norm = vec(v.norm.z, -v.norm.x, v.norm.y); + tcvert &tcv = tcverts.add(); + tcv.tc = vkey.y < 0 ? vec2(0, 0) : vec2(attrib[1][vkey.y].x, 1-attrib[1][vkey.y].y); + } + if(v0 < 0) v0 = *index; + else if(v1 < 0) v1 = *index; + else + { + tri &t = tris.add(); + t.vert[0] = ushort(*index); + t.vert[1] = ushort(v1); + t.vert[2] = ushort(v0); + v1 = *index; + } + } + break; + } + } + } + + if(curmesh) FLUSHMESH; + + delete file; + + return true; + } + }; + + meshgroup *loadmeshes(const char *name, va_list args) + { + objmeshgroup *group = new objmeshgroup; + if(!group->load(name, va_arg(args, double))) { delete group; return NULL; } + return group; + } + + bool loaddefaultparts() + { + part &mdl = addpart(); + const char *pname = parentdir(name); + defformatstring(name1, "packages/models/%s/tris.obj", name); + mdl.meshes = sharemeshes(path(name1), 2.0); + if(!mdl.meshes) + { + defformatstring(name2, "packages/models/%s/tris.obj", pname); // try obj in parent folder (vert sharing) + mdl.meshes = sharemeshes(path(name2), 2.0); + if(!mdl.meshes) return false; + } + Texture *tex, *masks; + loadskin(name, pname, tex, masks); + mdl.initskins(tex, masks); + if(tex==notexture) conoutf(CON_ERROR, "could not load model skin for %s", name1); + return true; + } +}; + +vertcommands objcommands; + diff --git a/src/engine/octa.cpp b/src/engine/octa.cpp new file mode 100644 index 0000000..e4f0901 --- /dev/null +++ b/src/engine/octa.cpp @@ -0,0 +1,1880 @@ +// core world management routines + +#include "engine.h" + +cube *worldroot = newcubes(F_SOLID); +int allocnodes = 0; + +cubeext *growcubeext(cubeext *old, int maxverts) +{ + cubeext *ext = (cubeext *)new uchar[sizeof(cubeext) + maxverts*sizeof(vertinfo)]; + if(old) + { + ext->va = old->va; + ext->ents = old->ents; + ext->tjoints = old->tjoints; + } + else + { + ext->va = NULL; + ext->ents = NULL; + ext->tjoints = -1; + } + ext->maxverts = maxverts; + return ext; +} + +void setcubeext(cube &c, cubeext *ext) +{ + cubeext *old = c.ext; + if(old == ext) return; + c.ext = ext; + if(old) delete[] (uchar *)old; +} + +cubeext *newcubeext(cube &c, int maxverts, bool init) +{ + if(c.ext && c.ext->maxverts >= maxverts) return c.ext; + cubeext *ext = growcubeext(c.ext, maxverts); + if(init) + { + if(c.ext) + { + memcpy(ext->surfaces, c.ext->surfaces, sizeof(ext->surfaces)); + memcpy(ext->verts(), c.ext->verts(), c.ext->maxverts*sizeof(vertinfo)); + } + else memset(ext->surfaces, 0, sizeof(ext->surfaces)); + } + setcubeext(c, ext); + return ext; +} + +cube *newcubes(uint face, int mat) +{ + cube *c = new cube[8]; + loopi(8) + { + c->children = NULL; + c->ext = NULL; + c->visible = 0; + c->merged = 0; + setfaces(*c, face); + loopl(6) c->texture[l] = DEFAULT_GEOM; + c->material = mat; + c++; + } + allocnodes++; + return c-8; +} + +int familysize(const cube &c) +{ + int size = 1; + if(c.children) loopi(8) size += familysize(c.children[i]); + return size; +} + +void freeocta(cube *c) +{ + if(!c) return; + loopi(8) discardchildren(c[i]); + delete[] c; + allocnodes--; +} + +void freecubeext(cube &c) +{ + if(c.ext) + { + delete[] (uchar *)c.ext; + c.ext = NULL; + } +} + +void discardchildren(cube &c, bool fixtex, int depth) +{ + c.material = MAT_AIR; + c.visible = 0; + c.merged = 0; + if(c.ext) + { + if(c.ext->va) destroyva(c.ext->va); + c.ext->va = NULL; + c.ext->tjoints = -1; + freeoctaentities(c); + freecubeext(c); + } + if(c.children) + { + uint filled = F_EMPTY; + loopi(8) + { + discardchildren(c.children[i], fixtex, depth+1); + filled |= c.children[i].faces[0]; + } + if(fixtex) + { + loopi(6) c.texture[i] = getmippedtexture(c, i); + if(depth > 0 && filled != F_EMPTY) c.faces[0] = F_SOLID; + } + DELETEA(c.children); + allocnodes--; + } +} + +void getcubevector(cube &c, int d, int x, int y, int z, ivec &p) +{ + ivec v(d, x, y, z); + + loopi(3) + p[i] = edgeget(cubeedge(c, i, v[R[i]], v[C[i]]), v[D[i]]); +} + +void setcubevector(cube &c, int d, int x, int y, int z, const ivec &p) +{ + ivec v(d, x, y, z); + + loopi(3) + edgeset(cubeedge(c, i, v[R[i]], v[C[i]]), v[D[i]], p[i]); +} + +static inline void getcubevector(cube &c, int i, ivec &p) +{ + p.x = edgeget(cubeedge(c, 0, (i>>R[0])&1, (i>>C[0])&1), (i>>D[0])&1); + p.y = edgeget(cubeedge(c, 1, (i>>R[1])&1, (i>>C[1])&1), (i>>D[1])&1); + p.z = edgeget(cubeedge(c, 2, (i>>R[2])&1, (i>>C[2])&1), (i>>D[2])&1); +} + +static inline void setcubevector(cube &c, int i, const ivec &p) +{ + edgeset(cubeedge(c, 0, (i>>R[0])&1, (i>>C[0])&1), (i>>D[0])&1, p.x); + edgeset(cubeedge(c, 1, (i>>R[1])&1, (i>>C[1])&1), (i>>D[1])&1, p.y); + edgeset(cubeedge(c, 2, (i>>R[2])&1, (i>>C[2])&1), (i>>D[2])&1, p.z); +} + +void optiface(uchar *p, cube &c) +{ + uint f = *(uint *)p; + if(((f>>4)&0x0F0F0F0FU) == (f&0x0F0F0F0FU)) emptyfaces(c); +} + +void printcube() +{ + cube &c = lookupcube(lu); // assume this is cube being pointed at + conoutf(CON_DEBUG, "= %p = (%d, %d, %d) @ %d", (void *)&c, lu.x, lu.y, lu.z, lusize); + conoutf(CON_DEBUG, " x %.8x", c.faces[0]); + conoutf(CON_DEBUG, " y %.8x", c.faces[1]); + conoutf(CON_DEBUG, " z %.8x", c.faces[2]); +} + +COMMAND(printcube, ""); + +bool isvalidcube(const cube &c) +{ + clipplanes p; + genclipplanes(c, ivec(0, 0, 0), 256, p); + loopi(8) // test that cube is convex + { + vec v = p.v[i]; + loopj(p.size) if(p.p[j].dist(v)>1e-3f) return false; + } + return true; +} + +void validatec(cube *c, int size) +{ + loopi(8) + { + if(c[i].children) + { + if(size<=1) + { + solidfaces(c[i]); + discardchildren(c[i], true); + } + else validatec(c[i].children, size>>1); + } + else if(size > 0x1000) + { + subdividecube(c[i], true, false); + validatec(c[i].children, size>>1); + } + else + { + loopj(3) + { + uint f = c[i].faces[j], e0 = f&0x0F0F0F0FU, e1 = (f>>4)&0x0F0F0F0FU; + if(e0 == e1 || ((e1+0x07070707U)|(e1-e0))&0xF0F0F0F0U) + { + emptyfaces(c[i]); + break; + } + } + } + } +} + +ivec lu; +int lusize; +cube &lookupcube(const ivec &to, int tsize, ivec &ro, int &rsize) +{ + int tx = clamp(to.x, 0, worldsize-1), + ty = clamp(to.y, 0, worldsize-1), + tz = clamp(to.z, 0, worldsize-1); + int scale = worldscale-1, csize = abs(tsize); + cube *c = &worldroot[octastep(tx, ty, tz, scale)]; + if(!(csize>>scale)) do + { + if(!c->children) + { + if(tsize > 0) do + { + subdividecube(*c); + scale--; + c = &c->children[octastep(tx, ty, tz, scale)]; + } while(!(csize>>scale)); + break; + } + scale--; + c = &c->children[octastep(tx, ty, tz, scale)]; + } while(!(csize>>scale)); + ro = ivec(tx, ty, tz).mask(~0U<children) + { + scale--; + c = &c->children[octastep(o.x, o.y, o.z, scale)]; + } + return c->material; +} + +const cube *neighbourstack[32]; +int neighbourdepth = -1; + +const cube &neighbourcube(const cube &c, int orient, const ivec &co, int size, ivec &ro, int &rsize) +{ + ivec n = co; + int dim = dimension(orient); + uint diff = n[dim]; + if(dimcoord(orient)) n[dim] += size; else n[dim] -= size; + diff ^= n[dim]; + if(diff >= uint(worldsize)) { ro = n; rsize = size; return c; } + int scale = worldscale; + const cube *nc = worldroot; + if(neighbourdepth >= 0) + { + scale -= neighbourdepth + 1; + diff >>= scale; + do { scale++; diff >>= 1; } while(diff); + nc = neighbourstack[worldscale - scale]; + } + scale--; + nc = &nc[octastep(n.x, n.y, n.z, scale)]; + if(!(size>>scale) && nc->children) do + { + scale--; + nc = &nc->children[octastep(n.x, n.y, n.z, scale)]; + } while(!(size>>scale) && nc->children); + ro = n.mask(~0U< DEFAULT_SKY) loopi(numtexs) if(texs[i] == tex) return tex; + texs[numtexs++] = tex; + } + loopirev(numtexs) if(!i || texs[i] > DEFAULT_SKY) return texs[i]; + return DEFAULT_GEOM; +} + +void forcemip(cube &c, bool fixtex) +{ + cube *ch = c.children; + emptyfaces(c); + + loopi(8) loopj(8) + { + int n = i^(j==3 ? 4 : (j==4 ? 3 : j)); + if(!isempty(ch[n])) // breadth first search for cube near vert + { + ivec v; + getcubevector(ch[n], i, v); + // adjust vert to parent size + setcubevector(c, i, ivec(n, v, 8).shr(1)); + break; + } + } + + if(fixtex) loopj(6) + c.texture[j] = getmippedtexture(c, j); +} + +static int midedge(const ivec &a, const ivec &b, int xd, int yd, bool &perfect) +{ + int ax = a[xd], ay = a[yd], bx = b[xd], by = b[yd]; + if(ay==by) return ay; + if(ax==bx) { perfect = false; return ay; } + bool crossx = (ax<8 && bx>8) || (ax>8 && bx<8); + bool crossy = (ay<8 && by>8) || (ay>8 && by<8); + if(crossy && !crossx) { midedge(a,b,yd,xd,perfect); return 8; } // to test perfection + if(ax<=8 && bx<=8) return ax>bx ? ay : by; + if(ax>=8 && bx>=8) return ax16)) perfect = false; + return crossy ? 8 : min(max(y, 0), 16); +} + +static inline bool crosscenter(const ivec &a, const ivec &b, int xd, int yd) +{ + int ax = a[xd], ay = a[yd], bx = b[xd], by = b[yd]; + return (((ax <= 8 && bx <= 8) || (ax >= 8 && bx >= 8)) && + ((ay <= 8 && by <= 8) || (ay >= 8 && by >= 8))) || + (ax + bx == 16 && ay + by == 16); +} + +bool subdividecube(cube &c, bool fullcheck, bool brighten) +{ + if(c.children) return true; + if(c.ext) memset(c.ext->surfaces, 0, sizeof(c.ext->surfaces)); + if(isempty(c) || isentirelysolid(c)) + { + c.children = newcubes(isempty(c) ? F_EMPTY : F_SOLID, c.material); + loopi(8) + { + loopl(6) c.children[i].texture[l] = c.texture[l]; + if(brighten && !isempty(c)) brightencube(c.children[i]); + } + return true; + } + cube *ch = c.children = newcubes(F_SOLID, c.material); + bool perfect = true; + ivec v[8]; + loopi(8) + { + getcubevector(c, i, v[i]); + v[i].mul(2); + } + + loopj(6) + { + int d = dimension(j), z = dimcoord(j); + const ivec &v00 = v[octaindex(d, 0, 0, z)], + &v10 = v[octaindex(d, 1, 0, z)], + &v01 = v[octaindex(d, 0, 1, z)], + &v11 = v[octaindex(d, 1, 1, z)]; + int e[3][3]; + // corners + e[0][0] = v00[d]; + e[0][2] = v01[d]; + e[2][0] = v10[d]; + e[2][2] = v11[d]; + // edges + e[0][1] = midedge(v00, v01, C[d], d, perfect); + e[1][0] = midedge(v00, v10, R[d], d, perfect); + e[1][2] = midedge(v11, v01, R[d], d, perfect); + e[2][1] = midedge(v11, v10, C[d], d, perfect); + // center + bool p1 = perfect, p2 = perfect; + int c1 = midedge(v00, v11, R[d], d, p1); + int c2 = midedge(v01, v10, R[d], d, p2); + if(z ? c1 > c2 : c1 < c2) + { + e[1][1] = c1; + perfect = p1 && (c1 == c2 || crosscenter(v00, v11, C[d], R[d])); + } + else + { + e[1][1] = c2; + perfect = p2 && (c1 == c2 || crosscenter(v01, v10, C[d], R[d])); + } + + loopi(8) + { + ch[i].texture[j] = c.texture[j]; + int rd = (i>>R[d])&1, cd = (i>>C[d])&1, dd = (i>>D[d])&1; + edgeset(cubeedge(ch[i], d, 0, 0), z, clamp(e[rd][cd] - dd*8, 0, 8)); + edgeset(cubeedge(ch[i], d, 1, 0), z, clamp(e[1+rd][cd] - dd*8, 0, 8)); + edgeset(cubeedge(ch[i], d, 0, 1), z, clamp(e[rd][1+cd] - dd*8, 0, 8)); + edgeset(cubeedge(ch[i], d, 1, 1), z, clamp(e[1+rd][1+cd] - dd*8, 0, 8)); + } + } + + validatec(ch); + if(fullcheck) loopi(8) if(!isvalidcube(ch[i])) // not so good... + { + emptyfaces(ch[i]); + perfect=false; + } + if(brighten) loopi(8) if(!isempty(ch[i])) brightencube(ch[i]); + return perfect; +} + +bool crushededge(uchar e, int dc) { return dc ? e==0 : e==0x88; } + +int visibleorient(const cube &c, int orient) +{ + loopi(2) + { + int a = faceedgesidx[orient][i*2 + 0]; + int b = faceedgesidx[orient][i*2 + 1]; + loopj(2) + { + if(crushededge(c.edges[a],j) && + crushededge(c.edges[b],j) && + touchingface(c, orient)) return ((a>>2)<<1) + j; + } + } + return orient; +} + +VAR(mipvis, 0, 0, 1); + +static int remipprogress = 0, remiptotal = 0; + +bool remip(cube &c, const ivec &co, int size) +{ + cube *ch = c.children; + if(!ch) + { + if(size<<1 <= 0x1000) return true; + subdividecube(c); + ch = c.children; + } + else if((remipprogress++&0xFFF)==1) renderprogress(float(remipprogress)/remiptotal, "remipping..."); + + bool perfect = true; + loopi(8) + { + ivec o(i, co, size); + if(!remip(ch[i], o, size>>1)) perfect = false; + } + + solidfaces(c); // so texmip is more consistent + loopj(6) + c.texture[j] = getmippedtexture(c, j); // parents get child texs regardless + + if(!perfect) return false; + if(size<<1 > 0x1000) return false; + + ushort mat = MAT_AIR; + loopi(8) + { + mat = ch[i].material; + if((mat&MATF_CLIP) == MAT_NOCLIP || mat&MAT_ALPHA) + { + if(i > 0) return false; + while(++i < 8) if(ch[i].material != mat) return false; + break; + } + else if(!isentirelysolid(ch[i])) + { + while(++i < 8) + { + int omat = ch[i].material; + if(isentirelysolid(ch[i]) ? (omat&MATF_CLIP) == MAT_NOCLIP || omat&MAT_ALPHA : mat != omat) return false; + } + break; + } + } + + cube n = c; + n.ext = NULL; + forcemip(n); + n.children = NULL; + if(!subdividecube(n, false, false)) + { freeocta(n.children); return false; } + + cube *nh = n.children; + uchar vis[6] = {0, 0, 0, 0, 0, 0}; + loopi(8) + { + if(ch[i].faces[0] != nh[i].faces[0] || + ch[i].faces[1] != nh[i].faces[1] || + ch[i].faces[2] != nh[i].faces[2]) + { freeocta(nh); return false; } + + if(isempty(ch[i]) && isempty(nh[i])) continue; + + ivec o(i, co, size); + loop(orient, 6) + if(visibleface(ch[i], orient, o, size, MAT_AIR, (mat&MAT_ALPHA)^MAT_ALPHA, MAT_ALPHA)) + { + if(ch[i].texture[orient] != n.texture[orient]) { freeocta(nh); return false; } + vis[orient] |= 1<>1); + remip(worldroot[i], o, worldsize>>2); + } + calcmerges(); + if(!local) allchanged(); +} + +void remip_() +{ + mpremip(true); + allchanged(); +} + +COMMANDN(remip, remip_, ""); + +static inline int edgeval(cube &c, const ivec &p, int dim, int coord) +{ + return edgeget(cubeedge(c, dim, p[R[dim]]>>3, p[C[dim]]>>3), coord); +} + +void genvertp(cube &c, ivec &p1, ivec &p2, ivec &p3, plane &pl, bool solid = false) +{ + int dim = 0; + if(p1.y==p2.y && p2.y==p3.y) dim = 1; + else if(p1.z==p2.z && p2.z==p3.z) dim = 2; + + int coord = p1[dim]; + ivec v1(p1), v2(p2), v3(p3); + v1[dim] = solid ? coord*8 : edgeval(c, p1, dim, coord); + v2[dim] = solid ? coord*8 : edgeval(c, p2, dim, coord); + v3[dim] = solid ? coord*8 : edgeval(c, p3, dim, coord); + + pl.toplane(vec(v1), vec(v2), vec(v3)); +} + +static bool threeplaneintersect(plane &pl1, plane &pl2, plane &pl3, vec &dest) +{ + vec &t1 = dest, t2, t3, t4; + t1.cross(pl1, pl2); t4 = t1; t1.mul(pl3.offset); + t2.cross(pl3, pl1); t2.mul(pl2.offset); + t3.cross(pl2, pl3); t3.mul(pl1.offset); + t1.add(t2); + t1.add(t3); + t1.mul(-1); + float d = t4.dot(pl3); + if(d==0) return false; + t1.div(d); + return true; +} + +static void genedgespanvert(ivec &p, cube &c, vec &v) +{ + ivec p1(8-p.x, p.y, p.z); + ivec p2(p.x, 8-p.y, p.z); + ivec p3(p.x, p.y, 8-p.z); + + plane plane1, plane2, plane3; + genvertp(c, p, p1, p2, plane1); + genvertp(c, p, p2, p3, plane2); + genvertp(c, p, p3, p1, plane3); + if(plane1==plane2) genvertp(c, p, p1, p2, plane1, true); + if(plane1==plane3) genvertp(c, p, p1, p2, plane1, true); + if(plane2==plane3) genvertp(c, p, p2, p3, plane2, true); + + ASSERT(threeplaneintersect(plane1, plane2, plane3, v)); + //ASSERT(v.x>=0 && v.x<=8); + //ASSERT(v.y>=0 && v.y<=8); + //ASSERT(v.z>=0 && v.z<=8); + v.x = max(0.0f, min(8.0f, v.x)); + v.y = max(0.0f, min(8.0f, v.y)); + v.z = max(0.0f, min(8.0f, v.z)); +} + +void edgespan2vectorcube(cube &c) +{ + if(isentirelysolid(c) || isempty(c)) return; + cube o = c; + loop(x, 2) loop(y, 2) loop(z, 2) + { + ivec p(8*x, 8*y, 8*z); + vec v; + genedgespanvert(p, o, v); + + edgeset(cubeedge(c, 0, y, z), x, int(v.x+0.49f)); + edgeset(cubeedge(c, 1, z, x), y, int(v.y+0.49f)); + edgeset(cubeedge(c, 2, x, y), z, int(v.z+0.49f)); + } +} + +const ivec cubecoords[8] = // verts of bounding cube +{ +#define GENCUBEVERT(n, x, y, z) ivec(x, y, z), + GENCUBEVERTS(0, 8, 0, 8, 0, 8) +#undef GENCUBEVERT +}; + +template +static inline void gencubevert(const cube &c, int i, T &v) +{ + switch(i) + { + default: +#define GENCUBEVERT(n, x, y, z) \ + case n: \ + v = T(edgeget(cubeedge(c, 0, y, z), x), \ + edgeget(cubeedge(c, 1, z, x), y), \ + edgeget(cubeedge(c, 2, x, y), z)); \ + break; + GENCUBEVERTS(0, 1, 0, 1, 0, 1) +#undef GENCUBEVERT + } +} + +void genfaceverts(const cube &c, int orient, ivec v[4]) +{ + switch(orient) + { + default: +#define GENFACEORIENT(o, v0, v1, v2, v3) \ + case o: v0 v1 v2 v3 break; +#define GENFACEVERT(o, n, x,y,z, xv,yv,zv) \ + v[n] = ivec(edgeget(cubeedge(c, 0, y, z), x), \ + edgeget(cubeedge(c, 1, z, x), y), \ + edgeget(cubeedge(c, 2, x, y), z)); + GENFACEVERTS(0, 1, 0, 1, 0, 1, , , , , , ) + #undef GENFACEORIENT + #undef GENFACEVERT + } +} + +const ivec facecoords[6][4] = +{ +#define GENFACEORIENT(o, v0, v1, v2, v3) \ + { v0, v1, v2, v3 }, +#define GENFACEVERT(o, n, x,y,z, xv,yv,zv) \ + ivec(x,y,z) + GENFACEVERTS(0, 8, 0, 8, 0, 8, , , , , , ) +#undef GENFACEORIENT +#undef GENFACEVERT +}; + +const uchar fv[6][4] = // indexes for cubecoords, per each vert of a face orientation +{ + { 2, 1, 6, 5 }, + { 3, 4, 7, 0 }, + { 4, 5, 6, 7 }, + { 1, 2, 3, 0 }, + { 6, 1, 0, 7 }, + { 5, 4, 3, 2 }, +}; + +const uchar fvmasks[64] = // mask of verts used given a mask of visible face orientations +{ + 0x00, 0x66, 0x99, 0xFF, 0xF0, 0xF6, 0xF9, 0xFF, + 0x0F, 0x6F, 0x9F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xC3, 0xE7, 0xDB, 0xFF, 0xF3, 0xF7, 0xFB, 0xFF, + 0xCF, 0xEF, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x3C, 0x7E, 0xBD, 0xFF, 0xFC, 0xFE, 0xFD, 0xFF, + 0x3F, 0x7F, 0xBF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, +}; + +const uchar faceedgesidx[6][4] = // ordered edges surrounding each orient +{//0..1 = row edges, 2..3 = column edges + { 4, 5, 8, 10 }, + { 6, 7, 9, 11 }, + { 8, 9, 0, 2 }, + { 10, 11, 1, 3 }, + { 0, 1, 4, 6 }, + { 2, 3, 5, 7 }, +}; + +bool flataxisface(const cube &c, int orient) +{ + uint face = c.faces[dimension(orient)]; + if(dimcoord(orient)) face >>= 4; + return (face&0x0F0F0F0F) == 0x01010101*(face&0x0F); +} + +bool collideface(const cube &c, int orient) +{ + if(flataxisface(c, orient)) + { + uchar r1 = c.edges[faceedgesidx[orient][0]], r2 = c.edges[faceedgesidx[orient][1]]; + if(uchar((r1>>4)|(r2&0xF0)) == uchar((r1&0x0F)|(r2<<4))) return false; + uchar c1 = c.edges[faceedgesidx[orient][2]], c2 = c.edges[faceedgesidx[orient][3]]; + if(uchar((c1>>4)|(c2&0xF0)) == uchar((c1&0x0F)|(c2<<4))) return false; + } + return true; +} + +bool touchingface(const cube &c, int orient) +{ + uint face = c.faces[dimension(orient)]; + return dimcoord(orient) ? (face&0xF0F0F0F0)==0x80808080 : (face&0x0F0F0F0F)==0; +} + +bool notouchingface(const cube &c, int orient) +{ + uint face = c.faces[dimension(orient)]; + return dimcoord(orient) ? (face&0x80808080)==0 : ((0x88888888-face)&0x08080808) == 0; +} + +int faceconvexity(const ivec v[4]) +{ + ivec n; + n.cross(ivec(v[1]).sub(v[0]), ivec(v[2]).sub(v[0])); + return ivec(v[0]).sub(v[3]).dot(n); + // 1 if convex, -1 if concave, 0 if flat +} + +int faceconvexity(const vertinfo *verts, int numverts, int size) +{ + if(numverts < 4) return 0; + ivec v0 = verts[0].getxyz(), + e1 = verts[1].getxyz().sub(v0), + e2 = verts[2].getxyz().sub(v0), + n; + if(size >= (8<<5)) + { + if(size >= (8<<10)) n.cross(e1.shr(10), e2.shr(10)); + else n.cross(e1, e2).shr(10); + } + else n.cross(e1, e2); + return verts[3].getxyz().sub(v0).dot(n); +} + +int faceconvexity(const ivec v[4], int &vis) +{ + ivec e1, e2, e3, n; + n.cross((e1 = v[1]).sub(v[0]), (e2 = v[2]).sub(v[0])); + int convex = (e3 = v[0]).sub(v[3]).dot(n); + if(!convex) + { + if(ivec().cross(e3, e2).iszero()) { if(!n.iszero()) vis = 1; } + else if(n.iszero()) { vis = 2; } + return 0; + } + return convex; +} + +int faceconvexity(const cube &c, int orient) +{ + if(flataxisface(c, orient)) return 0; + ivec v[4]; + genfaceverts(c, orient, v); + return faceconvexity(v); +} + +int faceorder(const cube &c, int orient) // gets above 'fv' so that each face is convex +{ + return faceconvexity(c, orient)<0 ? 1 : 0; +} + +static inline void faceedges(const cube &c, int orient, uchar edges[4]) +{ + loopk(4) edges[k] = c.edges[faceedgesidx[orient][k]]; +} + +uint faceedges(const cube &c, int orient) +{ + union { uchar edges[4]; uint face; } u; + faceedges(c, orient, u.edges); + return u.face; +} + +static inline int genfacevecs(const cube &cu, int orient, const ivec &pos, int size, bool solid, ivec2 *fvecs, const ivec *v = NULL) +{ + int i = 0; + if(solid) + { + switch(orient) + { + #define GENFACEORIENT(orient, v0, v1, v2, v3) \ + case orient: \ + { \ + if(dimcoord(orient)) { v0 v1 v2 v3 } else { v3 v2 v1 v0 } \ + break; \ + } + #define GENFACEVERT(orient, vert, xv,yv,zv, x,y,z) \ + { ivec2 &f = fvecs[i]; x ((xv)<<3); y ((yv)<<3); z ((zv)<<3); i++; } + GENFACEVERTS(pos.x, pos.x+size, pos.y, pos.y+size, pos.z, pos.z+size, f.x = , f.x = , f.y = , f.y = , (void), (void)) + #undef GENFACEVERT + } + return 4; + } + ivec buf[4]; + if(!v) { genfaceverts(cu, orient, buf); v = buf; } + ivec2 prev(INT_MAX, INT_MAX); + switch(orient) + { + #define GENFACEVERT(orient, vert, sx,sy,sz, dx,dy,dz) \ + { \ + const ivec &e = v[vert]; \ + ivec ef; \ + ef.dx = e.sx; ef.dy = e.sy; ef.dz = e.sz; \ + if(ef.z == dimcoord(orient)*8) \ + { \ + ivec2 &f = fvecs[i]; \ + ivec pf; \ + pf.dx = pos.sx; pf.dy = pos.sy; pf.dz = pos.sz; \ + f = ivec2(ef.x*size + (pf.x<<3), ef.y*size + (pf.y<<3)); \ + if(f != prev) { prev = f; i++; } \ + } \ + } + GENFACEVERTS(x, x, y, y, z, z, x, x, y, y, z, z) + #undef GENFACEORIENT + #undef GENFACEVERT + } + if(fvecs[0] == prev) i--; + return i; +} + +static inline int clipfacevecy(const ivec2 &o, const ivec2 &dir, int cx, int cy, int size, ivec2 &r) +{ + if(dir.x >= 0) + { + if(cx <= o.x || cx >= o.x+dir.x) return 0; + } + else if(cx <= o.x+dir.x || cx >= o.x) return 0; + + int t = (o.y-cy) + (cx-o.x)*dir.y/dir.x; + if(t <= 0 || t >= size) return 0; + + r.x = cx; + r.y = cy + t; + return 1; +} + +static inline int clipfacevecx(const ivec2 &o, const ivec2 &dir, int cx, int cy, int size, ivec2 &r) +{ + if(dir.y >= 0) + { + if(cy <= o.y || cy >= o.y+dir.y) return 0; + } + else if(cy <= o.y+dir.y || cy >= o.y) return 0; + + int t = (o.x-cx) + (cy-o.y)*dir.x/dir.y; + if(t <= 0 || t >= size) return 0; + + r.x = cx + t; + r.y = cy; + return 1; +} + +static inline int clipfacevec(const ivec2 &o, const ivec2 &dir, int cx, int cy, int size, ivec2 *rvecs) +{ + int r = 0; + + if(o.x >= cx && o.x <= cx+size && + o.y >= cy && o.y <= cy+size && + ((o.x != cx && o.x != cx+size) || (o.y != cy && o.y != cy+size))) + { + rvecs[0].x = o.x; + rvecs[0].y = o.y; + r++; + } + + r += clipfacevecx(o, dir, cx, cy, size, rvecs[r]); + r += clipfacevecx(o, dir, cx, cy+size, size, rvecs[r]); + r += clipfacevecy(o, dir, cx, cy, size, rvecs[r]); + r += clipfacevecy(o, dir, cx+size, cy, size, rvecs[r]); + + ASSERT(r <= 2); + return r; +} + +static inline bool insideface(const ivec2 *p, int nump, const ivec2 *o, int numo) +{ + int bounds = 0; + ivec2 prev = o[numo-1]; + loopi(numo) + { + const ivec2 &cur = o[i]; + ivec2 dir(cur.x-prev.x, cur.y-prev.y); + int offset = dir.x*prev.y - dir.y*prev.x; + loopj(nump) if(dir.x*p[j].y - dir.y*p[j].x > offset) return false; + bounds++; + prev = cur; + } + return bounds>=3; +} + +static inline int clipfacevecs(const ivec2 *o, int numo, int cx, int cy, int size, ivec2 *rvecs) +{ + cx <<= 3; + cy <<= 3; + size <<= 3; + + int r = 0; + ivec2 prev = o[numo-1]; + loopi(numo) + { + const ivec2 &cur = o[i]; + r += clipfacevec(prev, ivec2(cur.x-prev.x, cur.y-prev.y), cx, cy, size, &rvecs[r]); + prev = cur; + } + ivec2 corner[4] = {ivec2(cx, cy), ivec2(cx+size, cy), ivec2(cx+size, cy+size), ivec2(cx, cy+size)}; + loopi(4) if(insideface(&corner[i], 1, o, numo)) rvecs[r++] = corner[i]; + ASSERT(r <= 8); + return r; +} + +bool collapsedface(const cube &c, int orient) +{ + int e0 = c.edges[faceedgesidx[orient][0]], e1 = c.edges[faceedgesidx[orient][1]], + e2 = c.edges[faceedgesidx[orient][2]], e3 = c.edges[faceedgesidx[orient][3]], + face = dimension(orient)*4, + f0 = c.edges[face+0], f1 = c.edges[face+1], + f2 = c.edges[face+2], f3 = c.edges[face+3]; + if(dimcoord(orient)) { f0 >>= 4; f1 >>= 4; f2 >>= 4; f3 >>= 4; } + else { f0 &= 0xF; f1 &= 0xF; f2 &= 0xF; f3 &= 0xF; } + ivec v0(e0&0xF, e2&0xF, f0), + v1(e0>>4, e3&0xF, f1), + v2(e1>>4, e3>>4, f3), + v3(e1&0xF, e2>>4, f2); + return ivec().cross(v1.sub(v0), v2.sub(v0)).iszero() && + ivec().cross(v2, v3.sub(v0)).iszero(); +} + +static inline bool occludesface(const cube &c, int orient, const ivec &o, int size, const ivec &vo, int vsize, ushort vmat, ushort nmat, ushort matmask, const ivec2 *vf, int numv) +{ + int dim = dimension(orient); + if(!c.children) + { + if(nmat != MAT_AIR && (c.material&matmask) == nmat) + { + ivec2 nf[8]; + return clipfacevecs(vf, numv, o[C[dim]], o[R[dim]], size, nf) < 3; + } + if(isentirelysolid(c)) return true; + if(vmat != MAT_AIR && ((c.material&matmask) == vmat || (isliquid(vmat) && isclipped(c.material&MATF_VOLUME)))) return true; + if(touchingface(c, orient) && faceedges(c, orient) == F_SOLID) return true; + ivec2 cf[8]; + int numc = clipfacevecs(vf, numv, o[C[dim]], o[R[dim]], size, cf); + if(numc < 3) return true; + if(isempty(c) || notouchingface(c, orient)) return false; + ivec2 of[4]; + int numo = genfacevecs(c, orient, o, size, false, of); + return numo >= 3 && insideface(cf, numc, of, numo); + } + + size >>= 1; + int coord = dimcoord(orient); + loopi(8) if(octacoord(dim, i) == coord) + { + if(!occludesface(c.children[i], orient, ivec(i, o, size), size, vo, vsize, vmat, nmat, matmask, vf, numv)) return false; + } + + return true; +} + +bool visibleface(const cube &c, int orient, const ivec &co, int size, ushort mat, ushort nmat, ushort matmask) +{ + if(mat != MAT_AIR) + { + if(faceedges(c, orient)==F_SOLID && touchingface(c, orient)) return false; + } + else + { + if(collapsedface(c, orient)) return false; + if(!touchingface(c, orient)) return true; + } + + ivec no; + int nsize; + const cube &o = neighbourcube(c, orient, co, size, no, nsize); + if(&o==&c) return false; + + int opp = opposite(orient); + if(nsize > size || (nsize == size && !o.children)) + { + if(nmat != MAT_AIR && (o.material&matmask) == nmat) return true; + if(isentirelysolid(o)) return false; + if(mat != MAT_AIR && ((o.material&matmask) == mat || (isliquid(mat) && (o.material&MATF_VOLUME) == MAT_GLASS))) return false; + if(isempty(o) || notouchingface(o, opp)) return true; + if(touchingface(o, opp) && faceedges(o, opp) == F_SOLID) return false; + + ivec vo = ivec(co).mask(0xFFF); + no.mask(0xFFF); + ivec2 cf[4], of[4]; + int numc = genfacevecs(c, orient, vo, size, mat != MAT_AIR, cf), + numo = genfacevecs(o, opp, no, nsize, false, of); + return numo < 3 || !insideface(cf, numc, of, numo); + } + + + ivec vo = ivec(co).mask(0xFFF); + no.mask(0xFFF); + ivec2 cf[4]; + int numc = genfacevecs(c, orient, vo, size, mat != MAT_AIR, cf); + return !occludesface(o, opp, no, nsize, vo, size, mat, nmat, matmask, cf, numc); +} + +int classifyface(const cube &c, int orient, const ivec &co, int size) +{ + if(collapsedface(c, orient)) return 0; + int vismask = (c.material&MATF_CLIP) == MAT_NOCLIP ? 1 : 3; + if(!touchingface(c, orient)) return vismask; + + ivec no; + int nsize; + const cube &o = neighbourcube(c, orient, co, size, no, nsize); + if(&o==&c) return 0; + + int vis = 0, opp = opposite(orient); + if(nsize > size || (nsize == size && !o.children)) + { + if((~c.material & o.material) & MAT_ALPHA) vis |= 1; + if((o.material&MATF_CLIP) == MAT_NOCLIP) vis |= vismask&2; + if(vis == vismask || isentirelysolid(o)) return vis; + if(isempty(o) || notouchingface(o, opp)) return vismask; + if(touchingface(o, opp) && faceedges(o, opp) == F_SOLID) return vis; + + ivec vo = ivec(co).mask(0xFFF); + no.mask(0xFFF); + ivec2 cf[4], of[4]; + int numc = genfacevecs(c, orient, vo, size, false, cf), + numo = genfacevecs(o, opp, no, nsize, false, of); + if(numo < 3 || !insideface(cf, numc, of, numo)) return vismask; + return vis; + } + + ivec vo = ivec(co).mask(0xFFF); + no.mask(0xFFF); + ivec2 cf[4]; + int numc = genfacevecs(c, orient, vo, size, false, cf); + if(!occludesface(o, opp, no, nsize, vo, size, MAT_AIR, (c.material&MAT_ALPHA)^MAT_ALPHA, MAT_ALPHA, cf, numc)) vis |= 1; + if(vismask&2 && !occludesface(o, opp, no, nsize, vo, size, MAT_AIR, MAT_NOCLIP, MATF_CLIP, cf, numc)) vis |= 2; + return vis; +} + +// more expensive version that checks both triangles of a face independently +int visibletris(const cube &c, int orient, const ivec &co, int size, ushort nmat, ushort matmask) +{ + int vis = 3, touching = 0xF; + ivec v[4], e1, e2, e3, n; + genfaceverts(c, orient, v); + n.cross((e1 = v[1]).sub(v[0]), (e2 = v[2]).sub(v[0])); + int convex = (e3 = v[0]).sub(v[3]).dot(n); + if(!convex) + { + if(ivec().cross(e3, e2).iszero() || v[1] == v[3]) { if(n.iszero()) return 0; vis = 1; touching = 0xF&~(1<<3); } + else if(n.iszero()) { vis = 2; touching = 0xF&~(1<<1); } + } + + int dim = dimension(orient), coord = dimcoord(orient); + if(v[0][dim] != coord*8) touching &= ~(1<<0); + if(v[1][dim] != coord*8) touching &= ~(1<<1); + if(v[2][dim] != coord*8) touching &= ~(1<<2); + if(v[3][dim] != coord*8) touching &= ~(1<<3); + static const int notouchmasks[2][16] = // mask of triangles not touching + { // order 0: flat or convex + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + { 3, 3, 3, 3, 3, 3, 3, 2, 3, 3, 3, 3, 3, 1, 3, 0 }, + // order 1: concave + { 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 3, 2, 0 }, + }; + int order = convex < 0 ? 1 : 0, notouch = notouchmasks[order][touching]; + if((vis¬ouch)==vis) return vis; + + ivec no; + int nsize; + const cube &o = neighbourcube(c, orient, co, size, no, nsize); + if(&o==&c) return 0; + + if((c.material&matmask) == nmat) nmat = MAT_AIR; + + ivec vo = ivec(co).mask(0xFFF); + no.mask(0xFFF); + ivec2 cf[4], of[4]; + int opp = opposite(orient), numo = 0, numc; + if(nsize > size || (nsize == size && !o.children)) + { + if(isempty(o) || notouchingface(o, opp)) return vis; + if(nmat != MAT_AIR && (o.material&matmask) == nmat) return vis; + if(isentirelysolid(o) || (touchingface(o, opp) && faceedges(o, opp) == F_SOLID)) return vis¬ouch; + + numc = genfacevecs(c, orient, vo, size, false, cf, v); + numo = genfacevecs(o, opp, no, nsize, false, of); + if(numo < 3) return vis; + if(insideface(cf, numc, of, numo)) return vis¬ouch; + } + else + { + numc = genfacevecs(c, orient, vo, size, false, cf, v); + if(occludesface(o, opp, no, nsize, vo, size, MAT_AIR, nmat, matmask, cf, numc)) return vis¬ouch; + } + if(vis != 3 || notouch) return vis; + + static const int triverts[2][2][2][3] = + { // order + { // coord + { { 1, 2, 3 }, { 0, 1, 3 } }, // verts + { { 0, 1, 2 }, { 0, 2, 3 } } + }, + { // coord + { { 0, 1, 2 }, { 3, 0, 2 } }, // verts + { { 1, 2, 3 }, { 1, 3, 0 } } + } + }; + + do + { + loopi(2) + { + const int *verts = triverts[order][coord][i]; + ivec2 tf[3] = { cf[verts[0]], cf[verts[1]], cf[verts[2]] }; + if(numo > 0) { if(!insideface(tf, 3, of, numo)) continue; } + else if(!occludesface(o, opp, no, nsize, vo, size, MAT_AIR, nmat, matmask, tf, 3)) continue; + return vis & ~(1<=8) v.mul(size/8); + else v.div(8/size); + v.add(ivec(co).shl(3)); +} + +void calcvert(const cube &c, const ivec &co, int size, vec &v, int i, bool solid) +{ + if(solid) v = vec(cubecoords[i]); else gencubevert(c, i, v); + v.mul(size/8.0f).add(vec(co)); +} + +int genclipplane(const cube &c, int orient, vec *v, plane *clip) +{ + int planes = 0, convex = faceconvexity(c, orient), order = convex < 0 ? 1 : 0; + const vec &v0 = v[fv[orient][order]], &v1 = v[fv[orient][order+1]], &v2 = v[fv[orient][order+2]], &v3 = v[fv[orient][(order+3)&3]]; + if(v0==v2) return 0; + if(v0!=v1 && v1!=v2) clip[planes++].toplane(v0, v1, v2); + if(v0!=v3 && v2!=v3 && (!planes || convex)) clip[planes++].toplane(v0, v2, v3); + return planes; +} + +void genclipplanes(const cube &c, const ivec &co, int size, clipplanes &p, bool collide) +{ + // generate tight bounding box + calcvert(c, co, size, p.v[0], 0); + vec mx = p.v[0], mn = p.v[0]; + for(int i = 1; i < 8; i++) + { + calcvert(c, co, size, p.v[i], i); + mx.max(p.v[i]); + mn.min(p.v[i]); + } + + p.r = mx.sub(mn).mul(0.5f); + p.o = mn.add(p.r); + + p.size = 0; + p.visible = 0; + if(collide || (c.visible&0xC0) == 0x40) + { + loopi(6) if(c.visible&(1< y.v2) return false; + if(x.u1 < y.u1) return true; + if(x.u1 > y.u1) return false; + return false; +} + +static int mergefacev(int orient, facebounds *m, int sz, facebounds &n) +{ + for(int i = sz-1; i >= 0; --i) + { + if(m[i].v2 < n.v1) break; + if(m[i].v2 == n.v1 && m[i].u1 == n.u1 && m[i].u2 == n.u2) + { + n.v1 = m[i].v1; + memmove(&m[i], &m[i+1], (sz - (i+1)) * sizeof(facebounds)); + return 1; + } + } + return 0; +} + +static int mergefaceu(int orient, facebounds &m, facebounds &n) +{ + if(m.v1 == n.v1 && m.v2 == n.v2 && m.u2 == n.u1) + { + n.u1 = m.u1; + return 1; + } + return 0; +} + +static int mergeface(int orient, facebounds *m, int sz, facebounds &n) +{ + for(bool merged = false; sz; merged = true) + { + int vmerged = mergefacev(orient, m, sz, n); + sz -= vmerged; + if(!vmerged && merged) break; + if(!sz) break; + int umerged = mergefaceu(orient, m[sz-1], n); + sz -= umerged; + if(!umerged) break; + } + m[sz++] = n; + return sz; +} + +int mergefaces(int orient, facebounds *m, int sz) +{ + quicksort(m, sz, mergefacecmp); + + int nsz = 0; + loopi(sz) nsz = mergeface(orient, m, nsz, m[i]); + return nsz; +} + +struct cfkey +{ + uchar orient; + ushort material, tex; + ivec n; + int offset; +}; + +static inline bool htcmp(const cfkey &x, const cfkey &y) +{ + return x.orient == y.orient && x.tex == y.tex && x.n == y.n && x.offset == y.offset && x.material==y.material; +} + +static inline uint hthash(const cfkey &k) +{ + return hthash(k.n)^k.offset^k.tex^k.orient^k.material; +} + +void mincubeface(const cube &cu, int orient, const ivec &o, int size, const facebounds &orig, facebounds &cf, ushort nmat, ushort matmask) +{ + int dim = dimension(orient); + if(cu.children) + { + size >>= 1; + int coord = dimcoord(orient); + loopi(8) if(octacoord(dim, i) == coord) + mincubeface(cu.children[i], orient, ivec(i, o, size), size, orig, cf, nmat, matmask); + return; + } + int c = C[dim], r = R[dim]; + ushort uco = (o[c]&0xFFF)<<3, vco = (o[r]&0xFFF)<<3; + ushort uc1 = uco, vc1 = vco, uc2 = ushort(size<<3)+uco, vc2 = ushort(size<<3)+vco; + uc1 = max(uc1, orig.u1); + uc2 = min(uc2, orig.u2); + vc1 = max(vc1, orig.v1); + vc2 = min(vc2, orig.v2); + if(!isempty(cu) && touchingface(cu, orient) && !(nmat!=MAT_AIR && (cu.material&matmask)==nmat)) + { + uchar r1 = cu.edges[faceedgesidx[orient][0]], r2 = cu.edges[faceedgesidx[orient][1]], + c1 = cu.edges[faceedgesidx[orient][2]], c2 = cu.edges[faceedgesidx[orient][3]]; + ushort u1 = max(c1&0xF, c2&0xF)*size+uco, u2 = min(c1>>4, c2>>4)*size+uco, + v1 = max(r1&0xF, r2&0xF)*size+vco, v2 = min(r1>>4, r2>>4)*size+vco; + u1 = max(u1, orig.u1); + u2 = min(u2, orig.u2); + v1 = max(v1, orig.v1); + v2 = min(v2, orig.v2); + if(v2-v1==vc2-vc1) + { + if(u2-u1==uc2-uc1) return; + if(u1==uc1) uc1 = u2; + if(u2==uc2) uc2 = u1; + } + else if(u2-u1==uc2-uc1) + { + if(v1==vc1) vc1 = v2; + if(v2==vc2) vc2 = v1; + } + } + if(uc1==uc2 || vc1==vc2) return; + cf.u1 = min(cf.u1, uc1); + cf.u2 = max(cf.u2, uc2); + cf.v1 = min(cf.v1, vc1); + cf.v2 = max(cf.v2, vc2); +} + +bool mincubeface(const cube &cu, int orient, const ivec &co, int size, facebounds &orig) +{ + ivec no; + int nsize; + const cube &nc = neighbourcube(cu, orient, co, size, no, nsize); + facebounds mincf; + mincf.u1 = orig.u2; + mincf.u2 = orig.u1; + mincf.v1 = orig.v2; + mincf.v2 = orig.v1; + mincubeface(nc, opposite(orient), no, nsize, orig, mincf, cu.material&MAT_ALPHA ? MAT_AIR : MAT_ALPHA, MAT_ALPHA); + bool smaller = false; + if(mincf.u1 > orig.u1) { orig.u1 = mincf.u1; smaller = true; } + if(mincf.u2 < orig.u2) { orig.u2 = mincf.u2; smaller = true; } + if(mincf.v1 > orig.v1) { orig.v1 = mincf.v1; smaller = true; } + if(mincf.v2 < orig.v2) { orig.v2 = mincf.v2; smaller = true; } + return smaller; +} + +VAR(maxmerge, 0, 6, 12); +VAR(minface, 0, 4, 12); + +struct pvert +{ + ushort x, y; + + pvert() {} + pvert(ushort x, ushort y) : x(x), y(y) {} + + bool operator==(const pvert &o) const { return x == o.x && y == o.y; } + bool operator!=(const pvert &o) const { return x != o.x || y != o.y; } +}; + +struct pedge +{ + pvert from, to; + + pedge() {} + pedge(const pvert &from, const pvert &to) : from(from), to(to) {} + + bool operator==(const pedge &o) const { return from == o.from && to == o.to; } + bool operator!=(const pedge &o) const { return from != o.from || to != o.to; } +}; + +static inline uint hthash(const pedge &x) { return uint(x.from.x)^(uint(x.from.y)<<8); } +static inline bool htcmp(const pedge &x, const pedge &y) { return x == y; } + +struct poly +{ + cube *c; + int numverts; + bool merged; + pvert verts[MAXFACEVERTS]; +}; + +bool clippoly(poly &p, const facebounds &b) +{ + pvert verts1[MAXFACEVERTS+4], verts2[MAXFACEVERTS+4]; + int numverts1 = 0, numverts2 = 0, px = p.verts[p.numverts-1].x, py = p.verts[p.numverts-1].y; + loopi(p.numverts) + { + int x = p.verts[i].x, y = p.verts[i].y; + if(x < b.u1) + { + if(px > b.u2) verts1[numverts1++] = pvert(b.u2, y + ((y - py)*(b.u2 - x))/(x - px)); + if(px > b.u1) verts1[numverts1++] = pvert(b.u1, y + ((y - py)*(b.u1 - x))/(x - px)); + } + else if(x > b.u2) + { + if(px < b.u1) verts1[numverts1++] = pvert(b.u1, y + ((y - py)*(b.u1 - x))/(x - px)); + if(px < b.u2) verts1[numverts1++] = pvert(b.u2, y + ((y - py)*(b.u2 - x))/(x - px)); + } + else + { + if(px < b.u1) + { + if(x > b.u1) verts1[numverts1++] = pvert(b.u1, y + ((y - py)*(b.u1 - x))/(x - px)); + } + else if(px > b.u2 && x < b.u2) verts1[numverts1++] = pvert(b.u2, y + ((y - py)*(b.u2 - x))/(x - px)); + verts1[numverts1++] = pvert(x, y); + } + px = x; + py = y; + } + if(numverts1 < 3) return false; + px = verts1[numverts1-1].x; + py = verts1[numverts1-1].y; + loopi(numverts1) + { + int x = verts1[i].x, y = verts1[i].y; + if(y < b.v1) + { + if(py > b.v2) verts2[numverts2++] = pvert(x + ((x - px)*(b.v2 - y))/(y - py), b.v2); + if(py > b.v1) verts2[numverts2++] = pvert(x + ((x - px)*(b.v1 - y))/(y - py), b.v1); + } + else if(y > b.v2) + { + if(py < b.v1) verts2[numverts2++] = pvert(x + ((x - px)*(b.v1 - y))/(y - py), b.v1); + if(py < b.v2) verts2[numverts2++] = pvert(x + ((x - px)*(b.v2 - y))/(y - py), b.v2); + } + else + { + if(py < b.v1) + { + if(y > b.v1) verts2[numverts2++] = pvert(x + ((x - px)*(b.v1 - y))/(y - py), b.v1); + } + else if(py > b.v2 && y < b.v2) verts2[numverts2++] = pvert(x + ((x - px)*(b.v2 - y))/(y - py), b.v2); + verts2[numverts2++] = pvert(x, y); + } + px = x; + py = y; + } + if(numverts2 < 3) return false; + if(numverts2 > MAXFACEVERTS) return false; + memcpy(p.verts, verts2, numverts2*sizeof(pvert)); + p.numverts = numverts2; + return true; +} + +bool genpoly(cube &cu, int orient, const ivec &o, int size, int vis, ivec &n, int &offset, poly &p) +{ + int dim = dimension(orient), coord = dimcoord(orient); + ivec v[4]; + genfaceverts(cu, orient, v); + if(flataxisface(cu, orient)) + { + n = ivec(0, 0, 0); + n[dim] = coord ? 1 : -1; + } + else + { + if(faceconvexity(v)) return false; + n.cross(ivec(v[1]).sub(v[0]), ivec(v[2]).sub(v[0])); + if(n.iszero()) n.cross(ivec(v[2]).sub(v[0]), ivec(v[3]).sub(v[0])); + reduceslope(n); + } + + ivec po = ivec(o).mask(0xFFF).shl(3); + loopk(4) v[k].mul(size).add(po); + offset = -n.dot(v[3]); + + int r = R[dim], c = C[dim], order = vis&4 ? 1 : 0; + p.numverts = 0; + if(coord) + { + const ivec &v0 = v[order]; p.verts[p.numverts++] = pvert(v0[c], v0[r]); + if(vis&1) { const ivec &v1 = v[order+1]; p.verts[p.numverts++] = pvert(v1[c], v1[r]); } + const ivec &v2 = v[order+2]; p.verts[p.numverts++] = pvert(v2[c], v2[r]); + if(vis&2) { const ivec &v3 = v[(order+3)&3]; p.verts[p.numverts++] = pvert(v3[c], v3[r]); } + } + else + { + if(vis&2) { const ivec &v3 = v[(order+3)&3]; p.verts[p.numverts++] = pvert(v3[c], v3[r]); } + const ivec &v2 = v[order+2]; p.verts[p.numverts++] = pvert(v2[c], v2[r]); + if(vis&1) { const ivec &v1 = v[order+1]; p.verts[p.numverts++] = pvert(v1[c], v1[r]); } + const ivec &v0 = v[order]; p.verts[p.numverts++] = pvert(v0[c], v0[r]); + } + + if(faceedges(cu, orient)!=F_SOLID) + { + int px = int(p.verts[p.numverts-2].x) - int(p.verts[p.numverts-3].x), py = int(p.verts[p.numverts-2].y) - int(p.verts[p.numverts-3].y), + cx = int(p.verts[p.numverts-1].x) - int(p.verts[p.numverts-2].x), cy = int(p.verts[p.numverts-1].y) - int(p.verts[p.numverts-2].y), + dir = px*cy - py*cx; + if(dir > 0) return false; + if(!dir) { if(p.numverts < 4) return false; p.verts[p.numverts-2] = p.verts[p.numverts-1]; p.numverts--; } + px = cx; py = cy; + cx = int(p.verts[0].x) - int(p.verts[p.numverts-1].x); cy = int(p.verts[0].y) - int(p.verts[p.numverts-1].y); + dir = px*cy - py*cx; + if(dir > 0) return false; + if(!dir) { if(p.numverts < 4) return false; p.numverts--; } + px = cx; py = cy; + cx = int(p.verts[1].x) - int(p.verts[0].x); cy = int(p.verts[1].y) - int(p.verts[0].y); + dir = px*cy - py*cx; + if(dir > 0) return false; + if(!dir) { if(p.numverts < 4) return false; p.verts[0] = p.verts[p.numverts-1]; p.numverts--; } + px = cx; py = cy; + cx = int(p.verts[2].x) - int(p.verts[1].x); cy = int(p.verts[2].y) - int(p.verts[1].y); + dir = px*cy - py*cx; + if(dir > 0) return false; + if(!dir) { if(p.numverts < 4) return false; p.verts[1] = p.verts[2]; p.verts[2] = p.verts[3]; p.numverts--; } + } + + p.c = &cu; + p.merged = false; + + if(minface && size >= 1< &links, vector &queue, int owner, poly &p, poly &q, const pedge &e) +{ + int pe = -1, qe = -1; + loopi(p.numverts) if(p.verts[i] == e.from) { pe = i; break; } + loopi(q.numverts) if(q.verts[i] == e.to) { qe = i; break; } + if(pe < 0 || qe < 0) return false; + if(p.verts[(pe+1)%p.numverts] != e.to || q.verts[(qe+1)%q.numverts] != e.from) return false; + /* + * c----d + * | | + * F----T + * | P | + * b----a + */ + pvert verts[2*MAXFACEVERTS]; + int numverts = 0, index = pe+2; // starts at A = T+1, ends at F = T+p.numverts + loopi(p.numverts-1) + { + if(index >= p.numverts) index -= p.numverts; + verts[numverts++] = p.verts[index++]; + } + index = qe+2; // starts at C = T+2 = F+1, ends at T = T+q.numverts + int px = int(verts[numverts-1].x) - int(verts[numverts-2].x), py = int(verts[numverts-1].y) - int(verts[numverts-2].y); + loopi(q.numverts-1) + { + if(index >= q.numverts) index -= q.numverts; + pvert &src = q.verts[index++]; + int cx = int(src.x) - int(verts[numverts-1].x), cy = int(src.y) - int(verts[numverts-1].y), + dir = px*cy - py*cx; + if(dir > 0) return false; + if(!dir) numverts--; + verts[numverts++] = src; + px = cx; + py = cy; + } + int cx = int(verts[0].x) - int(verts[numverts-1].x), cy = int(verts[0].y) - int(verts[numverts-1].y), + dir = px*cy - py*cx; + if(dir > 0) return false; + if(!dir) numverts--; + + if(numverts > MAXFACEVERTS) return false; + + q.merged = true; + q.numverts = 0; + + p.merged = true; + p.numverts = numverts; + memcpy(p.verts, verts, numverts*sizeof(pvert)); + + int prev = p.numverts-1; + loopj(p.numverts) + { + pedge e(p.verts[prev], p.verts[j]); + int order = e.from.x > e.to.x || (e.from.x == e.to.x && e.from.y > e.to.y) ? 1 : 0; + if(order) swap(e.from, e.to); + plink &l = links.access(e, e); + bool shouldqueue = l.polys[order] < 0 && l.polys[order^1] >= 0; + l.polys[order] = owner; + if(shouldqueue) queue.add(&l); + prev = j; + } + + return true; +} + +void addmerge(cube &cu, int orient, const ivec &co, const ivec &n, int offset, poly &p) +{ + cu.merged |= 1<surfaces[orient] = ambientsurface; + return; + } + surfaceinfo surf = brightsurface; + vertinfo verts[MAXFACEVERTS]; + surf.numverts |= p.numverts; + int dim = dimension(orient), coord = dimcoord(orient), c = C[dim], r = R[dim]; + loopk(p.numverts) + { + pvert &src = p.verts[coord ? k : p.numverts-1-k]; + vertinfo &dst = verts[k]; + ivec v; + v[c] = src.x; + v[r] = src.y; + v[dim] = -(offset + n[c]*src.x + n[r]*src.y)/n[dim]; + dst.set(v); + } + if(cu.ext) + { + const surfaceinfo &oldsurf = cu.ext->surfaces[orient]; + int numverts = oldsurf.numverts&MAXFACEVERTS; + if(numverts == p.numverts) + { + ivec v0 = verts[0].getxyz(); + const vertinfo *oldverts = cu.ext->verts() + oldsurf.verts; + loopj(numverts) if(v0 == oldverts[j].getxyz()) + { + for(int k = 1; k < numverts; k++) + { + if(++j >= numverts) j = 0; + if(verts[k].getxyz() != oldverts[j].getxyz()) goto nomatch; + } + return; + } + nomatch:; + } + } + setsurface(cu, orient, surf, verts, p.numverts); +} + +static inline void clearmerge(cube &c, int orient) +{ + if(c.merged&(1<surfaces[orient] = brightsurface; + } +} + +void addmerges(int orient, const ivec &co, const ivec &n, int offset, vector &polys) +{ + loopv(polys) + { + poly &p = polys[i]; + if(p.merged) addmerge(*p.c, orient, co, n, offset, p); + else clearmerge(*p.c, orient); + } +} + +void mergepolys(int orient, const ivec &co, const ivec &n, int offset, vector &polys) +{ + if(polys.length() <= 1) { addmerges(orient, co, n, offset, polys); return; } + hashset links(polys.length() <= 32 ? 128 : 1024); + vector queue; + loopv(polys) + { + poly &p = polys[i]; + int prev = p.numverts-1; + loopj(p.numverts) + { + pedge e(p.verts[prev], p.verts[j]); + int order = e.from.x > e.to.x || (e.from.x == e.to.x && e.from.y > e.to.y) ? 1 : 0; + if(order) swap(e.from, e.to); + plink &l = links.access(e, e); + l.polys[order] = i; + if(l.polys[0] >= 0 && l.polys[1] >= 0) queue.add(&l); + prev = j; + } + } + vector nextqueue; + while(queue.length()) + { + loopv(queue) + { + plink &l = *queue[i]; + if(l.polys[0] >= 0 && l.polys[1] >= 0) + mergepolys(orient, links, nextqueue, l.polys[0], polys[l.polys[0]], polys[l.polys[1]], l); + } + queue.setsize(0); + queue.move(nextqueue); + } + addmerges(orient, co, n, offset, polys); +} + +static int genmergeprogress = 0; + +struct cfpolys +{ + vector polys; +}; + +static hashtable cpolys; + +void genmerges(cube *c = worldroot, const ivec &o = ivec(0, 0, 0), int size = worldsize>>1) +{ + if((genmergeprogress++&0xFFF)==0) renderprogress(float(genmergeprogress)/allocnodes, "merging faces..."); + neighbourstack[++neighbourdepth] = c; + loopi(8) + { + ivec co(i, o, size); + int vis; + if(c[i].children) genmerges(c[i].children, co, size>>1); + else if(!isempty(c[i])) loopj(6) if((vis = visibletris(c[i], j, co, size))) + { + cfkey k; + poly p; + if(size < 1<= 1<= x2 && + mo.y <= y1 && mo.y + (1<= y2 && + mo.z <= z1 && mo.z + (1<= z2) + break; + bits++; + } + return bits-3; +} + +static void invalidatemerges(cube &c) +{ + if(c.merged) + { + brightencube(c); + c.merged = 0; + } + if(c.ext) + { + if(c.ext->va) + { + if(!(c.ext->va->hasmerges&(MERGE_PART | MERGE_ORIGIN))) return; + destroyva(c.ext->va); + c.ext->va = NULL; + } + if(c.ext->tjoints >= 0) c.ext->tjoints = -1; + } + if(c.children) loopi(8) invalidatemerges(c.children[i]); +} + +static int invalidatedmerges = 0; + +void invalidatemerges(cube &c, const ivec &co, int size, bool msg) +{ + if(msg && invalidatedmerges!=totalmillis) + { + renderprogress(0, "invalidating merged surfaces..."); + invalidatedmerges = totalmillis; + } + invalidatemerges(c); +} + +void calcmerges() +{ + genmergeprogress = 0; + genmerges(); +} + diff --git a/src/engine/octa.h b/src/engine/octa.h new file mode 100644 index 0000000..6b7bfca --- /dev/null +++ b/src/engine/octa.h @@ -0,0 +1,340 @@ +// 6-directional octree heightfield map format + +struct elementset +{ + ushort texture, lmid, envmap; + uchar dim, layer; + ushort length[2], minvert[2], maxvert[2]; +}; + +enum +{ + EMID_NONE = 0, + EMID_CUSTOM, + EMID_SKY, + EMID_RESERVED +}; + +struct materialsurface +{ + ivec o; + ushort csize, rsize; + ushort material, skip; + uchar orient, visible; + union + { + short index; + short depth; + }; + union + { + entity *light; + ushort envmap; + uchar ends; + }; +}; + +struct vertinfo +{ + ushort x, y, z, u, v, norm; + + void setxyz(ushort a, ushort b, ushort c) { x = a; y = b; z = c; } + void setxyz(const ivec &v) { setxyz(v.x, v.y, v.z); } + void set(ushort a, ushort b, ushort c, ushort s = 0, ushort t = 0, ushort n = 0) { setxyz(a, b, c); u = s; v = t; norm = n; } + void set(const ivec &v, ushort s = 0, ushort t = 0, ushort n = 0) { set(v.x, v.y, v.z, s, t, n); } + ivec getxyz() const { return ivec(x, y, z); } +}; + +enum +{ + LAYER_TOP = (1<<5), + LAYER_BOTTOM = (1<<6), + LAYER_DUP = (1<<7), + + LAYER_BLEND = LAYER_TOP|LAYER_BOTTOM, + + MAXFACEVERTS = 15 +}; + +enum { LMID_AMBIENT = 0, LMID_AMBIENT1, LMID_BRIGHT, LMID_BRIGHT1, LMID_DARK, LMID_DARK1, LMID_RESERVED }; + +struct surfaceinfo +{ + uchar lmid[2]; + uchar verts, numverts; + + int totalverts() const { return numverts&LAYER_DUP ? (numverts&MAXFACEVERTS)*2 : numverts&MAXFACEVERTS; } + bool used() const { return lmid[0] != LMID_AMBIENT || lmid[1] != LMID_AMBIENT || numverts&~LAYER_TOP; } + void clear() { lmid[0] = LMID_AMBIENT; lmid[1] = LMID_AMBIENT; numverts = (numverts&MAXFACEVERTS) | LAYER_TOP; } + void brighten() { lmid[0] = LMID_BRIGHT; lmid[1] = LMID_AMBIENT; numverts = (numverts&MAXFACEVERTS) | LAYER_TOP; } +}; + +static const surfaceinfo ambientsurface = {{LMID_AMBIENT, LMID_AMBIENT}, 0, LAYER_TOP}; +static const surfaceinfo brightsurface = {{LMID_BRIGHT, LMID_AMBIENT}, 0, LAYER_TOP}; +static const surfaceinfo brightbottomsurface = {{LMID_AMBIENT, LMID_BRIGHT}, 0, LAYER_BOTTOM}; + +struct grasstri +{ + vec v[4]; + int numv; + vec4 tcu, tcv; + plane surface; + vec center; + float radius; + float minz, maxz; + ushort texture, lmid; +}; + +struct occludequery +{ + void *owner; + GLuint id; + int fragments; +}; + +struct vtxarray; + +struct octaentities +{ + vector mapmodels; + vector other; + occludequery *query; + octaentities *next, *rnext; + int distance; + ivec o; + int size; + ivec bbmin, bbmax; + + octaentities(const ivec &o, int size) : query(0), o(o), size(size), bbmin(o), bbmax(o) + { + bbmin.add(size); + } +}; + +enum +{ + OCCLUDE_NOTHING = 0, + OCCLUDE_GEOM, + OCCLUDE_BB, + OCCLUDE_PARENT +}; + +enum +{ + MERGE_ORIGIN = 1<<0, + MERGE_PART = 1<<1, + MERGE_USE = 1<<2 +}; + +struct vtxarray +{ + vtxarray *parent; + vector children; + vtxarray *next, *rnext; // linked list of visible VOBs + vertex *vdata; // vertex data + ushort voffset; // offset into vertex data + ushort *edata, *skydata; // vertex indices + GLuint vbuf, ebuf, skybuf; // VBOs + ushort minvert, maxvert; // DRE info + elementset *eslist; // List of element indices sets (range) per texture + materialsurface *matbuf; // buffer of material surfaces + int verts, tris, texs, blendtris, blends, alphabacktris, alphaback, alphafronttris, alphafront, alphatris, texmask, sky, explicitsky, skyfaces, skyclip, matsurfs, distance; + double skyarea; + ivec o; + int size; // location and size of cube. + ivec geommin, geommax; // BB of geom + ivec shadowmapmin, shadowmapmax; // BB of shadowmapped surfaces + ivec matmin, matmax; // BB of any materials + ivec bbmin, bbmax; // BB of everything including children + uchar curvfc, occluded; + occludequery *query; + vector mapmodels; + vector grasstris; + int hasmerges, mergelevel; + uint dynlightmask; + bool shadowed; +}; + +struct cube; + +struct clipplanes +{ + vec o, r, v[8]; + plane p[12]; + uchar side[12]; + uchar size, visible; + const cube *owner; + int version; +}; + +struct facebounds +{ + ushort u1, u2, v1, v2; + + bool empty() const { return u1 >= u2 || v1 >= v2; } +}; + +struct tjoint +{ + int next; + ushort offset; + uchar edge; +}; + +struct cubeext +{ + vtxarray *va; // vertex array for children, or NULL + octaentities *ents; // map entities inside cube + surfaceinfo surfaces[6]; // render info for each surface + int tjoints; // linked list of t-joints + uchar maxverts; // allocated space for verts + + vertinfo *verts() { return (vertinfo *)(this+1); } +}; + +struct cube +{ + cube *children; // points to 8 cube structures which are its children, or NULL. -Z first, then -Y, -X + cubeext *ext; // extended info for the cube + union + { + uchar edges[12]; // edges of the cube, each uchar is 2 4bit values denoting the range. + // see documentation jpgs for more info. + uint faces[3]; // 4 edges of each dimension together representing 2 perpendicular faces + }; + ushort texture[6]; // one for each face. same order as orient. + ushort material; // empty-space material + uchar merged; // merged faces of the cube + union + { + uchar escaped; // mask of which children have escaped merges + uchar visible; // visibility info for faces + }; +}; + +struct block3 +{ + ivec o, s; + int grid, orient; + block3() {} + block3(const selinfo &sel) : o(sel.o), s(sel.s), grid(sel.grid), orient(sel.orient) {} + cube *c() { return (cube *)(this+1); } + int size() const { return s.x*s.y*s.z; } +}; + +struct editinfo +{ + block3 *copy; + editinfo() : copy(NULL) {} +}; + +struct undoent { int i; entity e; }; +struct undoblock // undo header, all data sits in payload +{ + undoblock *prev, *next; + int size, timestamp, numents; // if numents is 0, is a cube undo record, otherwise an entity undo record + + block3 *block() { return (block3 *)(this + 1); } + uchar *gridmap() + { + block3 *ub = block(); + return (uchar *)(ub->c() + ub->size()); + } + undoent *ents() { return (undoent *)(this + 1); } +}; + +extern cube *worldroot; // the world data. only a ptr to 8 cubes (ie: like cube.children above) +extern int wtris, wverts, vtris, vverts, glde, gbatches, rplanes; +extern int allocnodes, allocva, selchildcount, selchildmat; + +const uint F_EMPTY = 0; // all edges in the range (0,0) +const uint F_SOLID = 0x80808080; // all edges in the range (0,8) + +#define isempty(c) ((c).faces[0]==F_EMPTY) +#define isentirelysolid(c) ((c).faces[0]==F_SOLID && (c).faces[1]==F_SOLID && (c).faces[2]==F_SOLID) +#define setfaces(c, face) { (c).faces[0] = (c).faces[1] = (c).faces[2] = face; } +#define solidfaces(c) setfaces(c, F_SOLID) +#define emptyfaces(c) setfaces(c, F_EMPTY) + +#define edgemake(a, b) ((b)<<4|a) +#define edgeget(edge, coord) ((coord) ? (edge)>>4 : (edge)&0xF) +#define edgeset(edge, coord, val) ((edge) = ((coord) ? ((edge)&0xF)|((val)<<4) : ((edge)&0xF0)|(val))) + +#define cubeedge(c, d, x, y) ((c).edges[(((d)<<2)+((y)<<1)+(x))]) + +#define octadim(d) (1<<(d)) // creates mask for bit of given dimension +#define octacoord(d, i) (((i)&octadim(d))>>(d)) +#define oppositeocta(d, i) ((i)^octadim(D[d])) +#define octaindex(d,x,y,z) (((z)<>(scale))&1)<<2) | ((((y)>>(scale))&1)<<1) | (((x)>>(scale))&1)) + +static inline uchar octaboxoverlap(const ivec &o, int size, const ivec &bbmin, const ivec &bbmax) +{ + uchar p = 0xFF; // bitmask of possible collisions with octants. 0 bit = 0 octant, etc + ivec mid = ivec(o).add(size); + if(mid.z <= bbmin.z) p &= 0xF0; // not in a -ve Z octant + else if(mid.z >= bbmax.z) p &= 0x0F; // not in a +ve Z octant + if(mid.y <= bbmin.y) p &= 0xCC; // not in a -ve Y octant + else if(mid.y >= bbmax.y) p &= 0x33; // etc.. + if(mid.x <= bbmin.x) p &= 0xAA; + else if(mid.x >= bbmax.x) p &= 0x55; + return p; +} + +#define loopoctabox(o, size, bbmin, bbmax) uchar possible = octaboxoverlap(o, size, bbmin, bbmax); loopi(8) if(possible&(1<>1) +#define dimcoord(orient) ((orient)&1) +#define opposite(orient) ((orient)^1) + +enum +{ + VFC_FULL_VISIBLE = 0, + VFC_PART_VISIBLE, + VFC_FOGGED, + VFC_NOT_VISIBLE, + PVS_FULL_VISIBLE, + PVS_PART_VISIBLE, + PVS_FOGGED +}; + +#define GENCUBEVERTS(x0,x1, y0,y1, z0,z1) \ + GENCUBEVERT(0, x1, y1, z0) \ + GENCUBEVERT(1, x0, y1, z0) \ + GENCUBEVERT(2, x0, y1, z1) \ + GENCUBEVERT(3, x1, y1, z1) \ + GENCUBEVERT(4, x1, y0, z1) \ + GENCUBEVERT(5, x0, y0, z1) \ + GENCUBEVERT(6, x0, y0, z0) \ + GENCUBEVERT(7, x1, y0, z0) + +#define GENFACEVERTX(o,n, x,y,z, xv,yv,zv) GENFACEVERT(o,n, x,y,z, xv,yv,zv) +#define GENFACEVERTSX(x0,x1, y0,y1, z0,z1, c0,c1, r0,r1, d0,d1) \ + GENFACEORIENT(0, GENFACEVERTX(0,0, x0,y1,z1, d0,r1,c1), GENFACEVERTX(0,1, x0,y1,z0, d0,r1,c0), GENFACEVERTX(0,2, x0,y0,z0, d0,r0,c0), GENFACEVERTX(0,3, x0,y0,z1, d0,r0,c1)) \ + GENFACEORIENT(1, GENFACEVERTX(1,0, x1,y1,z1, d1,r1,c1), GENFACEVERTX(1,1, x1,y0,z1, d1,r0,c1), GENFACEVERTX(1,2, x1,y0,z0, d1,r0,c0), GENFACEVERTX(1,3, x1,y1,z0, d1,r1,c0)) +#define GENFACEVERTY(o,n, x,y,z, xv,yv,zv) GENFACEVERT(o,n, x,y,z, xv,yv,zv) +#define GENFACEVERTSY(x0,x1, y0,y1, z0,z1, c0,c1, r0,r1, d0,d1) \ + GENFACEORIENT(2, GENFACEVERTY(2,0, x1,y0,z1, c1,d0,r1), GENFACEVERTY(2,1, x0,y0,z1, c0,d0,r1), GENFACEVERTY(2,2, x0,y0,z0, c0,d0,r0), GENFACEVERTY(2,3, x1,y0,z0, c1,d0,r0)) \ + GENFACEORIENT(3, GENFACEVERTY(3,0, x0,y1,z0, c0,d1,r0), GENFACEVERTY(3,1, x0,y1,z1, c0,d1,r1), GENFACEVERTY(3,2, x1,y1,z1, c1,d1,r1), GENFACEVERTY(3,3, x1,y1,z0, c1,d1,r0)) +#define GENFACEVERTZ(o,n, x,y,z, xv,yv,zv) GENFACEVERT(o,n, x,y,z, xv,yv,zv) +#define GENFACEVERTSZ(x0,x1, y0,y1, z0,z1, c0,c1, r0,r1, d0,d1) \ + GENFACEORIENT(4, GENFACEVERTZ(4,0, x0,y0,z0, r0,c0,d0), GENFACEVERTZ(4,1, x0,y1,z0, r0,c1,d0), GENFACEVERTZ(4,2, x1,y1,z0, r1,c1,d0), GENFACEVERTZ(4,3, x1,y0,z0, r1,c0,d0)) \ + GENFACEORIENT(5, GENFACEVERTZ(5,0, x0,y0,z1, r0,c0,d1), GENFACEVERTZ(5,1, x1,y0,z1, r1,c0,d1), GENFACEVERTZ(5,2, x1,y1,z1, r1,c1,d1), GENFACEVERTZ(5,3, x0,y1,z1, r0,c1,d1)) +#define GENFACEVERTSXY(x0,x1, y0,y1, z0,z1, c0,c1, r0,r1, d0,d1) \ + GENFACEVERTSX(x0,x1, y0,y1, z0,z1, c0,c1, r0,r1, d0,d1) \ + GENFACEVERTSY(x0,x1, y0,y1, z0,z1, c0,c1, r0,r1, d0,d1) +#define GENFACEVERTS(x0,x1, y0,y1, z0,z1, c0,c1, r0,r1, d0,d1) \ + GENFACEVERTSXY(x0,x1, y0,y1, z0,z1, c0,c1, r0,r1, d0,d1) \ + GENFACEVERTSZ(x0,x1, y0,y1, z0,z1, c0,c1, r0,r1, d0,d1) + diff --git a/src/engine/octaedit.cpp b/src/engine/octaedit.cpp new file mode 100644 index 0000000..5baa6f0 --- /dev/null +++ b/src/engine/octaedit.cpp @@ -0,0 +1,2977 @@ +#include "engine.h" + +extern int outline; + +bool boxoutline = false; + +void boxs(int orient, vec o, const vec &s, float size) +{ + int d = dimension(orient), dc = dimcoord(orient); + float f = boxoutline ? (dc>0 ? 0.2f : -0.2f) : 0; + o[D[d]] += dc * s[D[d]] + f; + + vec r(0, 0, 0), c(0, 0, 0); + r[R[d]] = s[R[d]]; + c[C[d]] = s[C[d]]; + + vec v1 = o, v2 = vec(o).add(r), v3 = vec(o).add(r).add(c), v4 = vec(o).add(c); + + r[R[d]] = 0.5f*size; + c[C[d]] = 0.5f*size; + + gle::defvertex(); + gle::begin(GL_TRIANGLE_STRIP); + gle::attrib(vec(v1).sub(r).sub(c)); + gle::attrib(vec(v1).add(r).add(c)); + gle::attrib(vec(v2).add(r).sub(c)); + gle::attrib(vec(v2).sub(r).add(c)); + gle::attrib(vec(v3).add(r).add(c)); + gle::attrib(vec(v3).sub(r).sub(c)); + gle::attrib(vec(v4).sub(r).add(c)); + gle::attrib(vec(v4).add(r).sub(c)); + gle::attrib(vec(v1).sub(r).sub(c)); + gle::attrib(vec(v1).add(r).add(c)); + xtraverts += gle::end(); +} + +void boxs(int orient, vec o, const vec &s) +{ + int d = dimension(orient), dc = dimcoord(orient); + float f = boxoutline ? (dc>0 ? 0.2f : -0.2f) : 0; + o[D[d]] += dc * s[D[d]] + f; + + gle::defvertex(); + gle::begin(GL_LINE_LOOP); + + gle::attrib(o); o[R[d]] += s[R[d]]; + gle::attrib(o); o[C[d]] += s[C[d]]; + gle::attrib(o); o[R[d]] -= s[R[d]]; + gle::attrib(o); + + xtraverts += gle::end(); +} + +void boxs3D(const vec &o, vec s, int g) +{ + s.mul(g); + loopi(6) + boxs(i, o, s); +} + +void boxsgrid(int orient, vec o, vec s, int g) +{ + int d = dimension(orient), dc = dimcoord(orient); + float ox = o[R[d]], + oy = o[C[d]], + xs = s[R[d]], + ys = s[C[d]], + f = boxoutline ? (dc>0 ? 0.2f : -0.2f) : 0; + + o[D[d]] += dc * s[D[d]]*g + f; + + gle::defvertex(); + gle::begin(GL_LINES); + loop(x, xs) + { + o[R[d]] += g; + gle::attrib(o); + o[C[d]] += ys*g; + gle::attrib(o); + o[C[d]] = oy; + } + loop(y, ys) + { + o[C[d]] += g; + o[R[d]] = ox; + gle::attrib(o); + o[R[d]] += xs*g; + gle::attrib(o); + } + xtraverts += gle::end(); +} + +selinfo sel, lastsel, savedsel; + +int orient = 0; +int gridsize = 8; +ivec cor, lastcor; +ivec cur, lastcur; + +extern int entediting; +bool editmode = false; +bool havesel = false; +bool hmapsel = false; +int horient = 0; + +extern int entmoving; + +VARF(dragging, 0, 0, 1, + if(!dragging || cor[0]<0) return; + lastcur = cur; + lastcor = cor; + sel.grid = gridsize; + sel.orient = orient; +); + +int moving = 0; +ICOMMAND(moving, "b", (int *n), +{ + if(*n >= 0) + { + if(!*n || (moving<=1 && !pointinsel(sel, vec(cur).add(1)))) moving = 0; + else if(!moving) moving = 1; + } + intret(moving); +}); + +VARF(gridpower, 0, 3, 12, +{ + if(dragging) return; + gridsize = 1<=worldsize) gridsize = worldsize/2; + cancelsel(); +}); + +VAR(passthroughsel, 0, 0, 1); +VAR(editing, 1, 0, 0); +VAR(selectcorners, 0, 0, 1); +VARF(hmapedit, 0, 0, 1, horient = sel.orient); + +void forcenextundo() { lastsel.orient = -1; } + +extern void hmapcancel(); + +void cubecancel() +{ + havesel = false; + moving = dragging = hmapedit = passthroughsel = 0; + forcenextundo(); + hmapcancel(); +} + +void cancelsel() +{ + cubecancel(); + entcancel(); +} + +void toggleedit(bool force) +{ + if(!force) + { + if(!isconnected()) return; + if(player->state!=CS_ALIVE && player->state!=CS_DEAD && player->state!=CS_EDITING) return; // do not allow dead players to edit to avoid state confusion + if(!game::allowedittoggle()) return; // not in most multiplayer modes + } + if(!(editmode = !editmode)) + { + player->state = player->editstate; + player->o.z -= player->eyeheight; // entinmap wants feet pos + entinmap(player); // find spawn closest to current floating pos + } + else + { + game::resetgamestate(); + player->editstate = player->state; + player->state = CS_EDITING; + } + cancelsel(); + stoppaintblendmap(); + keyrepeat(editmode); + editing = entediting = editmode; + extern int fullbright; + if(fullbright) { initlights(); lightents(); } + if(!force) game::edittoggled(editmode); +} + +VARP(editinview, 0, 1, 1); + +bool noedit(bool view, bool msg) +{ + if(!editmode) { if(msg) conoutf(CON_ERROR, "operation only allowed in edit mode"); return true; } + if(view || haveselent()) return false; + float r = 1.0f; + vec o(sel.o), s(sel.s); + s.mul(float(sel.grid) / 2.0f); + o.add(s); + r = float(max(s.x, max(s.y, s.z))); + bool viewable = (isvisiblesphere(r, o) != VFC_NOT_VISIBLE); + if(viewable || !editinview) return false; + if(msg) conoutf(CON_ERROR, "selection not in view"); + return true; +} + +void reorient() +{ + sel.cx = 0; + sel.cy = 0; + sel.cxs = sel.s[R[dimension(orient)]]*2; + sel.cys = sel.s[C[dimension(orient)]]*2; + sel.orient = orient; +} + +void selextend() +{ + if(noedit(true)) return; + loopi(3) + { + if(cur[i]=sel.o[i]+sel.s[i]*sel.grid) + { + sel.s[i] = (cur[i]-sel.o[i])/sel.grid+1; + } + } +} + +ICOMMAND(edittoggle, "", (), toggleedit(false)); +COMMAND(entcancel, ""); +COMMAND(cubecancel, ""); +COMMAND(cancelsel, ""); +COMMAND(reorient, ""); +COMMAND(selextend, ""); + +ICOMMAND(selmoved, "", (), { if(noedit(true)) return; intret(sel.o != savedsel.o ? 1 : 0); }); +ICOMMAND(selsave, "", (), { if(noedit(true)) return; savedsel = sel; }); +ICOMMAND(selrestore, "", (), { if(noedit(true)) return; sel = savedsel; }); +ICOMMAND(selswap, "", (), { if(noedit(true)) return; swap(sel, savedsel); }); + +ICOMMAND(getselpos, "", (), +{ + if(noedit(true)) return; + defformatstring(pos, "%s %s %s", floatstr(sel.o.x), floatstr(sel.o.y), floatstr(sel.o.z)); + result(pos); +}); + +void setselpos(int *x, int *y, int *z) +{ + if(noedit(moving!=0)) return; + havesel = true; + sel.o = ivec(*x, *y, *z).mask(~(gridsize-1)); +} +COMMAND(setselpos, "iii"); + +void movesel(int *dir, int *dim) +{ + if(noedit(moving!=0)) return; + if(*dim < 0 || *dim > 2) return; + sel.o[*dim] += *dir * sel.grid; +} +COMMAND(movesel, "ii"); + +///////// selection support ///////////// + +cube &blockcube(int x, int y, int z, const block3 &b, int rgrid) // looks up a world cube, based on coordinates mapped by the block +{ + int dim = dimension(b.orient), dc = dimcoord(b.orient); + ivec s(dim, x*b.grid, y*b.grid, dc*(b.s[dim]-1)*b.grid); + s.add(b.o); + if(dc) s[dim] -= z*b.grid; else s[dim] += z*b.grid; + return lookupcube(s, rgrid); +} + +#define loopxy(b) loop(y,(b).s[C[dimension((b).orient)]]) loop(x,(b).s[R[dimension((b).orient)]]) +#define loopxyz(b, r, f) { loop(z,(b).s[D[dimension((b).orient)]]) loopxy((b)) { cube &c = blockcube(x,y,z,b,r); f; } } +#define loopselxyz(f) { if(local) makeundo(); loopxyz(sel, sel.grid, f); changed(sel); } +#define selcube(x, y, z) blockcube(x, y, z, sel, sel.grid) + +////////////// cursor /////////////// + +int selchildcount = 0, selchildmat = -1; + +ICOMMAND(havesel, "", (), intret(havesel ? selchildcount : 0)); + +void countselchild(cube *c, const ivec &cor, int size) +{ + ivec ss = ivec(sel.s).mul(sel.grid); + loopoctaboxsize(cor, size, sel.o, ss) + { + ivec o(i, cor, size); + if(c[i].children) countselchild(c[i].children, o, size/2); + else + { + selchildcount++; + if(c[i].material != MAT_AIR && selchildmat != MAT_AIR) + { + if(selchildmat < 0) selchildmat = c[i].material; + else if(selchildmat != c[i].material) selchildmat = MAT_AIR; + } + } + } +} + +void normalizelookupcube(const ivec &o) +{ + if(lusize>gridsize) + { + lu.x += (o.x-lu.x)/gridsize*gridsize; + lu.y += (o.y-lu.y)/gridsize*gridsize; + lu.z += (o.z-lu.z)/gridsize*gridsize; + } + else if(gridsize>lusize) + { + lu.x &= ~(gridsize-1); + lu.y &= ~(gridsize-1); + lu.z &= ~(gridsize-1); + } + lusize = gridsize; +} + +void updateselection() +{ + sel.o.x = min(lastcur.x, cur.x); + sel.o.y = min(lastcur.y, cur.y); + sel.o.z = min(lastcur.z, cur.z); + sel.s.x = abs(lastcur.x-cur.x)/sel.grid+1; + sel.s.y = abs(lastcur.y-cur.y)/sel.grid+1; + sel.s.z = abs(lastcur.z-cur.z)/sel.grid+1; +} + +bool editmoveplane(const vec &o, const vec &ray, int d, float off, vec &handle, vec &dest, bool first) +{ + plane pl(d, off); + float dist = 0.0f; + if(!pl.rayintersect(player->o, ray, dist)) + return false; + + dest = vec(ray).mul(dist).add(player->o); + if(first) handle = vec(dest).sub(o); + dest.sub(handle); + return true; +} + +inline bool isheightmap(int orient, int d, bool empty, cube *c); +extern void entdrag(const vec &ray); +extern bool hoveringonent(int ent, int orient); +extern void renderentselection(const vec &o, const vec &ray, bool entmoving); +extern float rayent(const vec &o, const vec &ray, float radius, int mode, int size, int &orient, int &ent); + +VAR(gridlookup, 0, 0, 1); +VAR(passthroughcube, 0, 1, 1); +VAR(passthroughent, 0, 1, 1); +VARF(passthrough, 0, 0, 1, { passthroughsel = passthrough; entcancel(); }); + +void rendereditcursor() +{ + int d = dimension(sel.orient), + od = dimension(orient), + odc = dimcoord(orient); + + bool hidecursor = g3d_windowhit(true, false) || blendpaintmode, hovering = false; + hmapsel = false; + + if(moving) + { + static vec dest, handle; + if(editmoveplane(vec(sel.o), camdir, od, sel.o[D[od]]+odc*sel.grid*sel.s[D[od]], handle, dest, moving==1)) + { + if(moving==1) + { + dest.add(handle); + handle = vec(ivec(handle).mask(~(sel.grid-1))); + dest.sub(handle); + moving = 2; + } + ivec o = ivec(dest).mask(~(sel.grid-1)); + sel.o[R[od]] = o[R[od]]; + sel.o[C[od]] = o[C[od]]; + } + } + else + if(entmoving) + { + entdrag(camdir); + } + else + { + ivec w; + float sdist = 0, wdist = 0, t; + int entorient = 0, ent = -1; + + wdist = rayent(player->o, camdir, 1e16f, + (editmode && showmat ? RAY_EDITMAT : 0) // select cubes first + | (!dragging && entediting && (!passthrough || !passthroughent) ? RAY_ENTS : 0) + | RAY_SKIPFIRST + | (passthroughcube || passthrough ? RAY_PASS : 0), gridsize, entorient, ent); + + if((havesel || dragging) && !passthroughsel && !hmapedit) // now try selecting the selection + if(rayboxintersect(vec(sel.o), vec(sel.s).mul(sel.grid), player->o, camdir, sdist, orient)) + { // and choose the nearest of the two + if(sdist < wdist) + { + wdist = sdist; + ent = -1; + } + } + + if((hovering = hoveringonent(hidecursor ? -1 : ent, entorient))) + { + if(!havesel) + { + selchildcount = 0; + selchildmat = -1; + sel.s = ivec(0, 0, 0); + } + } + else + { + vec w = vec(camdir).mul(wdist+0.05f).add(player->o); + if(!insideworld(w)) + { + loopi(3) wdist = min(wdist, ((camdir[i] > 0 ? worldsize : 0) - player->o[i]) / camdir[i]); + w = vec(camdir).mul(wdist-0.05f).add(player->o); + if(!insideworld(w)) + { + wdist = 0; + loopi(3) w[i] = clamp(player->o[i], 0.0f, float(worldsize)); + } + } + cube *c = &lookupcube(ivec(w)); + if(gridlookup && !dragging && !moving && !havesel && hmapedit!=1) gridsize = lusize; + int mag = lusize / gridsize; + normalizelookupcube(ivec(w)); + if(sdist == 0 || sdist > wdist) rayboxintersect(vec(lu), vec(gridsize), player->o, camdir, t=0, orient); // just getting orient + cur = lu; + cor = ivec(vec(w).mul(2).div(gridsize)); + od = dimension(orient); + d = dimension(sel.orient); + + if(hmapedit==1 && dimcoord(horient) == (camdir[dimension(horient)]<0)) + { + hmapsel = isheightmap(horient, dimension(horient), false, c); + if(hmapsel) + od = dimension(orient = horient); + } + + if(dragging) + { + updateselection(); + sel.cx = min(cor[R[d]], lastcor[R[d]]); + sel.cy = min(cor[C[d]], lastcor[C[d]]); + sel.cxs = max(cor[R[d]], lastcor[R[d]]); + sel.cys = max(cor[C[d]], lastcor[C[d]]); + + if(!selectcorners) + { + sel.cx &= ~1; + sel.cy &= ~1; + sel.cxs &= ~1; + sel.cys &= ~1; + sel.cxs -= sel.cx-2; + sel.cys -= sel.cy-2; + } + else + { + sel.cxs -= sel.cx-1; + sel.cys -= sel.cy-1; + } + + sel.cx &= 1; + sel.cy &= 1; + havesel = true; + } + else if(!havesel) + { + sel.o = lu; + sel.s.x = sel.s.y = sel.s.z = 1; + sel.cx = sel.cy = 0; + sel.cxs = sel.cys = 2; + sel.grid = gridsize; + sel.orient = orient; + d = od; + } + + sel.corner = (cor[R[d]]-(lu[R[d]]*2)/gridsize)+(cor[C[d]]-(lu[C[d]]*2)/gridsize)*2; + selchildcount = 0; + selchildmat = -1; + countselchild(worldroot, ivec(0, 0, 0), worldsize/2); + if(mag>=1 && selchildcount==1) + { + selchildmat = c->material; + if(mag>1) selchildcount = -mag; + } + } + } + + glEnable(GL_BLEND); + glBlendFunc(GL_ONE, GL_ONE); + + // cursors + + notextureshader->set(); + + renderentselection(player->o, camdir, entmoving!=0); + + boxoutline = outline!=0; + + enablepolygonoffset(GL_POLYGON_OFFSET_LINE); + + if(!moving && !hovering && !hidecursor) + { + if(hmapedit==1) + gle::colorub(0, hmapsel ? 255 : 40, 0); + else + gle::colorub(120,120,120); + boxs(orient, vec(lu), vec(lusize)); + } + + // selections + if(havesel || moving) + { + d = dimension(sel.orient); + gle::colorub(50,50,50); // grid + boxsgrid(sel.orient, vec(sel.o), vec(sel.s), sel.grid); + gle::colorub(200,0,0); // 0 reference + boxs3D(vec(sel.o).sub(0.5f*min(gridsize*0.25f, 2.0f)), vec(min(gridsize*0.25f, 2.0f)), 1); + gle::colorub(200,200,200);// 2D selection box + vec co(sel.o.v), cs(sel.s.v); + co[R[d]] += 0.5f*(sel.cx*gridsize); + co[C[d]] += 0.5f*(sel.cy*gridsize); + cs[R[d]] = 0.5f*(sel.cxs*gridsize); + cs[C[d]] = 0.5f*(sel.cys*gridsize); + cs[D[d]] *= gridsize; + boxs(sel.orient, co, cs); + if(hmapedit==1) // 3D selection box + gle::colorub(0,120,0); + else + gle::colorub(0,0,120); + boxs3D(vec(sel.o), vec(sel.s), sel.grid); + } + + disablepolygonoffset(GL_POLYGON_OFFSET_LINE); + + boxoutline = false; + + glDisable(GL_BLEND); +} + +void tryedit() +{ + extern int hidehud; + if(!editmode || hidehud || mainmenu) return; + if(blendpaintmode) trypaintblendmap(); +} + +//////////// ready changes to vertex arrays //////////// + +static bool haschanged = false; + +void readychanges(const ivec &bbmin, const ivec &bbmax, cube *c, const ivec &cor, int size) +{ + loopoctabox(cor, size, bbmin, bbmax) + { + ivec o(i, cor, size); + if(c[i].ext) + { + if(c[i].ext->va) // removes va s so that octarender will recreate + { + int hasmerges = c[i].ext->va->hasmerges; + destroyva(c[i].ext->va); + c[i].ext->va = NULL; + if(hasmerges) invalidatemerges(c[i], o, size, true); + } + freeoctaentities(c[i]); + c[i].ext->tjoints = -1; + } + if(c[i].children) + { + if(size<=1) + { + solidfaces(c[i]); + discardchildren(c[i], true); + brightencube(c[i]); + } + else readychanges(bbmin, bbmax, c[i].children, o, size/2); + } + else brightencube(c[i]); + } +} + +void commitchanges(bool force) +{ + if(!force && !haschanged) return; + haschanged = false; + + int oldlen = valist.length(); + resetclipplanes(); + entitiesinoctanodes(); + inbetweenframes = false; + octarender(); + inbetweenframes = true; + setupmaterials(oldlen); + invalidatepostfx(); + updatevabbs(); + resetblobs(); +} + +void changed(const block3 &sel, bool commit = true) +{ + if(sel.s.iszero()) return; + readychanges(ivec(sel.o).sub(1), ivec(sel.s).mul(sel.grid).add(sel.o).add(1), worldroot, ivec(0, 0, 0), worldsize/2); + haschanged = true; + + if(commit) commitchanges(); +} + +//////////// copy and undo ///////////// +static inline void copycube(const cube &src, cube &dst) +{ + dst = src; + dst.visible = 0; + dst.merged = 0; + dst.ext = NULL; // src cube is responsible for va destruction + if(src.children) + { + dst.children = newcubes(F_EMPTY); + loopi(8) copycube(src.children[i], dst.children[i]); + } +} + +static inline void pastecube(const cube &src, cube &dst) +{ + discardchildren(dst); + copycube(src, dst); +} + +void blockcopy(const block3 &s, int rgrid, block3 *b) +{ + *b = s; + cube *q = b->c(); + loopxyz(s, rgrid, copycube(c, *q++)); +} + +block3 *blockcopy(const block3 &s, int rgrid) +{ + int bsize = sizeof(block3)+sizeof(cube)*s.size(); + if(bsize <= 0 || bsize > (100<<20)) return NULL; + block3 *b = (block3 *)new (false) uchar[bsize]; + if(b) blockcopy(s, rgrid, b); + return b; +} + +void freeblock(block3 *b, bool alloced = true) +{ + cube *q = b->c(); + loopi(b->size()) discardchildren(*q++); + if(alloced) delete[] b; +} + +void selgridmap(selinfo &sel, uchar *g) // generates a map of the cube sizes at each grid point +{ + loopxyz(sel, -sel.grid, (*g++ = bitscan(lusize), (void)c)); +} + +void freeundo(undoblock *u) +{ + if(!u->numents) freeblock(u->block(), false); + delete[] (uchar *)u; +} + +void pasteundoblock(block3 *b, uchar *g) +{ + cube *s = b->c(); + loopxyz(*b, 1<numents) pasteundoents(u); + else pasteundoblock(u->block(), u->gridmap()); +} + +static inline int undosize(undoblock *u) +{ + if(u->numents) return u->numents*sizeof(undoent); + else + { + block3 *b = u->block(); + cube *q = b->c(); + int size = b->size(), total = size; + loopj(size) total += familysize(*q++)*sizeof(cube); + return total; + } +} + +struct undolist +{ + undoblock *first, *last; + + undolist() : first(NULL), last(NULL) {} + + bool empty() { return !first; } + + void add(undoblock *u) + { + u->next = NULL; + u->prev = last; + if(!first) first = last = u; + else + { + last->next = u; + last = u; + } + } + + undoblock *popfirst() + { + undoblock *u = first; + first = first->next; + if(first) first->prev = NULL; + else last = NULL; + return u; + } + + undoblock *poplast() + { + undoblock *u = last; + last = last->prev; + if(last) last->next = NULL; + else first = NULL; + return u; + } +}; + +undolist undos, redos; +VARP(undomegs, 0, 8, 100); // bounded by n megs +int totalundos = 0; + +void pruneundos(int maxremain) // bound memory +{ + while(totalundos > maxremain && !undos.empty()) + { + undoblock *u = undos.popfirst(); + totalundos -= u->size; + freeundo(u); + } + //conoutf(CON_DEBUG, "undo: %d of %d(%%%d)", totalundos, undomegs<<20, totalundos*100/(undomegs<<20)); + while(!redos.empty()) + { + undoblock *u = redos.popfirst(); + totalundos -= u->size; + freeundo(u); + } +} + +void clearundos() { pruneundos(0); } + +COMMAND(clearundos, ""); + +undoblock *newundocube(selinfo &s) +{ + int ssize = s.size(), + selgridsize = ssize, + blocksize = sizeof(block3)+ssize*sizeof(cube); + if(blocksize <= 0 || blocksize > (undomegs<<20)) return NULL; + undoblock *u = (undoblock *)new (false) uchar[sizeof(undoblock) + blocksize + selgridsize]; + if(!u) return NULL; + u->numents = 0; + block3 *b = u->block(); + blockcopy(s, -s.grid, b); + uchar *g = u->gridmap(); + selgridmap(s, g); + return u; +} + +void addundo(undoblock *u) +{ + u->size = undosize(u); + u->timestamp = totalmillis; + undos.add(u); + totalundos += u->size; + pruneundos(undomegs<<20); +} + +VARP(nompedit, 0, 1, 1); + +void makeundo(selinfo &s) +{ + undoblock *u = newundocube(s); + if(u) addundo(u); +} + +void makeundo() // stores state of selected cubes before editing +{ + if(lastsel==sel || sel.s.iszero()) return; + lastsel=sel; + makeundo(sel); +} + +static inline int countblock(cube *c, int n = 8) +{ + int r = 0; + loopi(n) if(c[i].children) r += countblock(c[i].children); else ++r; + return r; +} + +static int countblock(block3 *b) { return countblock(b->c(), b->size()); } + +void swapundo(undolist &a, undolist &b, int op) +{ + if(noedit()) return; + if(a.empty()) { conoutf(CON_WARN, "nothing more to %s", op == EDIT_REDO ? "redo" : "undo"); return; } + int ts = a.last->timestamp; + if(multiplayer(false)) + { + int n = 0, ops = 0; + for(undoblock *u = a.last; u && ts==u->timestamp; u = u->prev) + { + ++ops; + n += u->numents ? u->numents : countblock(u->block()); + if(ops > 10 || n > 2500) + { + conoutf(CON_WARN, "undo too big for multiplayer"); + if(nompedit) { multiplayer(); return; } + op = -1; + break; + } + } + } + selinfo l = sel; + while(!a.empty() && ts==a.last->timestamp) + { + if(op >= 0) game::edittrigger(sel, op); + undoblock *u = a.poplast(), *r; + if(u->numents) r = copyundoents(u); + else + { + block3 *ub = u->block(); + l.o = ub->o; + l.s = ub->s; + l.grid = ub->grid; + l.orient = ub->orient; + r = newundocube(l); + } + if(r) + { + r->size = u->size; + r->timestamp = totalmillis; + b.add(r); + } + pasteundo(u); + if(!u->numents) changed(*u->block(), false); + freeundo(u); + } + commitchanges(); + if(!hmapsel) + { + sel = l; + reorient(); + } + forcenextundo(); +} + +void editundo() { swapundo(undos, redos, EDIT_UNDO); } +void editredo() { swapundo(redos, undos, EDIT_REDO); } + +// guard against subdivision +#define protectsel(f) { undoblock *_u = newundocube(sel); f; if(_u) { pasteundo(_u); freeundo(_u); } } + +vector editinfos; +editinfo *localedit = NULL; + +template +static void packcube(cube &c, B &buf) +{ + if(c.children) + { + buf.put(0xFF); + loopi(8) packcube(c.children[i], buf); + } + else + { + cube data = c; + lilswap(data.texture, 6); + buf.put(c.material&0xFF); + buf.put(c.material>>8); + buf.put(data.edges, sizeof(data.edges)); + buf.put((uchar *)data.texture, sizeof(data.texture)); + } +} + +template +static bool packblock(block3 &b, B &buf) +{ + if(b.size() <= 0 || b.size() > (1<<20)) return false; + block3 hdr = b; + lilswap(hdr.o.v, 3); + lilswap(hdr.s.v, 3); + lilswap(&hdr.grid, 1); + lilswap(&hdr.orient, 1); + buf.put((const uchar *)&hdr, sizeof(hdr)); + cube *c = b.c(); + loopi(b.size()) packcube(c[i], buf); + return true; +} + +struct vslothdr +{ + ushort index; + ushort slot; +}; + +static void packvslots(cube &c, vector &buf, vector &used) +{ + if(c.children) + { + loopi(8) packvslots(c.children[i], buf, used); + } + else loopi(6) + { + ushort index = c.texture[i]; + if(vslots.inrange(index) && vslots[index]->changed && used.find(index) < 0) + { + used.add(index); + VSlot &vs = *vslots[index]; + vslothdr &hdr = *(vslothdr *)buf.pad(sizeof(vslothdr)); + hdr.index = index; + hdr.slot = vs.slot->index; + lilswap(&hdr.index, 2); + packvslot(buf, vs); + } + } +} + +static void packvslots(block3 &b, vector &buf) +{ + vector used; + cube *c = b.c(); + loopi(b.size()) packvslots(c[i], buf, used); + memset(buf.pad(sizeof(vslothdr)), 0, sizeof(vslothdr)); +} + +template +static void unpackcube(cube &c, B &buf) +{ + int mat = buf.get(); + if(mat == 0xFF) + { + c.children = newcubes(F_EMPTY); + loopi(8) unpackcube(c.children[i], buf); + } + else + { + c.material = mat | (buf.get()<<8); + buf.get(c.edges, sizeof(c.edges)); + buf.get((uchar *)c.texture, sizeof(c.texture)); + lilswap(c.texture, 6); + } +} + +template +static bool unpackblock(block3 *&b, B &buf) +{ + if(b) { freeblock(b); b = NULL; } + block3 hdr; + if(buf.get((uchar *)&hdr, sizeof(hdr)) < int(sizeof(hdr))) return false; + lilswap(hdr.o.v, 3); + lilswap(hdr.s.v, 3); + lilswap(&hdr.grid, 1); + lilswap(&hdr.orient, 1); + if(hdr.size() > (1<<20) || hdr.grid <= 0 || hdr.grid > (1<<12)) return false; + b = (block3 *)new (false) uchar[sizeof(block3)+hdr.size()*sizeof(cube)]; + if(!b) return false; + *b = hdr; + cube *c = b->c(); + memset(c, 0, b->size()*sizeof(cube)); + loopi(b->size()) unpackcube(c[i], buf); + return true; +} + +struct vslotmap +{ + int index; + VSlot *vslot; + + vslotmap() {} + vslotmap(int index, VSlot *vslot) : index(index), vslot(vslot) {} +}; +static vector unpackingvslots; + +static void unpackvslots(cube &c, ucharbuf &buf) +{ + if(c.children) + { + loopi(8) unpackvslots(c.children[i], buf); + } + else loopi(6) + { + ushort tex = c.texture[i]; + loopvj(unpackingvslots) if(unpackingvslots[j].index == tex) { c.texture[i] = unpackingvslots[j].vslot->index; break; } + } +} + +static void unpackvslots(block3 &b, ucharbuf &buf) +{ + while(buf.remaining() >= int(sizeof(vslothdr))) + { + vslothdr &hdr = *(vslothdr *)buf.pad(sizeof(vslothdr)); + lilswap(&hdr.index, 2); + if(!hdr.index) break; + VSlot &vs = *lookupslot(hdr.slot, false).variants; + VSlot ds; + if(!unpackvslot(buf, ds, false)) break; + if(vs.index < 0 || vs.index == DEFAULT_SKY) continue; + VSlot *edit = editvslot(vs, ds); + unpackingvslots.add(vslotmap(hdr.index, edit ? edit : &vs)); + } + + cube *c = b.c(); + loopi(b.size()) unpackvslots(c[i], buf); + + unpackingvslots.setsize(0); +} + +static bool compresseditinfo(const uchar *inbuf, int inlen, uchar *&outbuf, int &outlen) +{ + uLongf len = compressBound(inlen); + if(len > (1<<20)) return false; + outbuf = new (false) uchar[len]; + if(!outbuf || compress2((Bytef *)outbuf, &len, (const Bytef *)inbuf, inlen, Z_BEST_COMPRESSION) != Z_OK || len > (1<<16)) + { + delete[] outbuf; + outbuf = NULL; + return false; + } + outlen = len; + return true; +} + +static bool uncompresseditinfo(const uchar *inbuf, int inlen, uchar *&outbuf, int &outlen) +{ + if(compressBound(outlen) > (1<<20)) return false; + uLongf len = outlen; + outbuf = new (false) uchar[len]; + if(!outbuf || uncompress((Bytef *)outbuf, &len, (const Bytef *)inbuf, inlen) != Z_OK) + { + delete[] outbuf; + outbuf = NULL; + return false; + } + outlen = len; + return true; +} + +bool packeditinfo(editinfo *e, int &inlen, uchar *&outbuf, int &outlen) +{ + vector buf; + if(!e || !e->copy || !packblock(*e->copy, buf)) return false; + packvslots(*e->copy, buf); + inlen = buf.length(); + return compresseditinfo(buf.getbuf(), buf.length(), outbuf, outlen); +} + +bool unpackeditinfo(editinfo *&e, const uchar *inbuf, int inlen, int outlen) +{ + if(e && e->copy) { freeblock(e->copy); e->copy = NULL; } + uchar *outbuf = NULL; + if(!uncompresseditinfo(inbuf, inlen, outbuf, outlen)) return false; + ucharbuf buf(outbuf, outlen); + if(!e) e = editinfos.add(new editinfo); + if(!unpackblock(e->copy, buf)) + { + delete[] outbuf; + return false; + } + unpackvslots(*e->copy, buf); + delete[] outbuf; + return true; +} + +void freeeditinfo(editinfo *&e) +{ + if(!e) return; + editinfos.removeobj(e); + if(e->copy) freeblock(e->copy); + delete e; + e = NULL; +} + +bool packundo(undoblock *u, int &inlen, uchar *&outbuf, int &outlen) +{ + vector buf; + buf.reserve(512); + *(ushort *)buf.pad(2) = lilswap(ushort(u->numents)); + if(u->numents) + { + undoent *ue = u->ents(); + loopi(u->numents) + { + *(ushort *)buf.pad(2) = lilswap(ushort(ue[i].i)); + entity &e = *(entity *)buf.pad(sizeof(entity)); + e = ue[i].e; + lilswap(&e.o.x, 3); + lilswap(&e.attr1, 5); + } + } + else + { + block3 &b = *u->block(); + if(!packblock(b, buf)) return false; + buf.put(u->gridmap(), b.size()); + packvslots(b, buf); + } + inlen = buf.length(); + return compresseditinfo(buf.getbuf(), buf.length(), outbuf, outlen); +} + +bool unpackundo(const uchar *inbuf, int inlen, int outlen) +{ + uchar *outbuf = NULL; + if(!uncompresseditinfo(inbuf, inlen, outbuf, outlen)) return false; + ucharbuf buf(outbuf, outlen); + if(buf.remaining() < 2) + { + delete[] outbuf; + return false; + } + int numents = lilswap(*(const ushort *)buf.pad(2)); + if(numents) + { + if(buf.remaining() < numents*int(2 + sizeof(entity))) + { + delete[] outbuf; + return false; + } + loopi(numents) + { + int idx = lilswap(*(const ushort *)buf.pad(2)); + entity &e = *(entity *)buf.pad(sizeof(entity)); + lilswap(&e.o.x, 3); + lilswap(&e.attr1, 5); + pasteundoent(idx, e); + } + } + else + { + block3 *b = NULL; + if(!unpackblock(b, buf) || b->grid >= worldsize || buf.remaining() < b->size()) + { + freeblock(b); + delete[] outbuf; + return false; + } + uchar *g = buf.pad(b->size()); + unpackvslots(*b, buf); + pasteundoblock(b, g); + changed(*b, false); + freeblock(b); + } + delete[] outbuf; + commitchanges(); + return true; +} + +bool packundo(int op, int &inlen, uchar *&outbuf, int &outlen) +{ + switch(op) + { + case EDIT_UNDO: return !undos.empty() && packundo(undos.last, inlen, outbuf, outlen); + case EDIT_REDO: return !redos.empty() && packundo(redos.last, inlen, outbuf, outlen); + default: return false; + } +} + +struct prefabheader +{ + char magic[4]; + int version; +}; + +struct prefab : editinfo +{ + char *name; + GLuint ebo, vbo; + int numtris, numverts; + + prefab() : name(NULL), ebo(0), vbo(0), numtris(0), numverts(0) {} + ~prefab() { DELETEA(name); if(copy) freeblock(copy); } + + void cleanup() + { + if(ebo) { glDeleteBuffers_(1, &ebo); ebo = 0; } + if(vbo) { glDeleteBuffers_(1, &vbo); vbo = 0; } + numtris = numverts = 0; + } +}; + +static hashnameset prefabs; + +void cleanupprefabs() +{ + enumerate(prefabs, prefab, p, p.cleanup()); +} + +void delprefab(char *name) +{ + prefab *p = prefabs.access(name); + if(p) + { + p->cleanup(); + prefabs.remove(name); + conoutf("deleted prefab %s", name); + } +} +COMMAND(delprefab, "s"); + +void saveprefab(char *name) +{ + if(!name[0] || noedit(true) || (nompedit && multiplayer())) return; + prefab *b = prefabs.access(name); + if(!b) + { + b = &prefabs[name]; + b->name = newstring(name); + } + if(b->copy) freeblock(b->copy); + protectsel(b->copy = blockcopy(block3(sel), sel.grid)); + changed(sel); + defformatstring(filename, strpbrk(name, "/\\") ? "packages/%s.obr" : "packages/prefab/%s.obr", name); + path(filename); + stream *f = opengzfile(filename, "wb"); + if(!f) { conoutf(CON_ERROR, "could not write prefab to %s", filename); return; } + prefabheader hdr; + memcpy(hdr.magic, "OEBR", 4); + hdr.version = 0; + lilswap(&hdr.version, 1); + f->write(&hdr, sizeof(hdr)); + streambuf s(f); + if(!packblock(*b->copy, s)) { delete f; conoutf(CON_ERROR, "could not pack prefab %s", filename); return; } + delete f; + conoutf("wrote prefab file %s", filename); +} +COMMAND(saveprefab, "s"); + +void pasteblock(block3 &b, selinfo &sel, bool local) +{ + sel.s = b.s; + int o = sel.orient; + sel.orient = b.orient; + cube *s = b.c(); + loopselxyz(if(!isempty(*s) || s->children || s->material != MAT_AIR) pastecube(*s, c); s++); // 'transparent'. old opaque by 'delcube; paste' + sel.orient = o; +} + +bool prefabloaded(const char *name) +{ + return prefabs.access(name) != NULL; +} + +prefab *loadprefab(const char *name, bool msg = true) +{ + prefab *b = prefabs.access(name); + if(b) return b; + + defformatstring(filename, strpbrk(name, "/\\") ? "packages/%s.obr" : "packages/prefab/%s.obr", name); + path(filename); + stream *f = opengzfile(filename, "rb"); + if(!f) { if(msg) conoutf(CON_ERROR, "could not read prefab %s", filename); return NULL; } + prefabheader hdr; + if(f->read(&hdr, sizeof(hdr)) != sizeof(prefabheader) || memcmp(hdr.magic, "OEBR", 4)) { delete f; if(msg) conoutf(CON_ERROR, "prefab %s has malformatted header", filename); return NULL; } + lilswap(&hdr.version, 1); + if(hdr.version != 0) { delete f; if(msg) conoutf(CON_ERROR, "prefab %s uses unsupported version", filename); return NULL; } + streambuf s(f); + block3 *copy = NULL; + if(!unpackblock(copy, s)) { delete f; if(msg) conoutf(CON_ERROR, "could not unpack prefab %s", filename); return NULL; } + delete f; + + b = &prefabs[name]; + b->name = newstring(name); + b->copy = copy; + + return b; +} + +void pasteprefab(char *name) +{ + if(!name[0] || noedit() || (nompedit && multiplayer())) return; + prefab *b = loadprefab(name, true); + if(b) pasteblock(*b->copy, sel, true); +} +COMMAND(pasteprefab, "s"); + +struct prefabmesh +{ + struct vertex { vec pos; bvec4 norm; }; + + static const int SIZE = 1<<9; + int table[SIZE]; + vector verts; + vector chain; + vector tris; + + prefabmesh() { memset(table, -1, sizeof(table)); } + + int addvert(const vertex &v) + { + uint h = hthash(v.pos)&(SIZE-1); + for(int i = table[h]; i>=0; i = chain[i]) + { + const vertex &c = verts[i]; + if(c.pos==v.pos && c.norm==v.norm) return i; + } + if(verts.length() >= USHRT_MAX) return -1; + verts.add(v); + chain.add(table[h]); + return table[h] = verts.length()-1; + } + + int addvert(const vec &pos, const bvec &norm) + { + vertex vtx; + vtx.pos = pos; + vtx.norm = norm; + return addvert(vtx); + } + + void setup(prefab &p) + { + if(tris.empty()) return; + + p.cleanup(); + + loopv(verts) verts[i].norm.flip(); + if(!p.vbo) glGenBuffers_(1, &p.vbo); + gle::bindvbo(p.vbo); + glBufferData_(GL_ARRAY_BUFFER, verts.length()*sizeof(vertex), verts.getbuf(), GL_STATIC_DRAW); + gle::clearvbo(); + p.numverts = verts.length(); + + if(!p.ebo) glGenBuffers_(1, &p.ebo); + gle::bindebo(p.ebo); + glBufferData_(GL_ELEMENT_ARRAY_BUFFER, tris.length()*sizeof(ushort), tris.getbuf(), GL_STATIC_DRAW); + gle::clearebo(); + p.numtris = tris.length()/3; + } + +}; + +static void genprefabmesh(prefabmesh &r, cube &c, const ivec &co, int size) +{ + if(c.children) + { + neighbourstack[++neighbourdepth] = c.children; + loopi(8) + { + ivec o(i, co, size/2); + genprefabmesh(r, c.children[i], o, size/2); + } + --neighbourdepth; + } + else if(!isempty(c)) + { + int vis; + loopi(6) if((vis = visibletris(c, i, co, size))) + { + ivec v[4]; + genfaceverts(c, i, v); + int convex = 0; + if(!flataxisface(c, i)) convex = faceconvexity(v); + int order = vis&4 || convex < 0 ? 1 : 0, numverts = 0; + vec vo(co), pos[4], norm[4]; + pos[numverts++] = vec(v[order]).mul(size/8.0f).add(vo); + if(vis&1) pos[numverts++] = vec(v[order+1]).mul(size/8.0f).add(vo); + pos[numverts++] = vec(v[order+2]).mul(size/8.0f).add(vo); + if(vis&2) pos[numverts++] = vec(v[(order+3)&3]).mul(size/8.0f).add(vo); + guessnormals(pos, numverts, norm); + int index[4]; + loopj(numverts) index[j] = r.addvert(pos[j], bvec(norm[j])); + loopj(numverts-2) if(index[0]!=index[j+1] && index[j+1]!=index[j+2] && index[j+2]!=index[0]) + { + r.tris.add(index[0]); + r.tris.add(index[j+1]); + r.tris.add(index[j+2]); + } + } + } +} + +void genprefabmesh(prefab &p) +{ + block3 b = *p.copy; + b.o = ivec(0, 0, 0); + + cube *oldworldroot = worldroot; + int oldworldscale = worldscale, oldworldsize = worldsize; + + worldroot = newcubes(); + worldscale = 1; + worldsize = 2; + while(worldsize < max(max(b.s.x, b.s.y), b.s.z)*b.grid) + { + worldscale++; + worldsize *= 2; + } + + cube *s = p.copy->c(); + loopxyz(b, b.grid, if(!isempty(*s) || s->children) pastecube(*s, c); s++); + + prefabmesh r; + neighbourstack[++neighbourdepth] = worldroot; + loopi(8) genprefabmesh(r, worldroot[i], ivec(i, ivec(0, 0, 0), worldsize/2), worldsize/2); + --neighbourdepth; + r.setup(p); + + freeocta(worldroot); + + worldroot = oldworldroot; + worldscale = oldworldscale; + worldsize = oldworldsize; + + useshaderbyname("prefab"); +} + +extern int outlinecolour; + +static void renderprefab(prefab &p, const vec &o, float yaw, float pitch, float roll, float size, const vec &color) +{ + if(!p.numtris) + { + genprefabmesh(p); + if(!p.numtris) return; + } + + block3 &b = *p.copy; + + matrix4 m; + m.identity(); + m.settranslation(o); + if(yaw) m.rotate_around_z(yaw*RAD); + if(pitch) m.rotate_around_x(pitch*RAD); + if(roll) m.rotate_around_y(-roll*RAD); + matrix3 w(m); + if(size > 0 && size != 1) m.scale(size); + m.translate(vec(b.s).mul(-b.grid*0.5f)); + + gle::bindvbo(p.vbo); + gle::bindebo(p.ebo); + gle::enablevertex(); + gle::enablenormal(); + prefabmesh::vertex *v = (prefabmesh::vertex *)0; + gle::vertexpointer(sizeof(prefabmesh::vertex), v->pos.v); + gle::normalpointer(sizeof(prefabmesh::vertex), v->norm.v, GL_BYTE); + + matrix4 pm; + pm.mul(camprojmatrix, m); + GLOBALPARAM(prefabmatrix, pm); + GLOBALPARAM(prefabworld, w); + SETSHADER(prefab); + gle::color(color); + glDrawRangeElements_(GL_TRIANGLES, 0, p.numverts-1, p.numtris*3, GL_UNSIGNED_SHORT, (ushort *)0); + + glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + enablepolygonoffset(GL_POLYGON_OFFSET_LINE); + + pm.mul(camprojmatrix, m); + GLOBALPARAM(prefabmatrix, pm); + SETSHADER(prefab); + gle::color(vec::hexcolor(outlinecolour)); + glDrawRangeElements_(GL_TRIANGLES, 0, p.numverts-1, p.numtris*3, GL_UNSIGNED_SHORT, (ushort *)0); + + disablepolygonoffset(GL_POLYGON_OFFSET_LINE); + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + + gle::disablevertex(); + gle::disablenormal(); + gle::clearebo(); + gle::clearvbo(); +} + +void renderprefab(const char *name, const vec &o, float yaw, float pitch, float roll, float size, const vec &color) +{ + prefab *p = loadprefab(name, false); + if(p) renderprefab(*p, o, yaw, pitch, roll, size, color); +} + +void previewprefab(const char *name, const vec &color) +{ + prefab *p = loadprefab(name, false); + if(p) + { + block3 &b = *p->copy; + float yaw; + vec o = calcmodelpreviewpos(vec(b.s).mul(b.grid*0.5f), yaw); + renderprefab(*p, o, yaw, 0, 0, 1, color); + } +} + +void mpcopy(editinfo *&e, selinfo &sel, bool local) +{ + if(local) game::edittrigger(sel, EDIT_COPY); + if(e==NULL) e = editinfos.add(new editinfo); + if(e->copy) freeblock(e->copy); + e->copy = NULL; + protectsel(e->copy = blockcopy(block3(sel), sel.grid)); + changed(sel); +} + +void mppaste(editinfo *&e, selinfo &sel, bool local) +{ + if(e==NULL) return; + if(local) game::edittrigger(sel, EDIT_PASTE); + if(e->copy) pasteblock(*e->copy, sel, local); +} + +void copy() +{ + if(noedit(true)) return; + mpcopy(localedit, sel, true); +} + +void pastehilite() +{ + if(!localedit) return; + sel.s = localedit->copy->s; + reorient(); + havesel = true; +} + +void paste() +{ + if(noedit(true)) return; + mppaste(localedit, sel, true); +} + +COMMAND(copy, ""); +COMMAND(pastehilite, ""); +COMMAND(paste, ""); +COMMANDN(undo, editundo, ""); +COMMANDN(redo, editredo, ""); + +static vector editingvslots; +struct vslotref +{ + vslotref(int &index) { editingvslots.add(&index); } + ~vslotref() { editingvslots.pop(); } +}; +#define editingvslot(...) vslotref vslotrefs[] = { __VA_ARGS__ }; (void)vslotrefs; + +void compacteditvslots() +{ + loopv(editingvslots) if(*editingvslots[i]) compactvslot(*editingvslots[i]); + loopv(unpackingvslots) compactvslot(*unpackingvslots[i].vslot); + loopv(editinfos) + { + editinfo *e = editinfos[i]; + compactvslots(e->copy->c(), e->copy->size()); + } + for(undoblock *u = undos.first; u; u = u->next) + if(!u->numents) + compactvslots(u->block()->c(), u->block()->size()); + for(undoblock *u = redos.first; u; u = u->next) + if(!u->numents) + compactvslots(u->block()->c(), u->block()->size()); +} + +///////////// height maps //////////////// + +#define MAXBRUSH 64 +#define MAXBRUSHC 63 +#define MAXBRUSH2 32 +int brush[MAXBRUSH][MAXBRUSH]; +VAR(brushx, 0, MAXBRUSH2, MAXBRUSH); +VAR(brushy, 0, MAXBRUSH2, MAXBRUSH); +bool paintbrush = 0; +int brushmaxx = 0, brushminx = MAXBRUSH; +int brushmaxy = 0, brushminy = MAXBRUSH; + +void clearbrush() +{ + memset(brush, 0, sizeof brush); + brushmaxx = brushmaxy = 0; + brushminx = brushminy = MAXBRUSH; + paintbrush = false; +} + +void brushvert(int *x, int *y, int *v) +{ + *x += MAXBRUSH2 - brushx + 1; // +1 for automatic padding + *y += MAXBRUSH2 - brushy + 1; + if(*x<0 || *y<0 || *x>=MAXBRUSH || *y>=MAXBRUSH) return; + brush[*x][*y] = clamp(*v, 0, 8); + paintbrush = paintbrush || (brush[*x][*y] > 0); + brushmaxx = min(MAXBRUSH-1, max(brushmaxx, *x+1)); + brushmaxy = min(MAXBRUSH-1, max(brushmaxy, *y+1)); + brushminx = max(0, min(brushminx, *x-1)); + brushminy = max(0, min(brushminy, *y-1)); +} + +vector htextures; + +COMMAND(clearbrush, ""); +COMMAND(brushvert, "iii"); +void hmapcancel() { htextures.setsize(0); } +COMMAND(hmapcancel, ""); +ICOMMAND(hmapselect, "", (), + int t = lookupcube(cur).texture[orient]; + int i = htextures.find(t); + if(i<0) + htextures.add(t); + else + htextures.remove(i); +); + +inline bool isheightmap(int o, int d, bool empty, cube *c) +{ + return havesel || + (empty && isempty(*c)) || + htextures.empty() || + htextures.find(c->texture[o]) >= 0; +} + +namespace hmap +{ +# define PAINTED 1 +# define NOTHMAP 2 +# define MAPPED 16 + uchar flags[MAXBRUSH][MAXBRUSH]; + cube *cmap[MAXBRUSHC][MAXBRUSHC][4]; + int mapz[MAXBRUSHC][MAXBRUSHC]; + int map [MAXBRUSH][MAXBRUSH]; + + selinfo changes; + bool selecting; + int d, dc, dr, dcr, biasup, br, hws, fg; + int gx, gy, gz, mx, my, mz, nx, ny, nz, bmx, bmy, bnx, bny; + uint fs; + selinfo hundo; + + cube *getcube(ivec t, int f) + { + t[d] += dcr*f*gridsize; + if(t[d] > nz || t[d] < mz) return NULL; + cube *c = &lookupcube(t, gridsize); + if(c->children) forcemip(*c, false); + discardchildren(*c, true); + if(!isheightmap(sel.orient, d, true, c)) return NULL; + if (t.x < changes.o.x) changes.o.x = t.x; + else if(t.x > changes.s.x) changes.s.x = t.x; + if (t.y < changes.o.y) changes.o.y = t.y; + else if(t.y > changes.s.y) changes.s.y = t.y; + if (t.z < changes.o.z) changes.o.z = t.z; + else if(t.z > changes.s.z) changes.s.z = t.z; + return c; + } + + uint getface(cube *c, int d) + { + return 0x0f0f0f0f & ((dc ? c->faces[d] : 0x88888888 - c->faces[d]) >> fs); + } + + void pushside(cube &c, int d, int x, int y, int z) + { + ivec a; + getcubevector(c, d, x, y, z, a); + a[R[d]] = 8 - a[R[d]]; + setcubevector(c, d, x, y, z, a); + } + + void addpoint(int x, int y, int z, int v) + { + if(!(flags[x][y] & MAPPED)) + map[x][y] = v + (z*8); + flags[x][y] |= MAPPED; + } + + void select(int x, int y, int z) + { + if((NOTHMAP & flags[x][y]) || (PAINTED & flags[x][y])) return; + ivec t(d, x+gx, y+gy, dc ? z : hws-z); + t.shl(gridpower); + + // selections may damage; must makeundo before + hundo.o = t; + hundo.o[D[d]] -= dcr*gridsize*2; + makeundo(hundo); + + cube **c = cmap[x][y]; + loopk(4) c[k] = NULL; + c[1] = getcube(t, 0); + if(!c[1] || !isempty(*c[1])) + { // try up + c[2] = c[1]; + c[1] = getcube(t, 1); + if(!c[1] || isempty(*c[1])) { c[0] = c[1]; c[1] = c[2]; c[2] = NULL; } + else { z++; t[d]+=fg; } + } + else // drop down + { + z--; + t[d]-= fg; + c[0] = c[1]; + c[1] = getcube(t, 0); + } + + if(!c[1] || isempty(*c[1])) { flags[x][y] |= NOTHMAP; return; } + + flags[x][y] |= PAINTED; + mapz [x][y] = z; + + if(!c[0]) c[0] = getcube(t, 1); + if(!c[2]) c[2] = getcube(t, -1); + c[3] = getcube(t, -2); + c[2] = !c[2] || isempty(*c[2]) ? NULL : c[2]; + c[3] = !c[3] || isempty(*c[3]) ? NULL : c[3]; + + uint face = getface(c[1], d); + if(face == 0x08080808 && (!c[0] || !isempty(*c[0]))) { flags[x][y] |= NOTHMAP; return; } + if(c[1]->faces[R[d]] == F_SOLID) // was single + face += 0x08080808; + else // was pair + face += c[2] ? getface(c[2], d) : 0x08080808; + face += 0x08080808; // c[3] + uchar *f = (uchar*)&face; + addpoint(x, y, z, f[0]); + addpoint(x+1, y, z, f[1]); + addpoint(x, y+1, z, f[2]); + addpoint(x+1, y+1, z, f[3]); + + if(selecting) // continue to adjacent cubes + { + if(x>bmx) select(x-1, y, z); + if(xbmy) select(x, y-1, z); + if(y, <, 1, 0, -); + else + pullhmap(worldsize*8, <, >, 0, 8, +); + + cube **c = cmap[x][y]; + int e[2][2]; + int notempty = 0; + + loopk(4) if(c[k]) { + loopi(2) loopj(2) { + e[i][j] = min(8, map[x+i][y+j] - (mapz[x][y]+3-k)*8); + notempty |= e[i][j] > 0; + } + if(notempty) + { + c[k]->texture[sel.orient] = c[1]->texture[sel.orient]; + solidfaces(*c[k]); + loopi(2) loopj(2) + { + int f = e[i][j]; + if(f<0 || (f==0 && e[1-i][j]==0 && e[i][1-j]==0)) + { + f=0; + pushside(*c[k], d, i, j, 0); + pushside(*c[k], d, i, j, 1); + } + edgeset(cubeedge(*c[k], d, i, j), dc, dc ? f : 8-f); + } + } + else + emptyfaces(*c[k]); + } + + if(!changed) return; + if(x>mx) ripple(x-1, y, mapz[x][y], true); + if(xmy) ripple(x, y-1, mapz[x][y], true); + if(ymx && y>my)); // do diagonals because adjacents + DIAGONAL_RIPPLE(-1, +1, (x>mx && ymy)); + } + +#define loopbrush(i) for(int x=bmx; x<=bnx+i; x++) for(int y=bmy; y<=bny+i; y++) + + void paint() + { + loopbrush(1) + map[x][y] -= dr * brush[x][y]; + } + + void smooth() + { + int sum, div; + loopbrush(-2) + { + sum = 0; + div = 9; + loopi(3) loopj(3) + if(flags[x+i][y+j] & MAPPED) + sum += map[x+i][y+j]; + else div--; + if(div) + map[x+1][y+1] = sum / div; + } + } + + void rippleandset() + { + loopbrush(0) + ripple(x, y, gz, false); + } + + void run(int dir, int mode) + { + d = dimension(sel.orient); + dc = dimcoord(sel.orient); + dcr= dc ? 1 : -1; + dr = dir>0 ? 1 : -1; + br = dir>0 ? 0x08080808 : 0; + // biasup = mode == dir<0; + biasup = dir<0; + bool paintme = paintbrush; + int cx = (sel.corner&1 ? 0 : -1); + int cy = (sel.corner&2 ? 0 : -1); + hws= (worldsize>>gridpower); + gx = (cur[R[d]] >> gridpower) + cx - MAXBRUSH2; + gy = (cur[C[d]] >> gridpower) + cy - MAXBRUSH2; + gz = (cur[D[d]] >> gridpower); + fs = dc ? 4 : 0; + fg = dc ? gridsize : -gridsize; + mx = max(0, -gx); // ripple range + my = max(0, -gy); + nx = min(MAXBRUSH-1, hws-gx) - 1; + ny = min(MAXBRUSH-1, hws-gy) - 1; + if(havesel) + { // selection range + bmx = mx = max(mx, (sel.o[R[d]]>>gridpower)-gx); + bmy = my = max(my, (sel.o[C[d]]>>gridpower)-gy); + bnx = nx = min(nx, (sel.s[R[d]]+(sel.o[R[d]]>>gridpower))-gx-1); + bny = ny = min(ny, (sel.s[C[d]]+(sel.o[C[d]]>>gridpower))-gy-1); + } + if(havesel && mode<0) // -ve means smooth selection + paintme = false; + else + { // brush range + bmx = max(mx, brushminx); + bmy = max(my, brushminy); + bnx = min(nx, brushmaxx-1); + bny = min(ny, brushmaxy-1); + } + nz = worldsize-gridsize; + mz = 0; + hundo.s = ivec(d,1,1,5); + hundo.orient = sel.orient; + hundo.grid = gridsize; + forcenextundo(); + + changes.grid = gridsize; + changes.s = changes.o = cur; + memset(map, 0, sizeof map); + memset(flags, 0, sizeof flags); + + selecting = true; + select(clamp(MAXBRUSH2-cx, bmx, bnx), + clamp(MAXBRUSH2-cy, bmy, bny), + dc ? gz : hws - gz); + selecting = false; + if(paintme) + paint(); + else + smooth(); + rippleandset(); // pull up points to cubify, and set + changes.s.sub(changes.o).shr(gridpower).add(1); + changed(changes); + } +} + +void edithmap(int dir, int mode) { + if((nompedit && multiplayer()) || !hmapsel) return; + hmap::run(dir, mode); +} + +///////////// main cube edit //////////////// + +int bounded(int n) { return n<0 ? 0 : (n>8 ? 8 : n); } + +void pushedge(uchar &edge, int dir, int dc) +{ + int ne = bounded(edgeget(edge, dc)+dir); + edgeset(edge, dc, ne); + int oe = edgeget(edge, 1-dc); + if((dir<0 && dc && oe>ne) || (dir>0 && dc==0 && oe0) == dc && h<=0) || ((dir<0) == dc && h>=worldsize)) return; + if(dir<0) sel.o[d] += sel.grid * seldir; + } + + if(dc) sel.o[d] += sel.us(d)-sel.grid; + sel.s[d] = 1; + + loopselxyz( + if(c.children) solidfaces(c); + ushort mat = getmaterial(c); + discardchildren(c, true); + c.material = mat; + if(mode==1) // fill command + { + if(dir<0) + { + solidfaces(c); + cube &o = blockcube(x, y, 1, sel, -sel.grid); + loopi(6) + c.texture[i] = o.children ? DEFAULT_GEOM : o.texture[i]; + } + else + emptyfaces(c); + } + else + { + uint bak = c.faces[d]; + uchar *p = (uchar *)&c.faces[d]; + + if(mode==2) + linkedpush(c, d, sel.corner&1, sel.corner>>1, dc, seldir); // corner command + else + { + loop(mx,2) loop(my,2) // pull/push edges command + { + if(x==0 && mx==0 && sel.cx) continue; + if(y==0 && my==0 && sel.cy) continue; + if(x==sel.s[R[d]]-1 && mx==1 && (sel.cx+sel.cxs)&1) continue; + if(y==sel.s[C[d]]-1 && my==1 && (sel.cy+sel.cys)&1) continue; + if(p[mx+my*2] != ((uchar *)&bak)[mx+my*2]) continue; + + linkedpush(c, d, mx, my, dc, seldir); + } + } + + optiface(p, c); + if(invalidcubeguard==1 && !isvalidcube(c)) + { + uint newbak = c.faces[d]; + uchar *m = (uchar *)&bak; + uchar *n = (uchar *)&newbak; + loopk(4) if(n[k] != m[k]) // tries to find partial edit that is valid + { + c.faces[d] = bak; + c.edges[d*4+k] = n[k]; + if(isvalidcube(c)) + m[k] = n[k]; + } + c.faces[d] = bak; + } + } + ); + if (mode==1 && dir>0) + sel.o[d] += sel.grid * seldir; +} + +void editface(int *dir, int *mode) +{ + if(noedit(moving!=0)) return; + if(hmapedit!=1) + mpeditface(*dir, *mode, sel, true); + else + edithmap(*dir, *mode); +} + +VAR(selectionsurf, 0, 0, 1); + +void pushsel(int *dir) +{ + if(noedit(moving!=0)) return; + int d = dimension(orient); + int s = dimcoord(orient) ? -*dir : *dir; + sel.o[d] += s*sel.grid; + if(selectionsurf==1) + { + player->o[d] += s*sel.grid; + player->resetinterp(); + } +} + +void mpdelcube(selinfo &sel, bool local) +{ + if(local) game::edittrigger(sel, EDIT_DELCUBE); + loopselxyz(discardchildren(c, true); emptyfaces(c)); +} + +void delcube() +{ + if(noedit(true)) return; + mpdelcube(sel, true); +} + +COMMAND(pushsel, "i"); +COMMAND(editface, "ii"); +COMMAND(delcube, ""); + +/////////// texture editing ////////////////// + +int curtexindex = -1, lasttex = 0, lasttexmillis = -1; +int texpaneltimer = 0; +vector texmru; + +void tofronttex() // maintain most recently used of the texture lists when applying texture +{ + int c = curtexindex; + if(texmru.inrange(c)) + { + texmru.insert(0, texmru.remove(c)); + curtexindex = -1; + } +} + +selinfo repsel; +int reptex = -1; + +static vector remappedvslots; + +VAR(usevdelta, 1, 0, 0); + +static VSlot *remapvslot(int index, bool delta, const VSlot &ds) +{ + loopv(remappedvslots) if(remappedvslots[i].index == index) return remappedvslots[i].vslot; + VSlot &vs = lookupvslot(index, false); + if(vs.index < 0 || vs.index == DEFAULT_SKY) return NULL; + VSlot *edit = NULL; + if(delta) + { + VSlot ms; + mergevslot(ms, vs, ds); + edit = ms.changed ? editvslot(vs, ms) : vs.slot->variants; + } + else edit = ds.changed ? editvslot(vs, ds) : vs.slot->variants; + if(!edit) edit = &vs; + remappedvslots.add(vslotmap(vs.index, edit)); + return edit; +} + +static void remapvslots(cube &c, bool delta, const VSlot &ds, int orient, bool &findrep, VSlot *&findedit) +{ + if(c.children) + { + loopi(8) remapvslots(c.children[i], delta, ds, orient, findrep, findedit); + return; + } + static VSlot ms; + if(orient<0) loopi(6) + { + VSlot *edit = remapvslot(c.texture[i], delta, ds); + if(edit) + { + c.texture[i] = edit->index; + if(!findedit) findedit = edit; + } + } + else + { + int i = visibleorient(c, orient); + VSlot *edit = remapvslot(c.texture[i], delta, ds); + if(edit) + { + if(findrep) + { + if(reptex < 0) reptex = c.texture[i]; + else if(reptex != c.texture[i]) findrep = false; + } + c.texture[i] = edit->index; + if(!findedit) findedit = edit; + } + } +} + +void edittexcube(cube &c, int tex, int orient, bool &findrep) +{ + if(orient<0) loopi(6) c.texture[i] = tex; + else + { + int i = visibleorient(c, orient); + if(findrep) + { + if(reptex < 0) reptex = c.texture[i]; + else if(reptex != c.texture[i]) findrep = false; + } + c.texture[i] = tex; + } + if(c.children) loopi(8) edittexcube(c.children[i], tex, orient, findrep); +} + +VAR(allfaces, 0, 0, 1); + +void mpeditvslot(int delta, VSlot &ds, int allfaces, selinfo &sel, bool local) +{ + if(local) + { + game::edittrigger(sel, EDIT_VSLOT, delta, allfaces, 0, &ds); + if(!(lastsel==sel)) tofronttex(); + if(allfaces || !(repsel == sel)) reptex = -1; + repsel = sel; + } + bool findrep = local && !allfaces && reptex < 0; + VSlot *findedit = NULL; + loopselxyz(remapvslots(c, delta != 0, ds, allfaces ? -1 : sel.orient, findrep, findedit)); + remappedvslots.setsize(0); + if(local && findedit) + { + lasttex = findedit->index; + lasttexmillis = totalmillis; + curtexindex = texmru.find(lasttex); + if(curtexindex < 0) + { + curtexindex = texmru.length(); + texmru.add(lasttex); + } + } +} + +bool mpeditvslot(int delta, int allfaces, selinfo &sel, ucharbuf &buf) +{ + VSlot ds; + if(!unpackvslot(buf, ds, delta != 0)) return false; + editingvslot(ds.layer); + mpeditvslot(delta, ds, allfaces, sel, false); + return true; +} + +void vdelta(char *body) +{ + if(noedit()) return; + usevdelta++; + execute(body); + usevdelta--; +} +COMMAND(vdelta, "s"); + +void vrotate(int *n) +{ + if(noedit()) return; + VSlot ds; + ds.changed = 1<changed && nompedit && multiplayer()) return; + } + editingvslot(ds.layer); + mpeditvslot(usevdelta, ds, allfaces, sel, true); +} +COMMAND(vlayer, "i"); +ICOMMAND(getvlayer, "i", (int *tex), intret(lookupvslot(*tex, false).layer)); + +void valpha(float *front, float *back) +{ + if(noedit()) return; + VSlot ds; + ds.changed = 1< str; + loopv(vslot.params) + { + SlotShaderParam &p = vslot.params[i]; + if(i) str.put(' '); + str.put(p.name, strlen(p.name)); + } + str.add('\0'); + stringret(newstring(str.getbuf(), str.length()-1)); +}); + +void mpedittex(int tex, int allfaces, selinfo &sel, bool local) +{ + if(local) + { + game::edittrigger(sel, EDIT_TEX, tex, allfaces); + if(allfaces || !(repsel == sel)) reptex = -1; + repsel = sel; + } + bool findrep = local && !allfaces && reptex < 0; + loopselxyz(edittexcube(c, tex, allfaces ? -1 : sel.orient, findrep)); +} + +static int unpacktex(int &tex, ucharbuf &buf, bool insert = true) +{ + if(tex < 0x10000) return true; + VSlot ds; + if(!unpackvslot(buf, ds, false)) return false; + VSlot &vs = *lookupslot(tex & 0xFFFF, false).variants; + if(vs.index < 0 || vs.index == DEFAULT_SKY) return false; + VSlot *edit = insert ? editvslot(vs, ds) : findvslot(*vs.slot, vs, ds); + if(!edit) return false; + tex = edit->index; + return true; +} + +int shouldpacktex(int index) +{ + if(vslots.inrange(index)) + { + VSlot &vs = *vslots[index]; + if(vs.changed) return 0x10000 + vs.slot->index; + } + return 0; +} + + +bool mpedittex(int tex, int allfaces, selinfo &sel, ucharbuf &buf) +{ + if(!unpacktex(tex, buf)) return false; + mpedittex(tex, allfaces, sel, false); + return true; +} + +void filltexlist() +{ + if(texmru.length()!=vslots.length()) + { + loopvrev(texmru) if(texmru[i]>=vslots.length()) + { + if(curtexindex > i) curtexindex--; + else if(curtexindex == i) curtexindex = -1; + texmru.remove(i); + } + loopv(vslots) if(texmru.find(i)<0) texmru.add(i); + } +} + +void compactmruvslots() +{ + remappedvslots.setsize(0); + loopvrev(texmru) + { + if(vslots.inrange(texmru[i])) + { + VSlot &vs = *vslots[texmru[i]]; + if(vs.index >= 0) + { + texmru[i] = vs.index; + continue; + } + } + if(curtexindex > i) curtexindex--; + else if(curtexindex == i) curtexindex = -1; + texmru.remove(i); + } + if(vslots.inrange(lasttex)) + { + VSlot &vs = *vslots[lasttex]; + lasttex = vs.index >= 0 ? vs.index : 0; + } + else lasttex = 0; + reptex = vslots.inrange(reptex) ? vslots[reptex]->index : -1; +} + +void edittex(int i, bool save = true) +{ + lasttex = i; + lasttexmillis = totalmillis; + if(save) + { + loopvj(texmru) if(texmru[j]==lasttex) { curtexindex = j; break; } + } + mpedittex(i, allfaces, sel, true); +} + +void edittex_(int *dir) +{ + if(noedit()) return; + filltexlist(); + if(texmru.empty()) return; + texpaneltimer = 5000; + if(!(lastsel==sel)) tofronttex(); + curtexindex = clamp(curtexindex<0 ? 0 : curtexindex+*dir, 0, texmru.length()-1); + edittex(texmru[curtexindex], false); +} + +void gettex() +{ + if(noedit(true)) return; + filltexlist(); + int tex = -1; + loopxyz(sel, sel.grid, tex = c.texture[sel.orient]); + loopv(texmru) if(texmru[i]==tex) + { + curtexindex = i; + tofronttex(); + return; + } +} + +void getcurtex() +{ + if(noedit(true)) return; + filltexlist(); + int index = curtexindex < 0 ? 0 : curtexindex; + if(!texmru.inrange(index)) return; + intret(texmru[index]); +} + +void getseltex() +{ + if(noedit(true)) return; + cube &c = lookupcube(sel.o, -sel.grid); + if(c.children || isempty(c)) return; + intret(c.texture[sel.orient]); +} + +void gettexname(int *tex, int *subslot) +{ + if(noedit(true) || *tex<0) return; + VSlot &vslot = lookupvslot(*tex, false); + Slot &slot = *vslot.slot; + if(!slot.sts.inrange(*subslot)) return; + result(slot.sts[*subslot].name); +} + +void getslottex(int *idx) +{ + if(*idx < 0 || !slots.inrange(*idx)) { intret(-1); return; } + Slot &slot = lookupslot(*idx, false); + intret(slot.variants->index); +} + +COMMANDN(edittex, edittex_, "i"); +COMMAND(gettex, ""); +COMMAND(getcurtex, ""); +COMMAND(getseltex, ""); +ICOMMAND(getreptex, "", (), { if(!noedit()) intret(vslots.inrange(reptex) ? reptex : -1); }); +COMMAND(gettexname, "ii"); +ICOMMAND(numvslots, "", (), intret(vslots.length())); +ICOMMAND(numslots, "", (), intret(slots.length())); +COMMAND(getslottex, "i"); +ICOMMAND(texloaded, "i", (int *tex), intret(slots.inrange(*tex) && slots[*tex]->loaded ? 1 : 0)); + +void replacetexcube(cube &c, int oldtex, int newtex) +{ + loopi(6) if(c.texture[i] == oldtex) c.texture[i] = newtex; + if(c.children) loopi(8) replacetexcube(c.children[i], oldtex, newtex); +} + +void mpreplacetex(int oldtex, int newtex, bool insel, selinfo &sel, bool local) +{ + if(local) game::edittrigger(sel, EDIT_REPLACE, oldtex, newtex, insel ? 1 : 0); + if(insel) + { + loopselxyz(replacetexcube(c, oldtex, newtex)); + } + else + { + loopi(8) replacetexcube(worldroot[i], oldtex, newtex); + } + allchanged(); +} + +bool mpreplacetex(int oldtex, int newtex, bool insel, selinfo &sel, ucharbuf &buf) +{ + if(!unpacktex(oldtex, buf, false)) return false; + editingvslot(oldtex); + if(!unpacktex(newtex, buf)) return false; + mpreplacetex(oldtex, newtex, insel, sel, false); + return true; +} + +void replace(bool insel) +{ + if(noedit()) return; + if(reptex < 0) { conoutf(CON_ERROR, "can only replace after a texture edit"); return; } + mpreplacetex(reptex, lasttex, insel, sel, true); +} + +ICOMMAND(replace, "", (), replace(false)); +ICOMMAND(replacesel, "", (), replace(true)); + +////////// flip and rotate /////////////// +uint dflip(uint face) { return face==F_EMPTY ? face : 0x88888888 - (((face&0xF0F0F0F0)>>4) | ((face&0x0F0F0F0F)<<4)); } +uint cflip(uint face) { return ((face&0xFF00FF00)>>8) | ((face&0x00FF00FF)<<8); } +uint rflip(uint face) { return ((face&0xFFFF0000)>>16)| ((face&0x0000FFFF)<<16); } +uint mflip(uint face) { return (face&0xFF0000FF) | ((face&0x00FF0000)>>8) | ((face&0x0000FF00)<<8); } + +void flipcube(cube &c, int d) +{ + swap(c.texture[d*2], c.texture[d*2+1]); + c.faces[D[d]] = dflip(c.faces[D[d]]); + c.faces[C[d]] = cflip(c.faces[C[d]]); + c.faces[R[d]] = rflip(c.faces[R[d]]); + if(c.children) + { + loopi(8) if(i&octadim(d)) swap(c.children[i], c.children[i-octadim(d)]); + loopi(8) flipcube(c.children[i], d); + } +} + +void rotatequad(cube &a, cube &b, cube &c, cube &d) +{ + cube t = a; a = b; b = c; c = d; d = t; +} + +void rotatecube(cube &c, int d) // rotates cube clockwise. see pics in cvs for help. +{ + c.faces[D[d]] = cflip (mflip(c.faces[D[d]])); + c.faces[C[d]] = dflip (mflip(c.faces[C[d]])); + c.faces[R[d]] = rflip (mflip(c.faces[R[d]])); + swap(c.faces[R[d]], c.faces[C[d]]); + + swap(c.texture[2*R[d]], c.texture[2*C[d]+1]); + swap(c.texture[2*C[d]], c.texture[2*R[d]+1]); + swap(c.texture[2*C[d]], c.texture[2*C[d]+1]); + + if(c.children) + { + int row = octadim(R[d]); + int col = octadim(C[d]); + for(int i=0; i<=octadim(d); i+=octadim(d)) rotatequad + ( + c.children[i+row], + c.children[i], + c.children[i+col], + c.children[i+col+row] + ); + loopi(8) rotatecube(c.children[i], d); + } +} + +void mpflip(selinfo &sel, bool local) +{ + if(local) + { + game::edittrigger(sel, EDIT_FLIP); + makeundo(); + } + int zs = sel.s[dimension(sel.orient)]; + loopxy(sel) + { + loop(z,zs) flipcube(selcube(x, y, z), dimension(sel.orient)); + loop(z,zs/2) + { + cube &a = selcube(x, y, z); + cube &b = selcube(x, y, zs-z-1); + swap(a, b); + } + } + changed(sel); +} + +void flip() +{ + if(noedit()) return; + mpflip(sel, true); +} + +void mprotate(int cw, selinfo &sel, bool local) +{ + if(local) game::edittrigger(sel, EDIT_ROTATE, cw); + int d = dimension(sel.orient); + if(!dimcoord(sel.orient)) cw = -cw; + int m = sel.s[C[d]] < sel.s[R[d]] ? C[d] : R[d]; + int ss = sel.s[m] = max(sel.s[R[d]], sel.s[C[d]]); + if(local) makeundo(); + loop(z,sel.s[D[d]]) loopi(cw>0 ? 1 : 3) + { + loopxy(sel) rotatecube(selcube(x,y,z), d); + loop(y,ss/2) loop(x,ss-1-y*2) rotatequad + ( + selcube(ss-1-y, x+y, z), + selcube(x+y, y, z), + selcube(y, ss-1-x-y, z), + selcube(ss-1-x-y, ss-1-y, z) + ); + } + changed(sel); +} + +void rotate(int *cw) +{ + if(noedit()) return; + mprotate(*cw, sel, true); +} + +COMMAND(flip, ""); +COMMAND(rotate, "i"); + +enum { EDITMATF_EMPTY = 0x10000, EDITMATF_NOTEMPTY = 0x20000, EDITMATF_SOLID = 0x30000, EDITMATF_NOTSOLID = 0x40000 }; +static const struct { const char *name; int filter; } editmatfilters[] = +{ + { "empty", EDITMATF_EMPTY }, + { "notempty", EDITMATF_NOTEMPTY }, + { "solid", EDITMATF_SOLID }, + { "notsolid", EDITMATF_NOTSOLID } +}; + +void setmat(cube &c, ushort mat, ushort matmask, ushort filtermat, ushort filtermask, int filtergeom) +{ + if(c.children) + loopi(8) setmat(c.children[i], mat, matmask, filtermat, filtermask, filtergeom); + else if((c.material&filtermask) == filtermat) + { + switch(filtergeom) + { + case EDITMATF_EMPTY: if(isempty(c)) break; return; + case EDITMATF_NOTEMPTY: if(!isempty(c)) break; return; + case EDITMATF_SOLID: if(isentirelysolid(c)) break; return; + case EDITMATF_NOTSOLID: if(!isentirelysolid(c)) break; return; + } + if(mat!=MAT_AIR) + { + c.material &= matmask; + c.material |= mat; + } + else c.material = MAT_AIR; + } +} + +void mpeditmat(int matid, int filter, selinfo &sel, bool local) +{ + if(local) game::edittrigger(sel, EDIT_MAT, matid, filter); + + ushort filtermat = 0, filtermask = 0, matmask; + int filtergeom = 0; + if(filter >= 0) + { + filtermat = filter&0xFFFF; + filtermask = filtermat&(MATF_VOLUME|MATF_INDEX) ? MATF_VOLUME|MATF_INDEX : (filtermat&MATF_CLIP ? MATF_CLIP : filtermat); + filtergeom = filter&~0xFFFF; + } + if(matid < 0) + { + matid = 0; + matmask = filtermask; + if(isclipped(filtermat&MATF_VOLUME)) matmask &= ~MATF_CLIP; + if(isdeadly(filtermat&MATF_VOLUME)) matmask &= ~MAT_DEATH; + } + else + { + matmask = matid&(MATF_VOLUME|MATF_INDEX) ? 0 : (matid&MATF_CLIP ? ~MATF_CLIP : ~matid); + if(isclipped(matid&MATF_VOLUME)) matid |= MAT_CLIP; + if(isdeadly(matid&MATF_VOLUME)) matid |= MAT_DEATH; + } + loopselxyz(setmat(c, matid, matmask, filtermat, filtermask, filtergeom)); +} + +void editmat(char *name, char *filtername) +{ + if(noedit()) return; + int filter = -1; + if(filtername[0]) + { + loopi(sizeof(editmatfilters)/sizeof(editmatfilters[0])) if(!strcmp(editmatfilters[i].name, filtername)) { filter = editmatfilters[i].filter; break; } + if(filter < 0) filter = findmaterial(filtername); + if(filter < 0) + { + conoutf(CON_ERROR, "unknown material \"%s\"", filtername); + return; + } + } + int id = -1; + if(name[0] || filter < 0) + { + id = findmaterial(name); + if(id<0) { conoutf(CON_ERROR, "unknown material \"%s\"", name); return; } + } + mpeditmat(id, filter, sel, true); +} + +COMMAND(editmat, "ss"); + +extern int menudistance, menuautoclose; + +VARP(texguiwidth, 1, 15, 1000); +VARP(texguiheight, 1, 8, 1000); +FVARP(texguiscale, 0.1f, 1.5f, 10.0f); +VARP(texguitime, 0, 15, 1000); +VARP(texguiname, 0, 1, 1); + +static int lastthumbnail = 0; + +VARP(texgui2d, 0, 1, 1); +VAR(texguinum, 1, -1, 0); + +struct texturegui : g3d_callback +{ + bool menuon; + vec menupos; + int menustart, menutab; + + texturegui() : menustart(-1) {} + + void gui(g3d_gui &g, bool firstpass) + { + int origtab = menutab, numtabs = max((slots.length() + texguiwidth*texguiheight - 1)/(texguiwidth*texguiheight), 1); + if(!firstpass) texguinum = -1; + g.start(menustart, 0.04f, &menutab); + bool oldautotab = g.allowautotab(false); + loopi(numtabs) + { + g.tab(!i ? "Textures" : NULL, 0xFFDD88); + if(i+1 != origtab) continue; //don't load textures on non-visible tabs! + Slot *rollover = NULL; + loop(h, texguiheight) + { + g.pushlist(); + loop(w, texguiwidth) + { + extern VSlot dummyvslot; + int ti = (i*texguiheight+h)*texguiwidth+w; + if(tiset(); + } + } + else + { + g.texture(dummyvslot, texguiscale, false); //create an empty space + } + } + g.poplist(); + } + if(texguiname) + { + if(rollover) + { + defformatstring(name, "%d \f7:\fc %s", texguinum, rollover->sts[0].name); + g.title(name, 0xFFDD88); + } + else g.space(1); + } + } + g.allowautotab(oldautotab); + g.end(); + } + + void showtextures(bool on) + { + if(on == menuon) return; + if((menuon = on)) + { + if(menustart <= lasttexmillis) + menutab = 1+clamp(lookupvslot(lasttex, false).slot->index, 0, slots.length()-1)/(texguiwidth*texguiheight); + menupos = menuinfrontofplayer(); + menustart = starttime(); + } + else texguinum = -1; + } + + void show() + { + if(!menuon) return; + filltexlist(); + extern int usegui2d; + if(!editmode || ((!texgui2d || !usegui2d) && camera1->o.dist(menupos) > menuautoclose)) { menuon = false; texguinum = -1; } + else g3d_addgui(this, menupos, texgui2d ? GUI_2D : 0); + } +} gui; + +void g3d_texturemenu() +{ + gui.show(); +} + +void showtexgui(int *n) +{ + if(!editmode) { conoutf(CON_ERROR, "operation only allowed in edit mode"); return; } + gui.showtextures(*n==0 ? !gui.menuon : *n==1); +} + +// 0/noargs = toggle, 1 = on, other = off - will autoclose if too far away or exit editmode +COMMAND(showtexgui, "i"); + +bool cleartexgui() +{ + if(!gui.menuon) return false; + gui.showtextures(false); + return true; +} +ICOMMAND(cleartexgui, "", (), intret(cleartexgui() ? 1 : 0)); + +void rendertexturepanel(int w, int h) +{ + if((texpaneltimer -= curtime)>0 && editmode) + { + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + pushhudmatrix(); + hudmatrix.scale(h/1800.0f, h/1800.0f, 1); + flushhudmatrix(false); + SETSHADER(hudrgb); + + int y = 50, gap = 10; + + gle::defvertex(2); + gle::deftexcoord0(); + + loopi(7) + { + int s = (i == 3 ? 285 : 220), ti = curtexindex+i-3; + if(texmru.inrange(ti)) + { + VSlot &vslot = lookupvslot(texmru[ti]), *layer = NULL; + Slot &slot = *vslot.slot; + Texture *tex = slot.sts.empty() ? notexture : slot.sts[0].t, *glowtex = NULL, *layertex = NULL; + if(slot.texmask&(1<slot->sts.empty() ? notexture : layer->slot->sts[0].t; + } + float sx = min(1.0f, tex->xs/(float)tex->ys), sy = min(1.0f, tex->ys/(float)tex->xs); + int x = w*1800/h-s-50, r = s; + vec2 tc[4] = { vec2(0, 0), vec2(1, 0), vec2(1, 1), vec2(0, 1) }; + float xoff = vslot.offset.x, yoff = vslot.offset.y; + if(vslot.rotation) + { + const texrotation &r = texrotations[vslot.rotation]; + if(r.swapxy) { swap(xoff, yoff); loopk(4) swap(tc[k].x, tc[k].y); } + if(r.flipx) { xoff *= -1; loopk(4) tc[k].x *= -1; } + if(r.flipy) { yoff *= -1; loopk(4) tc[k].y *= -1; } + } + loopk(4) { tc[k].x = tc[k].x/sx - xoff/tex->xs; tc[k].y = tc[k].y/sy - yoff/tex->ys; } + glBindTexture(GL_TEXTURE_2D, tex->id); + loopj(glowtex ? 3 : 2) + { + if(j < 2) gle::color(vec(vslot.colorscale).mul(j), texpaneltimer/1000.0f); + else + { + glBindTexture(GL_TEXTURE_2D, glowtex->id); + glBlendFunc(GL_SRC_ALPHA, GL_ONE); + gle::color(vslot.glowcolor, texpaneltimer/1000.0f); + } + gle::begin(GL_TRIANGLE_STRIP); + gle::attribf(x, y); gle::attrib(tc[0]); + gle::attribf(x+r, y); gle::attrib(tc[1]); + gle::attribf(x, y+r); gle::attrib(tc[3]); + gle::attribf(x+r, y+r); gle::attrib(tc[2]); + xtraverts += gle::end(); + if(j==1 && layertex) + { + gle::color(layer->colorscale, texpaneltimer/1000.0f); + glBindTexture(GL_TEXTURE_2D, layertex->id); + gle::begin(GL_TRIANGLE_STRIP); + gle::attribf(x+r/2, y+r/2); gle::attrib(tc[0]); + gle::attribf(x+r, y+r/2); gle::attrib(tc[1]); + gle::attribf(x+r/2, y+r); gle::attrib(tc[3]); + gle::attribf(x+r, y+r); gle::attrib(tc[2]); + xtraverts += gle::end(); + } + if(!j) + { + r -= 10; + x += 5; + y += 5; + } + else if(j == 2) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + } + } + y += s+gap; + } + + pophudmatrix(true, false); + hudshader->set(); + } +} diff --git a/src/engine/octarender.cpp b/src/engine/octarender.cpp new file mode 100644 index 0000000..faf609f --- /dev/null +++ b/src/engine/octarender.cpp @@ -0,0 +1,1803 @@ +// octarender.cpp: fill vertex arrays with different cube surfaces. + +#include "engine.h" + +struct vboinfo +{ + int uses; +}; + +hashtable vbos; + +VAR(printvbo, 0, 0, 1); +VARFN(vbosize, maxvbosize, 0, 1<<14, 1<<16, allchanged()); + +enum +{ + VBO_VBUF = 0, + VBO_EBUF, + VBO_SKYBUF, + NUMVBO +}; + +static vector vbodata[NUMVBO]; +static vector vbovas[NUMVBO]; +static int vbosize[NUMVBO]; + +void destroyvbo(GLuint vbo) +{ + vboinfo *exists = vbos.access(vbo); + if(!exists) return; + vboinfo &vbi = *exists; + if(vbi.uses <= 0) return; + vbi.uses--; + if(!vbi.uses) + { + glDeleteBuffers_(1, &vbo); + vbos.remove(vbo); + } +} + +void genvbo(int type, void *buf, int len, vtxarray **vas, int numva) +{ + gle::disable(); + + GLuint vbo; + glGenBuffers_(1, &vbo); + GLenum target = type==VBO_VBUF ? GL_ARRAY_BUFFER : GL_ELEMENT_ARRAY_BUFFER; + glBindBuffer_(target, vbo); + glBufferData_(target, len, buf, GL_STATIC_DRAW); + glBindBuffer_(target, 0); + + vboinfo &vbi = vbos[vbo]; + vbi.uses = numva; + + if(printvbo) conoutf(CON_DEBUG, "vbo %d: type %d, size %d, %d uses", vbo, type, len, numva); + + loopi(numva) + { + vtxarray *va = vas[i]; + switch(type) + { + case VBO_VBUF: + va->vbuf = vbo; + break; + case VBO_EBUF: + va->ebuf = vbo; + break; + case VBO_SKYBUF: + va->skybuf = vbo; + break; + } + } +} + +bool readva(vtxarray *va, ushort *&edata, vertex *&vdata) +{ + if(!va->vbuf || !va->ebuf) return false; + + edata = new ushort[3*va->tris]; + vdata = new vertex[va->verts]; + + gle::bindebo(va->ebuf); + glGetBufferSubData_(GL_ELEMENT_ARRAY_BUFFER, (size_t)va->edata, 3*va->tris*sizeof(ushort), edata); + gle::clearebo(); + + gle::bindvbo(va->vbuf); + glGetBufferSubData_(GL_ARRAY_BUFFER, va->voffset*sizeof(vertex), va->verts*sizeof(vertex), vdata); + gle::clearvbo(); + return true; +} + +void flushvbo(int type = -1) +{ + if(type < 0) + { + loopi(NUMVBO) flushvbo(i); + return; + } + + vector &data = vbodata[type]; + if(data.empty()) return; + vector &vas = vbovas[type]; + genvbo(type, data.getbuf(), data.length(), vas.getbuf(), vas.length()); + data.setsize(0); + vas.setsize(0); + vbosize[type] = 0; +} + +uchar *addvbo(vtxarray *va, int type, int numelems, int elemsize) +{ + vbosize[type] += numelems; + + vector &data = vbodata[type]; + vector &vas = vbovas[type]; + + vas.add(va); + + int len = numelems*elemsize; + uchar *buf = data.reserve(len).buf; + data.advance(len); + return buf; +} + +struct verthash +{ + static const int SIZE = 1<<13; + int table[SIZE]; + vector verts; + vector chain; + + verthash() { clearverts(); } + + void clearverts() + { + memset(table, -1, sizeof(table)); + chain.setsize(0); + verts.setsize(0); + } + + int addvert(const vertex &v) + { + uint h = hthash(v.pos)&(SIZE-1); + for(int i = table[h]; i>=0; i = chain[i]) + { + const vertex &c = verts[i]; + if(c.pos==v.pos && c.tc==v.tc && c.norm==v.norm && c.tangent==v.tangent && (v.lm.iszero() || c.lm==v.lm)) + return i; + } + if(verts.length() >= USHRT_MAX) return -1; + verts.add(v); + chain.add(table[h]); + return table[h] = verts.length()-1; + } + + int addvert(const vec &pos, const vec2 &tc = vec2(0, 0), const svec2 &lm = svec2(0, 0), const bvec &norm = bvec(128, 128, 128), const bvec4 &tangent = bvec4(128, 128, 128, 128)) + { + vertex vtx; + vtx.pos = pos; + vtx.tc = tc; + vtx.lm = lm; + vtx.norm = norm; + vtx.tangent = tangent; + return addvert(vtx); + } +}; + +enum +{ + NO_ALPHA = 0, + ALPHA_BACK, + ALPHA_FRONT +}; + +struct sortkey +{ + ushort tex, lmid, envmap; + uchar dim, layer, alpha; + + sortkey() {} + sortkey(ushort tex, ushort lmid, uchar dim, uchar layer = LAYER_TOP, ushort envmap = EMID_NONE, uchar alpha = NO_ALPHA) + : tex(tex), lmid(lmid), envmap(envmap), dim(dim), layer(layer), alpha(alpha) + {} + + bool operator==(const sortkey &o) const { return tex==o.tex && lmid==o.lmid && envmap==o.envmap && dim==o.dim && layer==o.layer && alpha==o.alpha; } +}; + +struct sortval +{ + int unlit; + vector tris[2]; + + sortval() : unlit(0) {} +}; + +static inline bool htcmp(const sortkey &x, const sortkey &y) +{ + return x == y; +} + +static inline uint hthash(const sortkey &k) +{ + return k.tex + k.lmid*9741; +} + +struct vacollect : verthash +{ + ivec origin; + int size; + hashtable indices; + vector texs; + vector grasstris; + vector matsurfs; + vector mapmodels; + vector skyindices, explicitskyindices; + vector skyfaces[6]; + int worldtris, skytris, skymask, skyclip, skyarea; + + void clear() + { + clearverts(); + worldtris = skytris = 0; + skymask = 0; + skyclip = INT_MAX; + skyarea = 0; + indices.clear(); + skyindices.setsize(0); + explicitskyindices.setsize(0); + matsurfs.setsize(0); + mapmodels.setsize(0); + grasstris.setsize(0); + texs.setsize(0); + loopi(6) skyfaces[i].setsize(0); + } + + void remapunlit(vector &remap) + { + uint lastlmid[8] = { LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT }, + firstlmid[8] = { LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT }; + int firstlit[8] = { -1, -1, -1, -1, -1, -1, -1, -1 }; + loopv(texs) + { + sortkey &k = texs[i]; + if(k.lmid>=LMID_RESERVED) + { + LightMapTexture &lmtex = lightmaptexs[k.lmid]; + int type = lmtex.type&LM_TYPE; + if(k.layer==LAYER_BLEND) type += 2; + else if(k.alpha) type += 4 + 2*(k.alpha-1); + lastlmid[type] = lmtex.unlitx>=0 ? k.lmid : LMID_AMBIENT; + if(firstlmid[type]==LMID_AMBIENT && lastlmid[type]!=LMID_AMBIENT) + { + firstlit[type] = i; + firstlmid[type] = lastlmid[type]; + } + } + else if(k.lmid==LMID_AMBIENT) + { + Shader *s = lookupvslot(k.tex, false).slot->shader; + int type = s->type&SHADER_NORMALSLMS ? LM_BUMPMAP0 : LM_DIFFUSE; + if(k.layer==LAYER_BLEND) type += 2; + else if(k.alpha) type += 4 + 2*(k.alpha-1); + if(lastlmid[type]!=LMID_AMBIENT) + { + sortval &t = indices[k]; + if(t.unlit<=0) t.unlit = lastlmid[type]; + } + } + } + loopj(2) + { + int offset = 2*j; + if(firstlmid[offset]==LMID_AMBIENT && firstlmid[offset+1]==LMID_AMBIENT) continue; + loopi(max(firstlit[offset], firstlit[offset+1])) + { + sortkey &k = texs[i]; + if((j ? k.layer!=LAYER_BLEND : k.layer==LAYER_BLEND) || k.alpha) continue; + if(k.lmid!=LMID_AMBIENT) continue; + Shader *s = lookupvslot(k.tex, false).slot->shader; + int type = offset + (s->type&SHADER_NORMALSLMS ? LM_BUMPMAP0 : LM_DIFFUSE); + if(firstlmid[type]==LMID_AMBIENT) continue; + indices[k].unlit = firstlmid[type]; + } + } + loopj(2) + { + int offset = 4 + 2*j; + if(firstlmid[offset]==LMID_AMBIENT && firstlmid[offset+1]==LMID_AMBIENT) continue; + loopi(max(firstlit[offset], firstlit[offset+1])) + { + sortkey &k = texs[i]; + if(k.alpha != j+1) continue; + if(k.lmid!=LMID_AMBIENT) continue; + Shader *s = lookupvslot(k.tex, false).slot->shader; + int type = offset + (s->type&SHADER_NORMALSLMS ? LM_BUMPMAP0 : LM_DIFFUSE); + if(firstlmid[type]==LMID_AMBIENT) continue; + indices[k].unlit = firstlmid[type]; + } + } + loopv(remap) + { + sortkey &k = remap[i]; + sortval &t = indices[k]; + if(t.unlit<=0) continue; + LightMapTexture &lm = lightmaptexs[t.unlit]; + svec2 lmtc(short(ceil((lm.unlitx + 0.5f) * SHRT_MAX/lm.w)), + short(ceil((lm.unlity + 0.5f) * SHRT_MAX/lm.h))); + loopl(2) loopvj(t.tris[l]) + { + vertex &vtx = verts[t.tris[l][j]]; + if(vtx.lm.iszero()) vtx.lm = lmtc; + else if(vtx.lm != lmtc) + { + vertex vtx2 = vtx; + vtx2.lm = lmtc; + t.tris[l][j] = addvert(vtx2); + } + } + sortval *dst = indices.access(sortkey(k.tex, t.unlit, k.dim, k.layer, k.envmap, k.alpha)); + if(dst) loopl(2) loopvj(t.tris[l]) dst->tris[l].add(t.tris[l][j]); + } + } + + void optimize() + { + vector remap; + enumeratekt(indices, sortkey, k, sortval, t, + loopl(2) if(t.tris[l].length() && t.unlit<=0) + { + if(k.lmid>=LMID_RESERVED && lightmaptexs[k.lmid].unlitx>=0) + { + sortkey ukey(k.tex, LMID_AMBIENT, k.dim, k.layer, k.envmap, k.alpha); + sortval *uval = indices.access(ukey); + if(uval && uval->unlit<=0) + { + if(uval->unlit<0) texs.removeobj(ukey); + else remap.add(ukey); + uval->unlit = k.lmid; + } + } + else if(k.lmid==LMID_AMBIENT) + { + remap.add(k); + t.unlit = -1; + } + texs.add(k); + break; + } + ); + texs.sort(texsort); + + remapunlit(remap); + + matsurfs.shrink(optimizematsurfs(matsurfs.getbuf(), matsurfs.length())); + } + + static inline bool texsort(const sortkey &x, const sortkey &y) + { + if(x.alpha < y.alpha) return true; + if(x.alpha > y.alpha) return false; + if(x.layer < y.layer) return true; + if(x.layer > y.layer) return false; + if(x.tex == y.tex) + { + if(x.lmid < y.lmid) return true; + if(x.lmid > y.lmid) return false; + if(x.envmap < y.envmap) return true; + if(x.envmap > y.envmap) return false; + if(x.dim < y.dim) return true; + if(x.dim > y.dim) return false; + return false; + } + VSlot &xs = lookupvslot(x.tex, false), &ys = lookupvslot(y.tex, false); + if(xs.slot->shader < ys.slot->shader) return true; + if(xs.slot->shader > ys.slot->shader) return false; + if(xs.slot->params.length() < ys.slot->params.length()) return true; + if(xs.slot->params.length() > ys.slot->params.length()) return false; + if(x.tex < y.tex) return true; + else return false; + } + +#define GENVERTS(type, ptr, body) do \ + { \ + type *f = (type *)ptr; \ + loopv(verts) \ + { \ + const vertex &v = verts[i]; \ + body; \ + f++; \ + } \ + } while(0) + + void genverts(void *buf) + { + GENVERTS(vertex, buf, { *f = v; f->norm.flip(); f->tangent.flip(); }); + } + + void setupdata(vtxarray *va) + { + va->verts = verts.length(); + va->tris = worldtris/3; + va->vbuf = 0; + va->vdata = 0; + va->minvert = 0; + va->maxvert = va->verts-1; + va->voffset = 0; + if(va->verts) + { + if(vbosize[VBO_VBUF] + verts.length() > maxvbosize || + vbosize[VBO_EBUF] + worldtris > USHRT_MAX || + vbosize[VBO_SKYBUF] + skytris > USHRT_MAX) + flushvbo(); + + va->voffset = vbosize[VBO_VBUF]; + uchar *vdata = addvbo(va, VBO_VBUF, va->verts, sizeof(vertex)); + genverts(vdata); + va->minvert += va->voffset; + va->maxvert += va->voffset; + } + + va->matbuf = NULL; + va->matsurfs = matsurfs.length(); + if(va->matsurfs) + { + va->matbuf = new materialsurface[matsurfs.length()]; + memcpy(va->matbuf, matsurfs.getbuf(), matsurfs.length()*sizeof(materialsurface)); + } + + va->skybuf = 0; + va->skydata = 0; + va->sky = skyindices.length(); + va->explicitsky = explicitskyindices.length(); + if(va->sky + va->explicitsky) + { + va->skydata += vbosize[VBO_SKYBUF]; + ushort *skydata = (ushort *)addvbo(va, VBO_SKYBUF, va->sky+va->explicitsky, sizeof(ushort)); + memcpy(skydata, skyindices.getbuf(), va->sky*sizeof(ushort)); + memcpy(skydata+va->sky, explicitskyindices.getbuf(), va->explicitsky*sizeof(ushort)); + if(va->voffset) loopi(va->sky+va->explicitsky) skydata[i] += va->voffset; + } + + va->eslist = NULL; + va->texs = texs.length(); + va->blendtris = 0; + va->blends = 0; + va->alphabacktris = 0; + va->alphaback = 0; + va->alphafronttris = 0; + va->alphafront = 0; + va->ebuf = 0; + va->edata = 0; + va->texmask = 0; + if(va->texs) + { + va->eslist = new elementset[va->texs]; + va->edata += vbosize[VBO_EBUF]; + ushort *edata = (ushort *)addvbo(va, VBO_EBUF, worldtris, sizeof(ushort)), *curbuf = edata; + loopv(texs) + { + const sortkey &k = texs[i]; + const sortval &t = indices[k]; + elementset &e = va->eslist[i]; + e.texture = k.tex; + e.lmid = t.unlit>0 ? t.unlit : k.lmid; + e.dim = k.dim; + e.layer = k.layer; + e.envmap = k.envmap; + ushort *startbuf = curbuf; + loopl(2) + { + e.minvert[l] = USHRT_MAX; + e.maxvert[l] = 0; + + if(t.tris[l].length()) + { + memcpy(curbuf, t.tris[l].getbuf(), t.tris[l].length() * sizeof(ushort)); + + loopvj(t.tris[l]) + { + curbuf[j] += va->voffset; + e.minvert[l] = min(e.minvert[l], curbuf[j]); + e.maxvert[l] = max(e.maxvert[l], curbuf[j]); + } + + curbuf += t.tris[l].length(); + } + e.length[l] = curbuf-startbuf; + } + if(k.layer==LAYER_BLEND) { va->texs--; va->tris -= e.length[1]/3; va->blends++; va->blendtris += e.length[1]/3; } + else if(k.alpha==ALPHA_BACK) { va->texs--; va->tris -= e.length[1]/3; va->alphaback++; va->alphabacktris += e.length[1]/3; } + else if(k.alpha==ALPHA_FRONT) { va->texs--; va->tris -= e.length[1]/3; va->alphafront++; va->alphafronttris += e.length[1]/3; } + + Slot &slot = *lookupvslot(k.tex, false).slot; + loopvj(slot.sts) va->texmask |= 1<type&SHADER_ENVMAP) va->texmask |= 1<alphatris = va->alphabacktris + va->alphafronttris; + + if(grasstris.length()) + { + va->grasstris.move(grasstris); + useshaderbyname("grass"); + } + + if(mapmodels.length()) va->mapmodels.put(mapmodels.getbuf(), mapmodels.length()); + } + + bool emptyva() + { + return verts.empty() && matsurfs.empty() && skyindices.empty() && explicitskyindices.empty() && grasstris.empty() && mapmodels.empty(); + } +} vc; + +int recalcprogress = 0; +#define progress(s) if((recalcprogress++&0xFFF)==0) renderprogress(recalcprogress/(float)allocnodes, s); + +vector tjoints; + +vec shadowmapmin, shadowmapmax; + +int calcshadowmask(vec *pos, int numpos) +{ + extern vec shadowdir; + int mask = 0, used = 1; + vec pe = vec(pos[1]).sub(pos[0]); + loopk(numpos-2) + { + vec e = vec(pos[k+2]).sub(pos[0]); + if(vec().cross(pe, e).dot(shadowdir)>0) + { + mask |= 1< &idxs = key.tex==DEFAULT_SKY ? vc.explicitskyindices : vc.indices[key].tris[(shadowmask>>i)&1]; + int left = index[0], mid = index[i+1], right = index[i+2], start = left, i0 = left, i1 = -1; + loopk(4) + { + int i2 = -1, ctj = -1, cedge = -1; + switch(k) + { + case 1: i1 = i2 = mid; cedge = edge+i+1; break; + case 2: if(i1 != mid || i0 == left) { i0 = i1; i1 = right; } i2 = right; if(i+1 == numverts-2) cedge = edge+i+2; break; + case 3: if(i0 == start) { i0 = i1; i1 = left; } i2 = left; // fall-through + default: if(!i) cedge = edge; break; + } + if(i1 != i2) + { + if(total + 3 > USHRT_MAX) return; + total += 3; + idxs.add(i0); + idxs.add(i1); + idxs.add(i2); + i1 = i2; + } + if(cedge >= 0) + { + for(ctj = tj;;) + { + if(ctj < 0) break; + if(tjoints[ctj].edge < cedge) { ctj = tjoints[ctj].next; continue; } + if(tjoints[ctj].edge != cedge) ctj = -1; + break; + } + } + if(ctj >= 0) + { + int e1 = cedge%(MAXFACEVERTS+1), e2 = (e1+1)%numverts; + vertex &v1 = verts[e1], &v2 = verts[e2]; + ivec d(vec(v2.pos).sub(v1.pos).mul(8)); + int axis = abs(d.x) > abs(d.y) ? (abs(d.x) > abs(d.z) ? 0 : 2) : (abs(d.y) > abs(d.z) ? 1 : 2); + if(d[axis] < 0) d.neg(); + reduceslope(d); + int origin = int(min(v1.pos[axis], v2.pos[axis])*8)&~0x7FFF, + offset1 = (int(v1.pos[axis]*8) - origin) / d[axis], + offset2 = (int(v2.pos[axis]*8) - origin) / d[axis]; + vec o = vec(v1.pos).sub(vec(d).mul(offset1/8.0f)); + float doffset = 1.0f / (offset2 - offset1); + + if(i1 < 0) for(;;) + { + tjoint &t = tjoints[ctj]; + if(t.next < 0 || tjoints[t.next].edge != cedge) break; + ctj = t.next; + } + while(ctj >= 0) + { + tjoint &t = tjoints[ctj]; + if(t.edge != cedge) break; + float offset = (t.offset - offset1) * doffset; + vertex vt; + vt.pos = vec(d).mul(t.offset/8.0f).add(o); + vt.tc.lerp(v1.tc, v2.tc, offset); + vt.lm.x = short(v1.lm.x + (v2.lm.x-v1.lm.x)*offset), + vt.lm.y = short(v1.lm.y + (v2.lm.y-v1.lm.y)*offset); + vt.norm.lerp(v1.norm, v2.norm, offset); + vt.tangent.lerp(v1.tangent, v2.tangent, offset); + int i2 = vc.addvert(vt); + if(i2 < 0) return; + if(i1 >= 0) + { + if(total + 3 > USHRT_MAX) return; + total += 3; + idxs.add(i0); + idxs.add(i1); + idxs.add(i2); + i1 = i2; + } + else start = i0 = i2; + ctj = t.next; + } + } + } + } +} + +void addgrasstri(int face, vertex *verts, int numv, ushort texture, ushort lmid) +{ + grasstri &g = vc.grasstris.add(); + int i1, i2, i3, i4; + if(numv <= 3 && face%2) { i1 = face+1; i2 = face+2; i3 = i4 = 0; } + else { i1 = 0; i2 = face+1; i3 = face+2; i4 = numv > 3 ? face+3 : i3; } + g.v[0] = verts[i1].pos; + g.v[1] = verts[i2].pos; + g.v[2] = verts[i3].pos; + g.v[3] = verts[i4].pos; + g.numv = numv; + + g.surface.toplane(g.v[0], g.v[1], g.v[2]); + if(g.surface.z <= 0) { vc.grasstris.pop(); return; } + + g.minz = min(min(g.v[0].z, g.v[1].z), min(g.v[2].z, g.v[3].z)); + g.maxz = max(max(g.v[0].z, g.v[1].z), max(g.v[2].z, g.v[3].z)); + + g.center = vec(0, 0, 0); + loopk(numv) g.center.add(g.v[k]); + g.center.div(numv); + g.radius = 0; + loopk(numv) g.radius = max(g.radius, g.v[k].dist(g.center)); + + vec area, bx, by; + area.cross(vec(g.v[1]).sub(g.v[0]), vec(g.v[2]).sub(g.v[0])); + float scale; + int px, py; + + if(fabs(area.x) >= fabs(area.y) && fabs(area.x) >= fabs(area.z)) + scale = 1/area.x, px = 1, py = 2; + else if(fabs(area.y) >= fabs(area.x) && fabs(area.y) >= fabs(area.z)) + scale = -1/area.y, px = 0, py = 2; + else + scale = 1/area.z, px = 0, py = 1; + + bx.x = (g.v[2][py] - g.v[0][py])*scale; + bx.y = (g.v[2][px] - g.v[0][px])*scale; + bx.z = bx.x*g.v[2][px] - bx.y*g.v[2][py]; + + by.x = (g.v[2][py] - g.v[1][py])*scale; + by.y = (g.v[2][px] - g.v[1][px])*scale; + by.z = by.x*g.v[1][px] - by.y*g.v[1][py] - 1; + by.sub(bx); + + float tc1u = verts[i1].lm.x, + tc1v = verts[i1].lm.y, + tc2u = verts[i2].lm.x - verts[i1].lm.x, + tc2v = verts[i2].lm.y - verts[i1].lm.y, + tc3u = verts[i3].lm.x - verts[i1].lm.x, + tc3v = verts[i3].lm.y - verts[i1].lm.y; + + g.tcu = vec4(0, 0, 0, tc1u - (bx.z*tc2u + by.z*tc3u)); + g.tcu[px] = bx.x*tc2u + by.x*tc3u; + g.tcu[py] = -(bx.y*tc2u + by.y*tc3u); + + g.tcv = vec4(0, 0, 0, tc1v - (bx.z*tc2v + by.z*tc3v)); + g.tcv[px] = bx.x*tc2v + by.x*tc3v; + g.tcv[py] = -(bx.y*tc2v + by.y*tc3v); + + g.texture = texture; + g.lmid = lmid; +} + +static inline void calctexgen(VSlot &vslot, int dim, vec4 &sgen, vec4 &tgen) +{ + Texture *tex = vslot.slot->sts.empty() ? notexture : vslot.slot->sts[0].t; + const texrotation &r = texrotations[vslot.rotation]; + float k = TEX_SCALE/vslot.scale, + xs = r.flipx ? -tex->xs : tex->xs, + ys = r.flipy ? -tex->ys : tex->ys, + sk = k/xs, tk = k/ys, + soff = -(r.swapxy ? vslot.offset.y : vslot.offset.x)/xs, + toff = -(r.swapxy ? vslot.offset.x : vslot.offset.y)/ys; + static const int si[] = { 1, 0, 0 }, ti[] = { 2, 2, 1 }; + int sdim = si[dim], tdim = ti[dim]; + sgen = vec4(0, 0, 0, soff); + tgen = vec4(0, 0, 0, toff); + if(r.swapxy) + { + sgen[tdim] = (dim <= 1 ? -sk : sk); + tgen[sdim] = tk; + } + else + { + sgen[sdim] = sk; + tgen[tdim] = (dim <= 1 ? -tk : tk); + } +} + +ushort encodenormal(const vec &n) +{ + if(n.iszero()) return 0; + int yaw = int(-atan2(n.x, n.y)/RAD), pitch = int(asin(n.z)/RAD); + return ushort(clamp(pitch + 90, 0, 180)*360 + (yaw < 0 ? yaw%360 + 360 : yaw%360) + 1); +} + +vec decodenormal(ushort norm) +{ + if(!norm) return vec(0, 0, 1); + norm--; + const vec2 &yaw = sincos360[norm%360], &pitch = sincos360[norm/360+270]; + return vec(-yaw.y*pitch.x, yaw.x*pitch.x, pitch.y); +} + +void guessnormals(const vec *pos, int numverts, vec *normals) +{ + vec n1, n2; + n1.cross(pos[0], pos[1], pos[2]); + if(numverts != 4) + { + n1.normalize(); + loopk(numverts) normals[k] = n1; + return; + } + n2.cross(pos[0], pos[2], pos[3]); + if(n1.iszero()) + { + n2.normalize(); + loopk(4) normals[k] = n2; + return; + } + else n1.normalize(); + if(n2.iszero()) + { + loopk(4) normals[k] = n1; + return; + } + else n2.normalize(); + vec avg = vec(n1).add(n2).normalize(); + normals[0] = avg; + normals[1] = n1; + normals[2] = avg; + normals[3] = n2; +} + +void addcubeverts(VSlot &vslot, int orient, int size, vec *pos, int convex, ushort texture, ushort lmid, vertinfo *vinfo, int numverts, int tj = -1, ushort envmap = EMID_NONE, int grassy = 0, bool alpha = false, int layer = LAYER_TOP) +{ + int dim = dimension(orient); + int shadowmask = texture==DEFAULT_SKY || alpha ? 0 : calcshadowmask(pos, numverts); + + LightMap *lm = NULL; + LightMapTexture *lmtex = NULL; + if(lightmaps.inrange(lmid-LMID_RESERVED)) + { + lm = &lightmaps[lmid-LMID_RESERVED]; + if((lm->type&LM_TYPE)==LM_DIFFUSE || + ((lm->type&LM_TYPE)==LM_BUMPMAP0 && + lightmaps.inrange(lmid+1-LMID_RESERVED) && + (lightmaps[lmid+1-LMID_RESERVED].type&LM_TYPE)==LM_BUMPMAP1)) + lmtex = &lightmaptexs[lm->tex]; + else lm = NULL; + } + + vec4 sgen, tgen; + calctexgen(vslot, dim, sgen, tgen); + vertex verts[MAXFACEVERTS]; + int index[MAXFACEVERTS]; + loopk(numverts) + { + vertex &v = verts[k]; + v.pos = pos[k]; + v.tc = vec2(sgen.dot(v.pos), tgen.dot(v.pos)); + if(lmtex) + { + v.lm = svec2(short(ceil((lm->offsetx + vinfo[k].u*(float(LM_PACKW)/float(USHRT_MAX+1)) + 0.5f) * float(SHRT_MAX)/lmtex->w)), + short(ceil((lm->offsety + vinfo[k].v*(float(LM_PACKH)/float(USHRT_MAX+1)) + 0.5f) * float(SHRT_MAX)/lmtex->h))); + } + else v.lm = svec2(0, 0); + if(vinfo && vinfo[k].norm) + { + vec n = decodenormal(vinfo[k].norm), t = orientation_tangent[vslot.rotation][dim]; + t.project(n).normalize(); + v.norm = bvec(n); + v.tangent = bvec4(bvec(t), orientation_bitangent[vslot.rotation][dim].scalartriple(n, t) < 0 ? 0 : 255); + } + else + { + v.norm = vinfo && vinfo[k].norm && envmap != EMID_NONE ? bvec(decodenormal(vinfo[k].norm)) : bvec(128, 128, 255); + v.tangent = bvec4(255, 128, 128, 255); + } + index[k] = vc.addvert(v); + if(index[k] < 0) return; + } + + if(texture == DEFAULT_SKY) + { + loopk(numverts) vc.skyclip = min(vc.skyclip, int(pos[k].z*8)>>3); + vc.skymask |= 0x3F&~(1<= LMID_RESERVED) lmid = lm ? lm->tex : LMID_AMBIENT; + + sortkey key(texture, lmid, !vslot.scroll.iszero() ? dim : 3, layer == LAYER_BLEND ? LAYER_BLEND : LAYER_TOP, envmap, alpha ? (vslot.alphaback ? ALPHA_BACK : (vslot.alphafront ? ALPHA_FRONT : NO_ALPHA)) : NO_ALPHA); + addtris(key, orient, verts, index, numverts, convex, shadowmask, tj); + + if(grassy) + { + for(int i = 0; i < numverts-2; i += 2) + { + int faces = 0; + if(index[0]!=index[i+1] && index[i+1]!=index[i+2] && index[i+2]!=index[0]) faces |= 1; + if(i+3 < numverts && index[0]!=index[i+2] && index[i+2]!=index[i+3] && index[i+3]!=index[0]) faces |= 2; + if(grassy > 1 && faces==3) addgrasstri(i, verts, 4, texture, lmid); + else + { + if(faces&1) addgrasstri(i, verts, 3, texture, lmid); + if(faces&2) addgrasstri(i+1, verts, 3, texture, lmid); + } + } + } +} + +struct edgegroup +{ + ivec slope, origin; + int axis; +}; + +static uint hthash(const edgegroup &g) +{ + return g.slope.x^(g.slope.y<<2)^(g.slope.z<<4)^g.origin.x^g.origin.y^g.origin.z; +} + +static bool htcmp(const edgegroup &x, const edgegroup &y) +{ + return x.slope==y.slope && x.origin==y.origin; +} + +enum +{ + CE_START = 1<<0, + CE_END = 1<<1, + CE_FLIP = 1<<2, + CE_DUP = 1<<3 +}; + +struct cubeedge +{ + cube *c; + int next, offset; + ushort size; + uchar index, flags; +}; + +vector cubeedges; +hashtable edgegroups(1<<13); + +void gencubeedges(cube &c, const ivec &co, int size) +{ + ivec pos[MAXFACEVERTS]; + int vis; + loopi(6) if((vis = visibletris(c, i, co, size))) + { + int numverts = c.ext ? c.ext->surfaces[i].numverts&MAXFACEVERTS : 0; + if(numverts) + { + vertinfo *verts = c.ext->verts() + c.ext->surfaces[i].verts; + ivec vo = ivec(co).mask(~0xFFF).shl(3); + loopj(numverts) + { + vertinfo &v = verts[j]; + pos[j] = ivec(v.x, v.y, v.z).add(vo); + } + } + else if(c.merged&(1< abs(d.y) ? (abs(d.x) > abs(d.z) ? 0 : 2) : (abs(d.y) > abs(d.z) ? 1 : 2); + if(d[axis] < 0) + { + d.neg(); + swap(e1, e2); + } + reduceslope(d); + + int t1 = pos[e1][axis]/d[axis], + t2 = pos[e2][axis]/d[axis]; + edgegroup g; + g.origin = ivec(pos[e1]).sub(ivec(d).mul(t1)); + g.slope = d; + g.axis = axis; + cubeedge ce; + ce.c = &c; + ce.offset = t1; + ce.size = t2 - t1; + ce.index = i*(MAXFACEVERTS+1)+j; + ce.flags = CE_START | CE_END | (e1!=j ? CE_FLIP : 0); + ce.next = -1; + + bool insert = true; + int *exists = edgegroups.access(g); + if(exists) + { + int prev = -1, cur = *exists; + while(cur >= 0) + { + cubeedge &p = cubeedges[cur]; + if(ce.offset <= p.offset+p.size) + { + if(ce.offset < p.offset) break; + if(p.flags&CE_DUP ? + ce.offset+ce.size <= p.offset+p.size : + ce.offset==p.offset && ce.size==p.size) + { + p.flags |= CE_DUP; + insert = false; + break; + } + if(ce.offset == p.offset+p.size) ce.flags &= ~CE_START; + } + prev = cur; + cur = p.next; + } + if(insert) + { + ce.next = cur; + while(cur >= 0) + { + cubeedge &p = cubeedges[cur]; + if(ce.offset+ce.size==p.offset) { ce.flags &= ~CE_END; break; } + cur = p.next; + } + if(prev>=0) cubeedges[prev].next = cubeedges.length(); + else *exists = cubeedges.length(); + } + } + else edgegroups[g] = cubeedges.length(); + + if(insert) cubeedges.add(ce); + } + } +} + +void gencubeedges(cube *c = worldroot, const ivec &co = ivec(0, 0, 0), int size = worldsize>>1) +{ + progress("fixing t-joints..."); + neighbourstack[++neighbourdepth] = c; + loopi(8) + { + ivec o(i, co, size); + if(c[i].ext) c[i].ext->tjoints = -1; + if(c[i].children) gencubeedges(c[i].children, o, size>>1); + else if(!isempty(c[i])) gencubeedges(c[i], o, size); + } + --neighbourdepth; +} + +void gencubeverts(cube &c, const ivec &co, int size, int csi) +{ + if(!(c.visible&0xC0)) return; + + int vismask = ~c.merged & 0x3F; + if(!(c.visible&0x80)) vismask &= c.visible; + if(!vismask) return; + + int tj = filltjoints && c.ext ? c.ext->tjoints : -1, vis; + loopi(6) if(vismask&(1<surfaces[i].numverts&MAXFACEVERTS : 0, convex = 0; + if(numverts) + { + verts = c.ext->verts() + c.ext->surfaces[i].verts; + vec vo(ivec(co).mask(~0xFFF)); + loopj(numverts) pos[j] = vec(verts[j].getxyz()).mul(1.0f/8).add(vo); + if(!flataxisface(c, i)) convex = faceconvexity(verts, numverts, size); + } + else + { + ivec v[4]; + genfaceverts(c, i, v); + if(!flataxisface(c, i)) convex = faceconvexity(v); + int order = vis&4 || convex < 0 ? 1 : 0; + vec vo(co); + pos[numverts++] = vec(v[order]).mul(size/8.0f).add(vo); + if(vis&1) pos[numverts++] = vec(v[order+1]).mul(size/8.0f).add(vo); + pos[numverts++] = vec(v[order+2]).mul(size/8.0f).add(vo); + if(vis&2) pos[numverts++] = vec(v[(order+3)&3]).mul(size/8.0f).add(vo); + } + + VSlot &vslot = lookupvslot(c.texture[i], true), + *layer = vslot.layer && !(c.material&MAT_ALPHA) ? &lookupvslot(vslot.layer, true) : NULL; + ushort envmap = vslot.slot->shader->type&SHADER_ENVMAP ? (vslot.slot->texmask&(1<slot->shader->type&SHADER_ENVMAP ? (layer->slot->texmask&(1<= 0 && tjoints[tj].edge < i*(MAXFACEVERTS+1)) tj = tjoints[tj].next; + int hastj = tj >= 0 && tjoints[tj].edge < (i+1)*(MAXFACEVERTS+1) ? tj : -1; + int grassy = vslot.slot->autograss && i!=O_BOTTOM ? (vis!=3 || convex ? 1 : 2) : 0; + if(!c.ext) + addcubeverts(vslot, i, size, pos, convex, c.texture[i], LMID_AMBIENT, NULL, numverts, hastj, envmap, grassy, (c.material&MAT_ALPHA)!=0); + else + { + const surfaceinfo &surf = c.ext->surfaces[i]; + if(!surf.numverts || surf.numverts&LAYER_TOP) + addcubeverts(vslot, i, size, pos, convex, c.texture[i], surf.lmid[0], verts, numverts, hastj, envmap, grassy, (c.material&MAT_ALPHA)!=0, LAYER_TOP|(surf.numverts&LAYER_BLEND)); + if(surf.numverts&LAYER_BOTTOM) + addcubeverts(layer ? *layer : vslot, i, size, pos, convex, vslot.layer, surf.lmid[1], surf.numverts&LAYER_DUP ? verts + numverts : verts, numverts, hastj, envmap2); + } + } +} + +static inline bool skyoccluded(cube &c, int orient) +{ + return touchingface(c, orient) && faceedges(c, orient) == F_SOLID; +} + +static int dummyskyfaces[6]; +static inline int hasskyfaces(cube &c, const ivec &co, int size, int faces[6] = dummyskyfaces) +{ + int numfaces = 0; + if(isempty(c) || c.material&MAT_ALPHA) + { + if(co.x == 0) faces[numfaces++] = O_LEFT; + if(co.x + size == worldsize) faces[numfaces++] = O_RIGHT; + if(co.y == 0) faces[numfaces++] = O_BACK; + if(co.y + size == worldsize) faces[numfaces++] = O_FRONT; + if(co.z == 0) faces[numfaces++] = O_BOTTOM; + if(co.z + size == worldsize) faces[numfaces++] = O_TOP; + } + else if(!isentirelysolid(c)) + { + if(co.x == 0 && !skyoccluded(c, O_LEFT)) faces[numfaces++] = O_LEFT; + if(co.x + size == worldsize && !skyoccluded(c, O_RIGHT)) faces[numfaces++] = O_RIGHT; + if(co.y == 0 && !skyoccluded(c, O_BACK)) faces[numfaces++] = O_BACK; + if(co.y + size == worldsize && !skyoccluded(c, O_FRONT)) faces[numfaces++] = O_FRONT; + if(co.z == 0 && !skyoccluded(c, O_BOTTOM)) faces[numfaces++] = O_BOTTOM; + if(co.z + size == worldsize && !skyoccluded(c, O_TOP)) faces[numfaces++] = O_TOP; + } + return numfaces; +} + +void minskyface(cube &cu, int orient, const ivec &co, int size, facebounds &orig) +{ + facebounds mincf; + mincf.u1 = orig.u2; + mincf.u2 = orig.u1; + mincf.v1 = orig.v2; + mincf.v2 = orig.v1; + mincubeface(cu, orient, co, size, orig, mincf, MAT_ALPHA, MAT_ALPHA); + orig.u1 = max(mincf.u1, orig.u1); + orig.u2 = min(mincf.u2, orig.u2); + orig.v1 = max(mincf.v1, orig.v1); + orig.v2 = min(mincf.v2, orig.v2); +} + +void genskyfaces(cube &c, const ivec &o, int size) +{ + int faces[6], numfaces = hasskyfaces(c, o, size, faces); + if(!numfaces) return; + + loopi(numfaces) + { + int orient = faces[i], dim = dimension(orient); + facebounds m; + m.u1 = (o[C[dim]]&0xFFF)<<3; + m.u2 = m.u1 + (size<<3); + m.v1 = (o[R[dim]]&0xFFF)<<3; + m.v2 = m.v1 + (size<<3); + minskyface(c, orient, o, size, m); + if(m.u1 >= m.u2 || m.v1 >= m.v2) continue; + vc.skyarea += (int(m.u2-m.u1)*int(m.v2-m.v1) + (1<<(2*3))-1)>>(2*3); + vc.skyfaces[orient].add(m); + } +} + +void addskyverts(const ivec &o, int size) +{ + loopi(6) + { + int dim = dimension(i), c = C[dim], r = R[dim]; + vector &sf = vc.skyfaces[i]; + if(sf.empty()) continue; + vc.skymask |= 0x3F&~(1<>3); + } + if(vc.skytris + 6 > USHRT_MAX) break; + vc.skytris += 6; + vc.skyindices.add(index[0]); + vc.skyindices.add(index[1]); + vc.skyindices.add(index[2]); + + vc.skyindices.add(index[0]); + vc.skyindices.add(index[2]); + vc.skyindices.add(index[3]); + nextskyface:; + } + } +} + +////////// Vertex Arrays ////////////// + +int allocva = 0; +int wtris = 0, wverts = 0, vtris = 0, vverts = 0, glde = 0, gbatches = 0; +vector valist, varoot; + +vtxarray *newva(const ivec &co, int size) +{ + vc.optimize(); + + vtxarray *va = new vtxarray; + va->parent = NULL; + va->o = co; + va->size = size; + va->skyarea = vc.skyarea; + va->skyfaces = vc.skymask; + va->skyclip = vc.skyclip < INT_MAX ? vc.skyclip : INT_MAX; + va->curvfc = VFC_NOT_VISIBLE; + va->occluded = OCCLUDE_NOTHING; + va->query = NULL; + va->bbmin = ivec(-1, -1, -1); + va->bbmax = ivec(-1, -1, -1); + va->hasmerges = 0; + va->mergelevel = -1; + + vc.setupdata(va); + + wverts += va->verts; + wtris += va->tris + va->blends + va->alphatris; + allocva++; + valist.add(va); + + return va; +} + +void destroyva(vtxarray *va, bool reparent) +{ + wverts -= va->verts; + wtris -= va->tris + va->blends + va->alphatris; + allocva--; + valist.removeobj(va); + if(!va->parent) varoot.removeobj(va); + if(reparent) + { + if(va->parent) va->parent->children.removeobj(va); + loopv(va->children) + { + vtxarray *child = va->children[i]; + child->parent = va->parent; + if(child->parent) child->parent->children.add(child); + } + } + if(va->vbuf) destroyvbo(va->vbuf); + if(va->ebuf) destroyvbo(va->ebuf); + if(va->skybuf) destroyvbo(va->skybuf); + if(va->eslist) delete[] va->eslist; + if(va->matbuf) delete[] va->matbuf; + delete va; +} + +void clearvas(cube *c) +{ + loopi(8) + { + if(c[i].ext) + { + if(c[i].ext->va) destroyva(c[i].ext->va, false); + c[i].ext->va = NULL; + c[i].ext->tjoints = -1; + } + if(c[i].children) clearvas(c[i].children); + } +} + +void updatevabb(vtxarray *va, bool force) +{ + if(!force && va->bbmin.x >= 0) return; + + va->bbmin = va->geommin; + va->bbmax = va->geommax; + va->bbmin.min(va->matmin); + va->bbmax.max(va->matmax); + loopv(va->children) + { + vtxarray *child = va->children[i]; + updatevabb(child, force); + va->bbmin.min(child->bbmin); + va->bbmax.max(child->bbmax); + } + loopv(va->mapmodels) + { + octaentities *oe = va->mapmodels[i]; + va->bbmin.min(oe->bbmin); + va->bbmax.max(oe->bbmax); + } + va->bbmin.max(va->o); + va->bbmax.min(ivec(va->o).add(va->size)); + + if(va->skyfaces) + { + va->skyfaces |= 0x80; + if(va->sky) loop(dim, 3) if(va->skyfaces&(3<<(2*dim))) + { + int r = R[dim], c = C[dim]; + if((va->skyfaces&(1<<(2*dim)) && va->o[dim] < va->bbmin[dim]) || + (va->skyfaces&(2<<(2*dim)) && va->o[dim]+va->size > va->bbmax[dim]) || + va->o[r] < va->bbmin[r] || va->o[r]+va->size > va->bbmax[r] || + va->o[c] < va->bbmin[c] || va->o[c]+va->size > va->bbmax[c]) + { + va->skyfaces &= ~0x80; + break; + } + } + } +} + +void updatevabbs(bool force) +{ + loopv(varoot) updatevabb(varoot[i], force); +} + +struct mergedface +{ + uchar orient, lmid, numverts; + ushort mat, tex, envmap; + vertinfo *verts; + int tjoints; +}; + +#define MAXMERGELEVEL 12 +static int vahasmerges = 0, vamergemax = 0; +static vector vamerges[MAXMERGELEVEL+1]; + +int genmergedfaces(cube &c, const ivec &co, int size, int minlevel = -1) +{ + if(!c.ext || isempty(c)) return -1; + int tj = c.ext->tjoints, maxlevel = -1; + loopi(6) if(c.merged&(1<surfaces[i]; + int numverts = surf.numverts&MAXFACEVERTS; + if(!numverts) + { + if(minlevel < 0) vahasmerges |= MERGE_PART; + continue; + } + mergedface mf; + mf.orient = i; + mf.mat = c.material; + mf.tex = c.texture[i]; + mf.envmap = EMID_NONE; + mf.lmid = surf.lmid[0]; + mf.numverts = surf.numverts; + mf.verts = c.ext->verts() + surf.verts; + mf.tjoints = -1; + int level = calcmergedsize(i, co, size, mf.verts, mf.numverts&MAXFACEVERTS); + if(level > minlevel) + { + maxlevel = max(maxlevel, level); + + while(tj >= 0 && tjoints[tj].edge < i*(MAXFACEVERTS+1)) tj = tjoints[tj].next; + if(tj >= 0 && tjoints[tj].edge < (i+1)*(MAXFACEVERTS+1)) mf.tjoints = tj; + + VSlot &vslot = lookupvslot(mf.tex, true), + *layer = vslot.layer && !(c.material&MAT_ALPHA) ? &lookupvslot(vslot.layer, true) : NULL; + if(vslot.slot->shader->type&SHADER_ENVMAP) + mf.envmap = vslot.slot->texmask&(1<slot->shader->type&SHADER_ENVMAP ? (layer->slot->texmask&(1<= 0) + { + vamergemax = max(vamergemax, maxlevel); + vahasmerges |= MERGE_ORIGIN; + } + return maxlevel; +} + +int findmergedfaces(cube &c, const ivec &co, int size, int csi, int minlevel) +{ + if(c.ext && c.ext->va && !(c.ext->va->hasmerges&MERGE_ORIGIN)) return c.ext->va->mergelevel; + else if(c.children) + { + int maxlevel = -1; + loopi(8) + { + ivec o(i, co, size/2); + int level = findmergedfaces(c.children[i], o, size/2, csi-1, minlevel); + maxlevel = max(maxlevel, level); + } + return maxlevel; + } + else if(c.ext && c.merged) return genmergedfaces(c, co, size, minlevel); + else return -1; +} + +void addmergedverts(int level, const ivec &o) +{ + vector &mfl = vamerges[level]; + if(mfl.empty()) return; + vec vo(ivec(o).mask(~0xFFF)); + vec pos[MAXFACEVERTS]; + loopv(mfl) + { + mergedface &mf = mfl[i]; + int numverts = mf.numverts&MAXFACEVERTS; + loopi(numverts) + { + vertinfo &v = mf.verts[i]; + pos[i] = vec(v.x, v.y, v.z).mul(1.0f/8).add(vo); + } + VSlot &vslot = lookupvslot(mf.tex, true); + int grassy = vslot.slot->autograss && mf.orient!=O_BOTTOM && mf.numverts&LAYER_TOP ? 2 : 0; + addcubeverts(vslot, mf.orient, 1<va) + { + maxlevel = max(maxlevel, c.ext->va->mergelevel); + return; // don't re-render + } + + if(c.children) + { + neighbourstack[++neighbourdepth] = c.children; + c.escaped = 0; + loopi(8) + { + ivec o(i, co, size/2); + int level = -1; + rendercube(c.children[i], o, size/2, csi-1, level); + if(level >= csi) + c.escaped |= 1<ents && c.ext->ents->mapmodels.length()) vc.mapmodels.add(c.ext->ents); + } + return; + } + + genskyfaces(c, co, size); + + if(!isempty(c)) + { + gencubeverts(c, co, size, csi); + if(c.merged) maxlevel = max(maxlevel, genmergedfaces(c, co, size)); + } + if(c.material != MAT_AIR) genmatsurfs(c, co, size, vc.matsurfs); + + if(c.ext) + { + if(c.ext->ents && c.ext->ents->mapmodels.length()) vc.mapmodels.add(c.ext->ents); + } + + if(csi <= MAXMERGELEVEL && vamerges[csi].length()) addmergedverts(csi, co); +} + +void calcgeombb(const ivec &co, int size, ivec &bbmin, ivec &bbmax) +{ + vec vmin(co), vmax = vmin; + vmin.add(size); + + loopv(vc.verts) + { + const vec &v = vc.verts[i].pos; + vmin.min(v); + vmax.max(v); + } + + bbmin = ivec(vmin.mul(8)).shr(3); + bbmax = ivec(vmax.mul(8)).add(7).shr(3); +} + +void calcmatbb(const ivec &co, int size, ivec &bbmin, ivec &bbmax) +{ + bbmax = co; + (bbmin = bbmax).add(size); + loopv(vc.matsurfs) + { + materialsurface &m = vc.matsurfs[i]; + switch(m.material&MATF_VOLUME) + { + case MAT_WATER: + case MAT_GLASS: + case MAT_LAVA: + break; + + default: + continue; + } + + int dim = dimension(m.orient), + r = R[dim], + c = C[dim]; + bbmin[dim] = min(bbmin[dim], m.o[dim]); + bbmax[dim] = max(bbmax[dim], m.o[dim]); + + bbmin[r] = min(bbmin[r], m.o[r]); + bbmax[r] = max(bbmax[r], m.o[r] + m.rsize); + + bbmin[c] = min(bbmin[c], m.o[c]); + bbmax[c] = max(bbmax[c], m.o[c] + m.csize); + } +} + +void setva(cube &c, const ivec &co, int size, int csi) +{ + ASSERT(size <= 0x1000); + + int vamergeoffset[MAXMERGELEVEL+1]; + loopi(MAXMERGELEVEL+1) vamergeoffset[i] = vamerges[i].length(); + + vc.origin = co; + vc.size = size; + + shadowmapmin = vec(co).add(size); + shadowmapmax = vec(co); + + int maxlevel = -1; + rendercube(c, co, size, csi, maxlevel); + + ivec bbmin, bbmax; + + calcgeombb(co, size, bbmin, bbmax); + + addskyverts(co, size); + + if(size == min(0x1000, worldsize/2) || !vc.emptyva()) + { + vtxarray *va = newva(co, size); + ext(c).va = va; + va->geommin = bbmin; + va->geommax = bbmax; + calcmatbb(co, size, va->matmin, va->matmax); + va->shadowmapmin = ivec(shadowmapmin.mul(8)).shr(3); + va->shadowmapmax = ivec(shadowmapmax.mul(8)).add(7).shr(3); + va->hasmerges = vahasmerges; + va->mergelevel = vamergemax; + } + else + { + loopi(MAXMERGELEVEL+1) vamerges[i].setsize(vamergeoffset[i]); + } + + vc.clear(); +} + +static inline int setcubevisibility(cube &c, const ivec &co, int size) +{ + int numvis = 0, vismask = 0, collidemask = 0, checkmask = 0; + loopi(6) + { + int facemask = classifyface(c, i, co, size); + if(facemask&1) + { + vismask |= 1<surfaces[i].numverts&MAXFACEVERTS) numvis++; + } + else + { + numvis++; + if(c.texture[i] != DEFAULT_SKY && !(c.ext && c.ext->surfaces[i].numverts&MAXFACEVERTS)) checkmask |= 1<va) + { + varoot.add(c[i].ext->va); + if(c[i].ext->va->hasmerges&MERGE_ORIGIN) findmergedfaces(c[i], o, size, csi, csi); + } + else + { + if(c[i].children) count += updateva(c[i].children, o, size/2, csi-1); + else + { + if(!isempty(c[i])) count += setcubevisibility(c[i], o, size); + count += hasskyfaces(c[i], o, size); + } + int tcount = count + (csi <= MAXMERGELEVEL ? vamerges[csi].length() : 0); + if(tcount > vafacemax || (tcount >= vafacemin && size >= vacubesize) || size == min(0x1000, worldsize/2)) + { + loadprogress = clamp(recalcprogress/float(allocnodes), 0.0f, 1.0f); + setva(c[i], o, size, csi); + if(c[i].ext && c[i].ext->va) + { + while(varoot.length() > childpos) + { + vtxarray *child = varoot.pop(); + c[i].ext->va->children.add(child); + child->parent = c[i].ext->va; + } + varoot.add(c[i].ext->va); + if(vamergemax > size) + { + cmergemax = max(cmergemax, vamergemax); + chasmerges |= vahasmerges&~MERGE_USE; + } + continue; + } + else count = 0; + } + } + if(csi+1 <= MAXMERGELEVEL && vamerges[csi].length()) vamerges[csi+1].move(vamerges[csi]); + cmergemax = max(cmergemax, vamergemax); + chasmerges |= vahasmerges; + ccount += count; + } + --neighbourdepth; + vamergemax = cmergemax; + vahasmerges = chasmerges; + + return ccount; +} + +void addtjoint(const edgegroup &g, const cubeedge &e, int offset) +{ + int vcoord = (g.slope[g.axis]*offset + g.origin[g.axis]) & 0x7FFF; + tjoint &tj = tjoints.add(); + tj.offset = vcoord / g.slope[g.axis]; + tj.edge = e.index; + + int prev = -1, cur = ext(*e.c).tjoints; + while(cur >= 0) + { + tjoint &o = tjoints[cur]; + if(tj.edge < o.edge || (tj.edge==o.edge && (e.flags&CE_FLIP ? tj.offset > o.offset : tj.offset < o.offset))) break; + prev = cur; + cur = o.next; + } + + tj.next = cur; + if(prev < 0) e.c->ext->tjoints = tjoints.length()-1; + else tjoints[prev].next = tjoints.length()-1; +} + +void findtjoints(int cur, const edgegroup &g) +{ + int active = -1; + while(cur >= 0) + { + cubeedge &e = cubeedges[cur]; + int prevactive = -1, curactive = active; + while(curactive >= 0) + { + cubeedge &a = cubeedges[curactive]; + if(a.offset+a.size <= e.offset) + { + if(prevactive >= 0) cubeedges[prevactive].next = a.next; + else active = a.next; + } + else + { + prevactive = curactive; + if(!(a.flags&CE_DUP)) + { + if(e.flags&CE_START && e.offset > a.offset && e.offset < a.offset+a.size) + addtjoint(g, a, e.offset); + if(e.flags&CE_END && e.offset+e.size > a.offset && e.offset+e.size < a.offset+a.size) + addtjoint(g, a, e.offset+e.size); + } + if(!(e.flags&CE_DUP)) + { + if(a.flags&CE_START && a.offset > e.offset && a.offset < e.offset+e.size) + addtjoint(g, e, a.offset); + if(a.flags&CE_END && a.offset+a.size > e.offset && a.offset+a.size < e.offset+e.size) + addtjoint(g, e, a.offset+a.size); + } + } + curactive = a.next; + } + int next = e.next; + e.next = active; + active = cur; + cur = next; + } +} + +void findtjoints() +{ + recalcprogress = 0; + gencubeedges(); + tjoints.setsize(0); + enumeratekt(edgegroups, edgegroup, g, int, e, findtjoints(e, g)); + cubeedges.setsize(0); + edgegroups.clear(); +} + +void octarender() // creates va s for all leaf cubes that don't already have them +{ + int csi = 0; + while(1<explicitsky; + skyarea += va->skyarea; + } + + visibleva = NULL; +} + +void precachetextures() +{ + vector texs; + loopv(valist) + { + vtxarray *va = valist[i]; + loopj(va->texs + va->blends) if(texs.find(va->eslist[j].texture) < 0) texs.add(va->eslist[j].texture); + } + loopv(texs) + { + loadprogress = float(i+1)/texs.length(); + lookupvslot(texs[i]); + } + loadprogress = 0; +} + +void allchanged(bool load) +{ + renderprogress(0, "clearing vertex arrays..."); + clearvas(worldroot); + resetqueries(); + resetclipplanes(); + if(load) + { + setupsky(); + initenvmaps(); + } + guessshadowdir(); + entitiesinoctanodes(); + tjoints.setsize(0); + if(filltjoints) findtjoints(); + octarender(); + if(load) precachetextures(); + setupmaterials(); + invalidatepostfx(); + updatevabbs(true); + resetblobs(); + lightents(); + if(load) + { + seedparticles(); + drawtextures(); + } +} + +void recalc() +{ + allchanged(true); +} + +COMMAND(recalc, ""); + diff --git a/src/engine/physics.cpp b/src/engine/physics.cpp new file mode 100644 index 0000000..6e78863 --- /dev/null +++ b/src/engine/physics.cpp @@ -0,0 +1,2057 @@ +// physics.cpp: no physics books were hurt nor consulted in the construction of this code. +// All physics computations and constants were invented on the fly and simply tweaked until +// they "felt right", and have no basis in reality. Collision detection is simplistic but +// very robust (uses discrete steps at fixed fps). + +#include "engine.h" +#include "mpr.h" + +const int MAXCLIPPLANES = 1024; +static clipplanes clipcache[MAXCLIPPLANES]; +static int clipcacheversion = -2; + +static inline clipplanes &getclipplanes(const cube &c, const ivec &o, int size, bool collide = true, int offset = 0) +{ + clipplanes &p = clipcache[int(&c - worldroot)&(MAXCLIPPLANES-1)]; + if(p.owner != &c || p.version != clipcacheversion+offset) + { + p.owner = &c; + p.version = clipcacheversion+offset; + genclipplanes(c, o, size, p, collide); + } + return p; +} + +void resetclipplanes() +{ + clipcacheversion += 2; + if(!clipcacheversion) + { + memclear(clipcache); + clipcacheversion = 2; + } +} + +///////////////////////// ray - cube collision /////////////////////////////////////////////// + +#define INTERSECTPLANES(setentry, exit) \ + float enterdist = -1e16f, exitdist = 1e16f; \ + loopi(p.size) \ + { \ + float pdist = p.p[i].dist(v), facing = ray.dot(p.p[i]); \ + if(facing < 0) \ + { \ + pdist /= -facing; \ + if(pdist > enterdist) \ + { \ + if(pdist > exitdist) exit; \ + enterdist = pdist; \ + setentry; \ + } \ + } \ + else if(facing > 0) \ + { \ + pdist /= -facing; \ + if(pdist < exitdist) \ + { \ + if(pdist < enterdist) exit; \ + exitdist = pdist; \ + } \ + } \ + else if(pdist > 0) exit; \ + } + +#define INTERSECTBOX(setentry, exit) \ + loop(i, 3) \ + { \ + if(ray[i]) \ + { \ + float prad = fabs(p.r[i] * invray[i]), pdist = (p.o[i] - v[i]) * invray[i], pmin = pdist - prad, pmax = pdist + prad; \ + if(pmin > enterdist) \ + { \ + if(pmin > exitdist) exit; \ + enterdist = pmin; \ + setentry; \ + } \ + if(pmax < exitdist) \ + { \ + if(pmax < enterdist) exit; \ + exitdist = pmax; \ + } \ + } \ + else if(v[i] < p.o[i]-p.r[i] || v[i] > p.o[i]+p.r[i]) exit; \ + } + +vec hitsurface; + +static inline bool raycubeintersect(const clipplanes &p, const cube &c, const vec &v, const vec &ray, const vec &invray, float &dist) +{ + int entry = -1, bbentry = -1; + INTERSECTPLANES(entry = i, return false); + INTERSECTBOX(bbentry = i, return false); + if(exitdist < 0) return false; + dist = max(enterdist+0.1f, 0.0f); + if(bbentry>=0) { hitsurface = vec(0, 0, 0); hitsurface[bbentry] = ray[bbentry]>0 ? -1 : 1; } + else hitsurface = p.p[entry]; + return true; +} + +extern void entselectionbox(const entity &e, vec &eo, vec &es); +float hitentdist; +int hitent, hitorient; + +static float disttoent(octaentities *oc, const vec &o, const vec &ray, float radius, int mode, extentity *t) +{ + vec eo, es; + int orient = -1; + float dist = radius, f = 0.0f; + const vector &ents = entities::getents(); + + #define entintersect(mask, type, func) {\ + if((mode&(mask))==(mask)) loopv(oc->type) \ + { \ + extentity &e = *ents[oc->type[i]]; \ + if(!(e.flags&EF_OCTA) || &e==t) continue; \ + func; \ + if(f0 && vec(ray).mul(f).add(o).insidebb(oc->o, oc->size)) \ + { \ + hitentdist = dist = f; \ + hitent = oc->type[i]; \ + hitorient = orient; \ + } \ + } \ + } + + entintersect(RAY_POLY, mapmodels, + if(!mmintersect(e, o, ray, radius, mode, f)) continue; + ); + + entintersect(RAY_ENTS, other, + entselectionbox(e, eo, es); + if(!rayboxintersect(eo, es, o, ray, f, orient)) continue; + ); + + entintersect(RAY_ENTS, mapmodels, + entselectionbox(e, eo, es); + if(!rayboxintersect(eo, es, o, ray, f, orient)) continue; + ); + + return dist; +} + +static float disttooutsideent(const vec &o, const vec &ray, float radius, int mode, extentity *t) +{ + vec eo, es; + int orient; + float dist = radius, f = 0.0f; + const vector &ents = entities::getents(); + loopv(outsideents) + { + extentity &e = *ents[outsideents[i]]; + if(!(e.flags&EF_OCTA) || &e==t) continue; + entselectionbox(e, eo, es); + if(!rayboxintersect(eo, es, o, ray, f, orient)) continue; + if(f0) + { + hitentdist = dist = f; + hitent = outsideents[i]; + hitorient = orient; + } + } + return dist; +} + +// optimized shadow version +static float shadowent(octaentities *oc, const vec &o, const vec &ray, float radius, int mode, extentity *t) +{ + float dist = radius, f = 0.0f; + const vector &ents = entities::getents(); + loopv(oc->mapmodels) + { + extentity &e = *ents[oc->mapmodels[i]]; + if(!(e.flags&EF_OCTA) || &e==t) continue; + if(!mmintersect(e, o, ray, radius, mode, f)) continue; + if(f>0 && f 0 ? radius : 1e16f; \ + vec v(o), invray(ray.x ? 1/ray.x : 1e16f, ray.y ? 1/ray.y : 1e16f, ray.z ? 1/ray.z : 1e16f); \ + cube *levels[20]; \ + levels[worldscale] = worldroot; \ + int lshift = worldscale, elvl = mode&RAY_BB ? worldscale : 0; \ + ivec lsizemask(invray.x>0 ? 1 : 0, invray.y>0 ? 1 : 0, invray.z>0 ? 1 : 0); \ + +#define CHECKINSIDEWORLD \ + if(!insideworld(o)) \ + { \ + float disttoworld = 0, exitworld = 1e16f; \ + loopi(3) \ + { \ + float c = v[i]; \ + if(c<0 || c>=worldsize) \ + { \ + float d = ((invray[i]>0?0:worldsize)-c)*invray[i]; \ + if(d<0) return (radius>0?radius:-1); \ + disttoworld = max(disttoworld, 0.1f + d); \ + } \ + float e = ((invray[i]>0?worldsize:0)-c)*invray[i]; \ + exitworld = min(exitworld, e); \ + } \ + if(disttoworld > exitworld) return (radius>0?radius:-1); \ + v.add(vec(ray).mul(disttoworld)); \ + dist += disttoworld; \ + } + +#define DOWNOCTREE(disttoent, earlyexit) \ + cube *lc = levels[lshift]; \ + for(;;) \ + { \ + lshift--; \ + lc += octastep(x, y, z, lshift); \ + if(lc->ext && lc->ext->ents && lshift < elvl) \ + { \ + float edist = disttoent(lc->ext->ents, o, ray, dent, mode, t); \ + if(edist < dent) \ + { \ + earlyexit return min(edist, dist); \ + elvl = lshift; \ + dent = min(dent, edist); \ + } \ + } \ + if(lc->children==NULL) break; \ + lc = lc->children; \ + levels[lshift] = lc; \ + } + +#define FINDCLOSEST(xclosest, yclosest, zclosest) \ + float dx = (lo.x+(lsizemask.x<= uint(worldsize)) exitworld; \ + diff >>= lshift; \ + if(!diff) exitworld; \ + do \ + { \ + lshift++; \ + diff >>= 1; \ + } while(diff); + +float raycube(const vec &o, const vec &ray, float radius, int mode, int size, extentity *t) +{ + if(ray.iszero()) return 0; + + INITRAYCUBE; + CHECKINSIDEWORLD; + + int closest = -1, x = int(v.x), y = int(v.y), z = int(v.z); + for(;;) + { + DOWNOCTREE(disttoent, if(mode&RAY_SHADOW)); + + int lsize = 1<0 || !(mode&RAY_SKIPFIRST)) && + (((mode&RAY_CLIPMAT) && isclipped(c.material&MATF_VOLUME)) || + ((mode&RAY_EDITMAT) && c.material != MAT_AIR) || + (!(mode&RAY_PASS) && lsize==size && !isempty(c)) || + isentirelysolid(c) || + dent < dist)) + { + if(closest >= 0) { hitsurface = vec(0, 0, 0); hitsurface[closest] = ray[closest]>0 ? -1 : 1; } + return min(dent, dist); + } + + ivec lo(x&(~0U<0 || !(mode&RAY_SKIPFIRST))) + return min(dent, dist+f); + } + + FINDCLOSEST(closest = 0, closest = 1, closest = 2); + + if(radius>0 && dist>=radius) return min(dent, dist); + + UPOCTREE(return min(dent, radius>0 ? radius : dist)); + } +} + +// optimized version for lightmap shadowing... every cycle here counts!!! +float shadowray(const vec &o, const vec &ray, float radius, int mode, extentity *t) +{ + INITRAYCUBE; + CHECKINSIDEWORLD; + + int side = O_BOTTOM, x = int(v.x), y = int(v.y), z = int(v.z); + for(;;) + { + DOWNOCTREE(shadowent, ); + + cube &c = *lc; + ivec lo(x&(~0U<= 0) + { + if(c.texture[side]==DEFAULT_SKY && mode&RAY_SKIPSKY) + { + if(mode&RAY_SKYTEX) return radius; + } + else return dist+max(enterdist+0.1f, 0.0f); + } + } + } + + nextcube: + FINDCLOSEST(side = O_RIGHT - lsizemask.x, side = O_FRONT - lsizemask.y, side = O_TOP - lsizemask.z); + + if(dist>=radius) return dist; + + UPOCTREE(return radius); + } +} + +// thread safe version + +struct ShadowRayCache +{ + clipplanes clipcache[MAXCLIPPLANES]; + int version; + + ShadowRayCache() : version(-1) {} +}; + +ShadowRayCache *newshadowraycache() { return new ShadowRayCache; } + +void freeshadowraycache(ShadowRayCache *&cache) { delete cache; cache = NULL; } + +void resetshadowraycache(ShadowRayCache *cache) +{ + cache->version++; + if(!cache->version) + { + memclear(cache->clipcache); + cache->version = 1; + } +} + +float shadowray(ShadowRayCache *cache, const vec &o, const vec &ray, float radius, int mode, extentity *t) +{ + INITRAYCUBE; + CHECKINSIDEWORLD; + + int side = O_BOTTOM, x = int(v.x), y = int(v.y), z = int(v.z); + for(;;) + { + DOWNOCTREE(shadowent, ); + + cube &c = *lc; + ivec lo(x&(~0U<clipcache[int(&c - worldroot)&(MAXCLIPPLANES-1)]; + if(p.owner != &c || p.version != cache->version) { p.owner = &c; p.version = cache->version; genclipplanes(c, lo, 1<= 0) + { + if(c.texture[side]==DEFAULT_SKY && mode&RAY_SKIPSKY) + { + if(mode&RAY_SKYTEX) return radius; + } + else return dist+max(enterdist+0.1f, 0.0f); + } + } + } + + nextcube: + FINDCLOSEST(side = O_RIGHT - lsizemask.x, side = O_FRONT - lsizemask.y, side = O_TOP - lsizemask.z); + + if(dist>=radius) return dist; + + UPOCTREE(return radius); + } +} + +float rayent(const vec &o, const vec &ray, float radius, int mode, int size, int &orient, int &ent) +{ + hitent = -1; + hitentdist = radius; + hitorient = -1; + float dist = raycube(o, ray, radius, mode, size); + if((mode&RAY_ENTS) == RAY_ENTS) + { + float dent = disttooutsideent(o, ray, dist < 0 ? 1e16f : dist, mode, NULL); + if(dent < 1e15f && (dist < 0 || dent < dist)) dist = dent; + } + orient = hitorient; + ent = hitentdist == dist ? hitent : -1; + return dist; +} + +float raycubepos(const vec &o, const vec &ray, vec &hitpos, float radius, int mode, int size) +{ + hitpos = ray; + float dist = raycube(o, ray, radius, mode, size); + if(radius>0 && dist>=radius) dist = radius; + hitpos.mul(dist).add(o); + return dist; +} + +bool raycubelos(const vec &o, const vec &dest, vec &hitpos) +{ + vec ray(dest); + ray.sub(o); + float mag = ray.magnitude(); + ray.mul(1/mag); + float distance = raycubepos(o, ray, hitpos, mag, RAY_CLIPMAT|RAY_POLY); + return distance >= mag; +} + +float rayfloor(const vec &o, vec &floor, int mode, float radius) +{ + if(o.z<=0) return -1; + hitsurface = vec(0, 0, 1); + float dist = raycube(o, vec(0, 0, -1), radius, mode); + if(dist<0 || (radius>0 && dist>=radius)) return dist; + floor = hitsurface; + return dist; +} + +///////////////////////// entity collision /////////////////////////////////////////////// + +// info about collisions +int collideinside; // whether an internal collision happened +physent *collideplayer; // whether the collection hit a player +vec collidewall; // just the normal vectors. + +const float STAIRHEIGHT = 4.1f; +const float FLOORZ = 0.867f; +const float SLOPEZ = 0.5f; +const float WALLZ = 0.2f; +extern const float JUMPVEL = 125.0f; +extern const float GRAVITY = 200.0f; + +bool ellipseboxcollide(physent *d, const vec &dir, const vec &o, const vec ¢er, float yaw, float xr, float yr, float hi, float lo) +{ + float below = (o.z+center.z-lo) - (d->o.z+d->aboveeye), + above = (d->o.z-d->eyeheight) - (o.z+center.z+hi); + if(below>=0 || above>=0) return false; + + vec yo(d->o); + yo.sub(o); + yo.rotate_around_z(-yaw*RAD); + yo.sub(center); + + float dx = clamp(yo.x, -xr, xr) - yo.x, dy = clamp(yo.y, -yr, yr) - yo.y, + dist = sqrtf(dx*dx + dy*dy) - d->radius; + if(dist < 0) + { + int sx = yo.x <= -xr ? -1 : (yo.x >= xr ? 1 : 0), + sy = yo.y <= -yr ? -1 : (yo.y >= yr ? 1 : 0); + if(dist > (yo.z < 0 ? below : above) && (sx || sy)) + { + vec ydir(dir); + ydir.rotate_around_z(-yaw*RAD); + if(sx*yo.x - xr > sy*yo.y - yr) + { + if(dir.iszero() || sx*ydir.x < -1e-6f) + { + collidewall = vec(sx, 0, 0); + collidewall.rotate_around_z(yaw*RAD); + return true; + } + } + else if(dir.iszero() || sy*ydir.y < -1e-6f) + { + collidewall = vec(0, sy, 0); + collidewall.rotate_around_z(yaw*RAD); + return true; + } + } + if(yo.z < 0) + { + if(dir.iszero() || (dir.z > 0 && (d->type>=ENT_INANIMATE || below >= d->zmargin-(d->eyeheight+d->aboveeye)/4.0f))) + { + collidewall = vec(0, 0, -1); + return true; + } + } + else if(dir.iszero() || (dir.z < 0 && (d->type>=ENT_INANIMATE || above >= d->zmargin-(d->eyeheight+d->aboveeye)/3.0f))) + { + collidewall = vec(0, 0, 1); + return true; + } + collideinside++; + } + return false; +} + +bool ellipsecollide(physent *d, const vec &dir, const vec &o, const vec ¢er, float yaw, float xr, float yr, float hi, float lo) +{ + float below = (o.z+center.z-lo) - (d->o.z+d->aboveeye), + above = (d->o.z-d->eyeheight) - (o.z+center.z+hi); + if(below>=0 || above>=0) return false; + vec yo(center); + yo.rotate_around_z(yaw*RAD); + yo.add(o); + float x = yo.x - d->o.x, y = yo.y - d->o.y; + float angle = atan2f(y, x), dangle = angle-(d->yaw+90)*RAD, eangle = angle-(yaw+90)*RAD; + float dx = d->xradius*cosf(dangle), dy = d->yradius*sinf(dangle); + float ex = xr*cosf(eangle), ey = yr*sinf(eangle); + float dist = sqrtf(x*x + y*y) - sqrtf(dx*dx + dy*dy) - sqrtf(ex*ex + ey*ey); + if(dist < 0) + { + if(dist > (d->o.z < yo.z ? below : above) && (dir.iszero() || x*dir.x + y*dir.y > 0)) + { + collidewall = vec(-x, -y, 0); + if(!collidewall.iszero()) collidewall.normalize(); + return true; + } + if(d->o.z < yo.z) + { + if(dir.iszero() || (dir.z > 0 && (d->type>=ENT_INANIMATE || below >= d->zmargin-(d->eyeheight+d->aboveeye)/4.0f))) + { + collidewall = vec(0, 0, -1); + return true; + } + } + else if(dir.iszero() || (dir.z < 0 && (d->type>=ENT_INANIMATE || above >= d->zmargin-(d->eyeheight+d->aboveeye)/3.0f))) + { + collidewall = vec(0, 0, 1); + return true; + } + collideinside++; + } + return false; +} + +#define DYNENTCACHESIZE 1024 + +static uint dynentframe = 0; + +static struct dynentcacheentry +{ + int x, y; + uint frame; + vector dynents; +} dynentcache[DYNENTCACHESIZE]; + +void cleardynentcache() +{ + dynentframe++; + if(!dynentframe || dynentframe == 1) loopi(DYNENTCACHESIZE) dynentcache[i].frame = 0; + if(!dynentframe) dynentframe = 1; +} + +VARF(dynentsize, 4, 7, 12, cleardynentcache()); + +#define DYNENTHASH(x, y) (((((x)^(y))<<5) + (((x)^(y))>>5)) & (DYNENTCACHESIZE - 1)) + +const vector &checkdynentcache(int x, int y) +{ + dynentcacheentry &dec = dynentcache[DYNENTHASH(x, y)]; + if(dec.x == x && dec.y == y && dec.frame == dynentframe) return dec.dynents; + dec.x = x; + dec.y = y; + dec.frame = dynentframe; + dec.dynents.shrink(0); + int numdyns = game::numdynents(), dsize = 1<state != CS_ALIVE || + d->o.x+d->radius <= dx || d->o.x-d->radius >= dx+dsize || + d->o.y+d->radius <= dy || d->o.y-d->radius >= dy+dsize) + continue; + dec.dynents.add(d); + } + return dec.dynents; +} + +#define loopdynentcache(curx, cury, o, radius) \ + for(int curx = max(int(o.x-radius), 0)>>dynentsize, endx = min(int(o.x+radius), worldsize-1)>>dynentsize; curx <= endx; curx++) \ + for(int cury = max(int(o.y-radius), 0)>>dynentsize, endy = min(int(o.y+radius), worldsize-1)>>dynentsize; cury <= endy; cury++) + +void updatedynentcache(physent *d) +{ + loopdynentcache(x, y, d->o, d->radius) + { + dynentcacheentry &dec = dynentcache[DYNENTHASH(x, y)]; + if(dec.x != x || dec.y != y || dec.frame != dynentframe || dec.dynents.find(d) >= 0) continue; + dec.dynents.add(d); + } +} + +bool overlapsdynent(const vec &o, float radius) +{ + loopdynentcache(x, y, o, radius) + { + const vector &dynents = checkdynentcache(x, y); + loopv(dynents) + { + physent *d = dynents[i]; + if(o.dist(d->o)-d->radius < radius) return true; + } + } + return false; +} + +template +static inline bool plcollide(physent *d, const vec &dir, physent *o) +{ + E entvol(d); + O obvol(o); + vec cp; + if(mpr::collide(entvol, obvol, NULL, NULL, &cp)) + { + vec wn = vec(cp).sub(obvol.center()); + collidewall = obvol.contactface(wn, dir.iszero() ? vec(wn).neg() : dir); + if(!collidewall.iszero()) return true; + collideinside++; + } + return false; +} + +static inline bool plcollide(physent *d, const vec &dir, physent *o) +{ + switch(d->collidetype) + { + case COLLIDE_ELLIPSE: + case COLLIDE_ELLIPSE_PRECISE: + if(o->collidetype == COLLIDE_OBB) return ellipseboxcollide(d, dir, o->o, vec(0, 0, 0), o->yaw, o->xradius, o->yradius, o->aboveeye, o->eyeheight); + else return ellipsecollide(d, dir, o->o, vec(0, 0, 0), o->yaw, o->xradius, o->yradius, o->aboveeye, o->eyeheight); + case COLLIDE_OBB: + if(o->collidetype == COLLIDE_OBB) return plcollide(d, dir, o); + else return plcollide(d, dir, o); + default: return false; + } +} + +bool plcollide(physent *d, const vec &dir, bool insideplayercol) // collide with player or monster +{ + if(d->type==ENT_CAMERA || d->state!=CS_ALIVE) return false; + int lastinside = collideinside; + physent *insideplayer = NULL; + loopdynentcache(x, y, d->o, d->radius) + { + const vector &dynents = checkdynentcache(x, y); + loopv(dynents) + { + physent *o = dynents[i]; + if(o==d || d->o.reject(o->o, d->radius+o->radius)) continue; + if(plcollide(d, dir, o)) + { + collideplayer = o; + game::dynentcollide(d, o, collidewall); + return true; + } + if(collideinside > lastinside) + { + lastinside = collideinside; + insideplayer = o; + } + } + } + if(insideplayer && insideplayercol) + { + collideplayer = insideplayer; + game::dynentcollide(d, insideplayer, vec(0, 0, 0)); + return true; + } + return false; +} + +void rotatebb(vec ¢er, vec &radius, int yaw) +{ + if(yaw < 0) yaw = 360 + yaw%360; + else if(yaw >= 360) yaw %= 360; + const vec2 &rot = sincos360[yaw]; + vec2 oldcenter(center), oldradius(radius); + center.x = oldcenter.x*rot.x - oldcenter.y*rot.y; + center.y = oldcenter.y*rot.x + oldcenter.x*rot.y; + radius.x = fabs(oldradius.x*rot.x) + fabs(oldradius.y*rot.y); + radius.y = fabs(oldradius.y*rot.x) + fabs(oldradius.x*rot.y); +} + +template +static inline bool mmcollide(physent *d, const vec &dir, const extentity &e, const vec ¢er, const vec &radius, float yaw) +{ + E entvol(d); + M mdlvol(e.o, center, radius, yaw); + vec cp; + if(mpr::collide(entvol, mdlvol, NULL, NULL, &cp)) + { + vec wn = vec(cp).sub(mdlvol.center()); + collidewall = mdlvol.contactface(wn, dir.iszero() ? vec(wn).neg() : dir); + if(!collidewall.iszero()) return true; + collideinside++; + } + return false; +} + +bool mmcollide(physent *d, const vec &dir, octaentities &oc) // collide with a mapmodel +{ + const vector &ents = entities::getents(); + loopv(oc.mapmodels) + { + extentity &e = *ents[oc.mapmodels[i]]; + if(e.flags&EF_NOCOLLIDE) continue; + model *m = loadmapmodel(e.attr2); + if(!m || !m->collide) continue; + + vec center, radius; + float rejectradius = m->collisionbox(center, radius); + if(d->o.reject(e.o, d->radius + rejectradius)) continue; + + float yaw = e.attr1; + switch(d->collidetype) + { + case COLLIDE_ELLIPSE: + case COLLIDE_ELLIPSE_PRECISE: + if(m->ellipsecollide) + { + if(ellipsecollide(d, dir, e.o, center, yaw, radius.x, radius.y, radius.z, radius.z)) return true; + } + else if(ellipseboxcollide(d, dir, e.o, center, yaw, radius.x, radius.y, radius.z, radius.z)) return true; + break; + case COLLIDE_OBB: + if(m->ellipsecollide) + { + if(mmcollide(d, dir, e, center, radius, yaw)) return true; + } + else if(mmcollide(d, dir, e, center, radius, yaw)) return true; + break; + default: continue; + } + } + return false; +} + +template +static bool fuzzycollidesolid(physent *d, const vec &dir, float cutoff, const cube &c, const ivec &co, int size) // collide with solid cube geometry +{ + int crad = size/2; + if(fabs(d->o.x - co.x - crad) > d->radius + crad || fabs(d->o.y - co.y - crad) > d->radius + crad || + d->o.z + d->aboveeye < co.z || d->o.z - d->eyeheight > co.z + size) + return false; + + E entvol(d); + collidewall = vec(0, 0, 0); + float bestdist = -1e10f; + int visible = isentirelysolid(c) ? c.visible : 0xFF; + #define CHECKSIDE(side, distval, dotval, margin, normal) if(visible&(1< 0) return false; \ + if(dist <= bestdist) continue; \ + if(!dir.iszero()) \ + { \ + if(dotval >= -cutoff*dir.magnitude()) continue; \ + if(d->typeo.x + d->radius), -dir.x, -d->radius, vec(-1, 0, 0)); + CHECKSIDE(O_RIGHT, d->o.x - d->radius - (co.x + size), dir.x, -d->radius, vec(1, 0, 0)); + CHECKSIDE(O_BACK, co.y - (d->o.y + d->radius), -dir.y, -d->radius, vec(0, -1, 0)); + CHECKSIDE(O_FRONT, d->o.y - d->radius - (co.y + size), dir.y, -d->radius, vec(0, 1, 0)); + CHECKSIDE(O_BOTTOM, co.z - (d->o.z + d->aboveeye), -dir.z, d->zmargin-(d->eyeheight+d->aboveeye)/4.0f, vec(0, 0, -1)); + CHECKSIDE(O_TOP, d->o.z - d->eyeheight - (co.z + size), dir.z, d->zmargin-(d->eyeheight+d->aboveeye)/3.0f, vec(0, 0, 1)); + + if(collidewall.iszero()) + { + collideinside++; + return false; + } + return true; +} + +template +static inline bool clampcollide(const clipplanes &p, const E &entvol, const plane &w, const vec &pw) +{ + if(w.x && (w.y || w.z) && fabs(pw.x - p.o.x) > p.r.x) + { + vec c = entvol.center(); + float fv = pw.x < p.o.x ? p.o.x-p.r.x : p.o.x+p.r.x, fdist = (w.x*fv + w.y*c.y + w.z*c.z + w.offset) / (w.y*w.y + w.z*w.z); + vec fdir(fv - c.x, -w.y*fdist, -w.z*fdist); + if((pw.y-c.y-fdir.y)*w.y + (pw.z-c.z-fdir.z)*w.z >= 0 && entvol.supportpoint(fdir).squaredist(c) < fdir.squaredlen()) return true; + } + if(w.y && (w.x || w.z) && fabs(pw.y - p.o.y) > p.r.y) + { + vec c = entvol.center(); + float fv = pw.y < p.o.y ? p.o.y-p.r.y : p.o.y+p.r.y, fdist = (w.x*c.x + w.y*fv + w.z*c.z + w.offset) / (w.x*w.x + w.z*w.z); + vec fdir(-w.x*fdist, fv - c.y, -w.z*fdist); + if((pw.x-c.x-fdir.x)*w.x + (pw.z-c.z-fdir.z)*w.z >= 0 && entvol.supportpoint(fdir).squaredist(c) < fdir.squaredlen()) return true; + } + if(w.z && (w.x || w.y) && fabs(pw.z - p.o.z) > p.r.z) + { + vec c = entvol.center(); + float fv = pw.z < p.o.z ? p.o.z-p.r.z : p.o.z+p.r.z, fdist = (w.x*c.x + w.y*c.y + w.z*fv + w.offset) / (w.x*w.x + w.y*w.y); + vec fdir(-w.x*fdist, -w.y*fdist, fv - c.z); + if((pw.x-c.x-fdir.x)*w.x + (pw.y-c.y-fdir.y)*w.y >= 0 && entvol.supportpoint(fdir).squaredist(c) < fdir.squaredlen()) return true; + } + return false; +} + +template +static bool fuzzycollideplanes(physent *d, const vec &dir, float cutoff, const cube &c, const ivec &co, int size) // collide with deformed cube geometry +{ + const clipplanes &p = getclipplanes(c, co, size); + + if(fabs(d->o.x - p.o.x) > p.r.x + d->radius || fabs(d->o.y - p.o.y) > p.r.y + d->radius || + d->o.z + d->aboveeye < p.o.z - p.r.z || d->o.z - d->eyeheight > p.o.z + p.r.z) + return false; + + collidewall = vec(0, 0, 0); + float bestdist = -1e10f; + int visible = p.visible; + CHECKSIDE(O_LEFT, p.o.x - p.r.x - (d->o.x + d->radius), -dir.x, -d->radius, vec(-1, 0, 0)); + CHECKSIDE(O_RIGHT, d->o.x - d->radius - (p.o.x + p.r.x), dir.x, -d->radius, vec(1, 0, 0)); + CHECKSIDE(O_BACK, p.o.y - p.r.y - (d->o.y + d->radius), -dir.y, -d->radius, vec(0, -1, 0)); + CHECKSIDE(O_FRONT, d->o.y - d->radius - (p.o.y + p.r.y), dir.y, -d->radius, vec(0, 1, 0)); + CHECKSIDE(O_BOTTOM, p.o.z - p.r.z - (d->o.z + d->aboveeye), -dir.z, d->zmargin-(d->eyeheight+d->aboveeye)/4.0f, vec(0, 0, -1)); + CHECKSIDE(O_TOP, d->o.z - d->eyeheight - (p.o.z + p.r.z), dir.z, d->zmargin-(d->eyeheight+d->aboveeye)/3.0f, vec(0, 0, 1)); + + E entvol(d); + int bestplane = -1; + loopi(p.size) + { + const plane &w = p.p[i]; + vec pw = entvol.supportpoint(vec(w).neg()); + float dist = w.dist(pw); + if(dist >= 0) return false; + if(dist <= bestdist) continue; + bestplane = -1; + bestdist = dist; + if(!dir.iszero()) + { + if(w.dot(dir) >= -cutoff*dir.magnitude()) continue; + if(d->typezmargin-(d->eyeheight+d->aboveeye)/(dir.z < 0 ? 3.0f : 4.0f) : + ((dir.x*w.x < 0 || dir.y*w.y < 0) ? -d->radius : 0))) + continue; + } + if(clampcollide(p, entvol, w, pw)) continue; + bestplane = i; + } + if(bestplane >= 0) collidewall = p.p[bestplane]; + else if(collidewall.iszero()) + { + collideinside++; + return false; + } + return true; +} + +template +static bool cubecollidesolid(physent *d, const vec &dir, float cutoff, const cube &c, const ivec &co, int size) // collide with solid cube geometry +{ + int crad = size/2; + if(fabs(d->o.x - co.x - crad) > d->radius + crad || fabs(d->o.y - co.y - crad) > d->radius + crad || + d->o.z + d->aboveeye < co.z || d->o.z - d->eyeheight > co.z + size) + return false; + + E entvol(d); + bool collided = mpr::collide(mpr::SolidCube(co, size), entvol); + if(!collided) return false; + + collidewall = vec(0, 0, 0); + float bestdist = -1e10f; + int visible = isentirelysolid(c) ? c.visible : 0xFF; + CHECKSIDE(O_LEFT, co.x - entvol.right(), -dir.x, -d->radius, vec(-1, 0, 0)); + CHECKSIDE(O_RIGHT, entvol.left() - (co.x + size), dir.x, -d->radius, vec(1, 0, 0)); + CHECKSIDE(O_BACK, co.y - entvol.front(), -dir.y, -d->radius, vec(0, -1, 0)); + CHECKSIDE(O_FRONT, entvol.back() - (co.y + size), dir.y, -d->radius, vec(0, 1, 0)); + CHECKSIDE(O_BOTTOM, co.z - entvol.top(), -dir.z, d->zmargin-(d->eyeheight+d->aboveeye)/4.0f, vec(0, 0, -1)); + CHECKSIDE(O_TOP, entvol.bottom() - (co.z + size), dir.z, d->zmargin-(d->eyeheight+d->aboveeye)/3.0f, vec(0, 0, 1)); + + if(collidewall.iszero()) + { + collideinside++; + return false; + } + return true; +} + +template +static bool cubecollideplanes(physent *d, const vec &dir, float cutoff, const cube &c, const ivec &co, int size) // collide with deformed cube geometry +{ + const clipplanes &p = getclipplanes(c, co, size); + + if(fabs(d->o.x - p.o.x) > p.r.x + d->radius || fabs(d->o.y - p.o.y) > p.r.y + d->radius || + d->o.z + d->aboveeye < p.o.z - p.r.z || d->o.z - d->eyeheight > p.o.z + p.r.z) + return false; + + E entvol(d); + bool collided = mpr::collide(mpr::CubePlanes(p), entvol); + if(!collided) return false; + + collidewall = vec(0, 0, 0); + float bestdist = -1e10f; + int visible = p.visible; + CHECKSIDE(O_LEFT, p.o.x - p.r.x - entvol.right(), -dir.x, -d->radius, vec(-1, 0, 0)); + CHECKSIDE(O_RIGHT, entvol.left() - (p.o.x + p.r.x), dir.x, -d->radius, vec(1, 0, 0)); + CHECKSIDE(O_BACK, p.o.y - p.r.y - entvol.front(), -dir.y, -d->radius, vec(0, -1, 0)); + CHECKSIDE(O_FRONT, entvol.back() - (p.o.y + p.r.y), dir.y, -d->radius, vec(0, 1, 0)); + CHECKSIDE(O_BOTTOM, p.o.z - p.r.z - entvol.top(), -dir.z, d->zmargin-(d->eyeheight+d->aboveeye)/4.0f, vec(0, 0, -1)); + CHECKSIDE(O_TOP, entvol.bottom() - (p.o.z + p.r.z), dir.z, d->zmargin-(d->eyeheight+d->aboveeye)/3.0f, vec(0, 0, 1)); + + int bestplane = -1; + loopi(p.size) + { + const plane &w = p.p[i]; + vec pw = entvol.supportpoint(vec(w).neg()); + float dist = w.dist(pw); + if(dist <= bestdist) continue; + bestplane = -1; + bestdist = dist; + if(!dir.iszero()) + { + if(w.dot(dir) >= -cutoff*dir.magnitude()) continue; + if(d->typezmargin-(d->eyeheight+d->aboveeye)/(dir.z < 0 ? 3.0f : 4.0f) : + ((dir.x*w.x < 0 || dir.y*w.y < 0) ? -d->radius : 0))) + continue; + } + if(clampcollide(p, entvol, w, pw)) continue; + bestplane = i; + } + if(bestplane >= 0) collidewall = p.p[bestplane]; + else if(collidewall.iszero()) + { + collideinside++; + return false; + } + return true; +} + +static inline bool cubecollide(physent *d, const vec &dir, float cutoff, const cube &c, const ivec &co, int size, bool solid) +{ + switch(d->collidetype) + { + case COLLIDE_OBB: + if(isentirelysolid(c) || solid) return cubecollidesolid(d, dir, cutoff, c, co, size); + else return cubecollideplanes(d, dir, cutoff, c, co, size); + case COLLIDE_ELLIPSE: + if(isentirelysolid(c) || solid) return fuzzycollidesolid(d, dir, cutoff, c, co, size); + else return fuzzycollideplanes(d, dir, cutoff, c, co, size); + case COLLIDE_ELLIPSE_PRECISE: + if(isentirelysolid(c) || solid) return cubecollidesolid(d, dir, cutoff, c, co, size); + else return cubecollideplanes(d, dir, cutoff, c, co, size); + default: return false; + } +} + +static inline bool octacollide(physent *d, const vec &dir, float cutoff, const ivec &bo, const ivec &bs, const cube *c, const ivec &cor, int size) // collide with octants +{ + loopoctabox(cor, size, bo, bs) + { + if(c[i].ext && c[i].ext->ents) if(mmcollide(d, dir, *c[i].ext->ents)) return true; + ivec o(i, cor, size); + if(c[i].children) + { + if(octacollide(d, dir, cutoff, bo, bs, c[i].children, o, size>>1)) return true; + } + else + { + bool solid = false; + switch(c[i].material&MATF_CLIP) + { + case MAT_NOCLIP: continue; + case MAT_GAMECLIP: if(d->type==ENT_AI) solid = true; break; + case MAT_CLIP: if(isclipped(c[i].material&MATF_VOLUME) || d->type= uint(worldsize)) + return octacollide(d, dir, cutoff, bo, bs, worldroot, ivec(0, 0, 0), worldsize>>1); + const cube *c = &worldroot[octastep(bo.x, bo.y, bo.z, scale)]; + if(c->ext && c->ext->ents && mmcollide(d, dir, *c->ext->ents)) return true; + scale--; + while(c->children && !(diff&(1<children[octastep(bo.x, bo.y, bo.z, scale)]; + if(c->ext && c->ext->ents && mmcollide(d, dir, *c->ext->ents)) return true; + scale--; + } + if(c->children) return octacollide(d, dir, cutoff, bo, bs, c->children, ivec(bo).mask(~((2<material&MATF_CLIP) + { + case MAT_NOCLIP: return false; + case MAT_GAMECLIP: if(d->type==ENT_AI) solid = true; break; + case MAT_CLIP: if(isclipped(c->material&MATF_VOLUME) || d->typeo.x-d->radius), int(d->o.y-d->radius), int(d->o.z-d->eyeheight)), + bs(int(d->o.x+d->radius), int(d->o.y+d->radius), int(d->o.z+d->aboveeye)); + bs.add(1); // guard space for rounding errors + return octacollide(d, dir, cutoff, bo, bs) || (playercol && plcollide(d, dir, insideplayercol)); +} + +void recalcdir(physent *d, const vec &oldvel, vec &dir) +{ + float speed = oldvel.magnitude(); + if(speed > 1e-6f) + { + float step = dir.magnitude(); + dir = d->vel; + dir.add(d->falling); + dir.mul(step/speed); + } +} + +void slideagainst(physent *d, vec &dir, const vec &obstacle, bool foundfloor, bool slidecollide) +{ + vec wall(obstacle); + if(foundfloor ? wall.z > 0 : slidecollide) + { + wall.z = 0; + if(!wall.iszero()) wall.normalize(); + } + vec oldvel(d->vel); + oldvel.add(d->falling); + d->vel.project(wall); + d->falling.project(wall); + recalcdir(d, oldvel, dir); +} + +void switchfloor(physent *d, vec &dir, const vec &floor) +{ + if(floor.z >= FLOORZ) d->falling = vec(0, 0, 0); + + vec oldvel(d->vel); + oldvel.add(d->falling); + if(dir.dot(floor) >= 0) + { + if(d->physstate < PHYS_SLIDE || fabs(dir.dot(d->floor)) > 0.01f*dir.magnitude()) return; + d->vel.projectxy(floor, 0.0f); + } + else d->vel.projectxy(floor); + d->falling.project(floor); + recalcdir(d, oldvel, dir); +} + +bool trystepup(physent *d, vec &dir, const vec &obstacle, float maxstep, const vec &floor) +{ + vec old(d->o), stairdir = (obstacle.z >= 0 && obstacle.z < SLOPEZ ? vec(-obstacle.x, -obstacle.y, 0) : vec(dir.x, dir.y, 0)).rescale(1); + bool cansmooth = true; + /* check if there is space atop the stair to move to */ + if(d->physstate != PHYS_STEP_UP) + { + vec checkdir = stairdir; + checkdir.mul(0.1f); + checkdir.z += maxstep + 0.1f; + d->o.add(checkdir); + if(collide(d)) + { + d->o = old; + if(!collide(d, vec(0, 0, -1), SLOPEZ)) return false; + cansmooth = false; + } + } + + if(cansmooth) + { + vec checkdir = stairdir; + checkdir.z += 1; + checkdir.mul(maxstep); + d->o = old; + d->o.add(checkdir); + int scale = 2; + if(collide(d, checkdir)) + { + if(!collide(d, vec(0, 0, -1), SLOPEZ)) + { + d->o = old; + return false; + } + d->o.add(checkdir); + if(collide(d, vec(0, 0, -1), SLOPEZ)) scale = 1; + } + if(scale != 1) + { + d->o = old; + d->o.sub(checkdir.mul(vec(2, 2, 1))); + if(!collide(d, vec(0, 0, -1), SLOPEZ)) scale = 1; + } + + d->o = old; + vec smoothdir(dir.x, dir.y, 0); + float magxy = smoothdir.magnitude(); + if(magxy > 1e-9f) + { + if(magxy > scale*dir.z) + { + smoothdir.mul(1/magxy); + smoothdir.z = 1.0f/scale; + smoothdir.mul(dir.magnitude()/smoothdir.magnitude()); + } + else smoothdir.z = dir.z; + d->o.add(smoothdir); + d->o.z += maxstep + 0.1f; + if(!collide(d, smoothdir)) + { + d->o.z -= maxstep + 0.1f; + if(d->physstate == PHYS_FALL || d->floor != floor) + { + d->timeinair = 0; + d->floor = floor; + switchfloor(d, dir, d->floor); + } + d->physstate = PHYS_STEP_UP; + return true; + } + } + } + + /* try stepping up */ + d->o = old; + d->o.z += dir.magnitude(); + if(!collide(d, vec(0, 0, 1))) + { + if(d->physstate == PHYS_FALL || d->floor != floor) + { + d->timeinair = 0; + d->floor = floor; + switchfloor(d, dir, d->floor); + } + if(cansmooth) d->physstate = PHYS_STEP_UP; + return true; + } + d->o = old; + return false; +} + +bool trystepdown(physent *d, vec &dir, float step, float xy, float z, bool init = false) +{ + vec stepdir(dir.x, dir.y, 0); + stepdir.z = -stepdir.magnitude2()*z/xy; + if(!stepdir.z) return false; + stepdir.normalize(); + + vec old(d->o); + d->o.add(vec(stepdir).mul(STAIRHEIGHT/fabs(stepdir.z))).z -= STAIRHEIGHT; + d->zmargin = -STAIRHEIGHT; + if(collide(d, vec(0, 0, -1), SLOPEZ)) + { + d->o = old; + d->o.add(vec(stepdir).mul(step)); + d->zmargin = 0; + if(!collide(d, vec(0, 0, -1))) + { + vec stepfloor(stepdir); + stepfloor.mul(-stepfloor.z).z += 1; + stepfloor.normalize(); + if(d->physstate >= PHYS_SLOPE && d->floor != stepfloor) + { + // prevent alternating step-down/step-up states if player would keep bumping into the same floor + vec stepped(d->o); + d->o.z -= 0.5f; + d->zmargin = -0.5f; + if(collide(d, stepdir) && collidewall == d->floor) + { + d->o = old; + if(!init) { d->o.x += dir.x; d->o.y += dir.y; if(dir.z <= 0 || collide(d, dir)) d->o.z += dir.z; } + d->zmargin = 0; + d->physstate = PHYS_STEP_DOWN; + d->timeinair = 0; + return true; + } + d->o = init ? old : stepped; + d->zmargin = 0; + } + else if(init) d->o = old; + switchfloor(d, dir, stepfloor); + d->floor = stepfloor; + d->physstate = PHYS_STEP_DOWN; + d->timeinair = 0; + return true; + } + } + d->o = old; + d->zmargin = 0; + return false; +} + +bool trystepdown(physent *d, vec &dir, bool init = false) +{ + if((!d->move && !d->strafe) || !game::allowmove(d)) return false; + vec old(d->o); + d->o.z -= STAIRHEIGHT; + d->zmargin = -STAIRHEIGHT; + if(!collide(d, vec(0, 0, -1), SLOPEZ)) + { + d->o = old; + d->zmargin = 0; + return false; + } + d->o = old; + d->zmargin = 0; + float step = dir.magnitude(); +#if 1 + // weaker check, just enough to avoid hopping up slopes + if(trystepdown(d, dir, step, 4, 1, init)) return true; +#else + if(trystepdown(d, dir, step, 2, 1, init)) return true; + if(trystepdown(d, dir, step, 1, 1, init)) return true; + if(trystepdown(d, dir, step, 1, 2, init)) return true; +#endif + return false; +} + +void falling(physent *d, vec &dir, const vec &floor) +{ + if(floor.z > 0.0f && floor.z < SLOPEZ) + { + if(floor.z >= WALLZ) switchfloor(d, dir, floor); + d->timeinair = 0; + d->physstate = PHYS_SLIDE; + d->floor = floor; + } + else if(d->physstate < PHYS_SLOPE || dir.dot(d->floor) > 0.01f*dir.magnitude() || (floor.z != 0.0f && floor.z != 1.0f) || !trystepdown(d, dir, true)) + d->physstate = PHYS_FALL; +} + +void landing(physent *d, vec &dir, const vec &floor, bool collided) +{ +#if 0 + if(d->physstate == PHYS_FALL) + { + d->timeinair = 0; + if(dir.z < 0.0f) dir.z = d->vel.z = 0.0f; + } +#endif + switchfloor(d, dir, floor); + d->timeinair = 0; + if((d->physstate!=PHYS_STEP_UP && d->physstate!=PHYS_STEP_DOWN) || !collided) + d->physstate = floor.z >= FLOORZ ? PHYS_FLOOR : PHYS_SLOPE; + d->floor = floor; +} + +bool findfloor(physent *d, bool collided, const vec &obstacle, bool &slide, vec &floor) +{ + bool found = false; + vec moved(d->o); + d->o.z -= 0.1f; + if(collide(d, vec(0, 0, -1), d->physstate == PHYS_SLOPE || d->physstate == PHYS_STEP_DOWN ? SLOPEZ : FLOORZ)) + { + floor = collidewall; + found = true; + } + else if(collided && obstacle.z >= SLOPEZ) + { + floor = obstacle; + found = true; + slide = false; + } + else if(d->physstate == PHYS_STEP_UP || d->physstate == PHYS_SLIDE) + { + if(collide(d, vec(0, 0, -1)) && collidewall.z > 0.0f) + { + floor = collidewall; + if(floor.z >= SLOPEZ) found = true; + } + } + else if(d->physstate >= PHYS_SLOPE && d->floor.z < 1.0f) + { + if(collide(d, vec(d->floor).neg(), 0.95f) || collide(d, vec(0, 0, -1))) + { + floor = collidewall; + if(floor.z >= SLOPEZ && floor.z < 1.0f) found = true; + } + } + if(collided && (!found || obstacle.z > floor.z)) + { + floor = obstacle; + slide = !found && (floor.z < WALLZ || floor.z >= SLOPEZ); + } + d->o = moved; + return found; +} + +bool move(physent *d, vec &dir) +{ + vec old(d->o); + bool collided = false, slidecollide = false; + vec obstacle; + d->o.add(dir); + if(collide(d, dir) || ((d->type==ENT_AI || d->type==ENT_INANIMATE) && collide(d, vec(0, 0, 0), 0, false))) + { + obstacle = collidewall; + /* check to see if there is an obstacle that would prevent this one from being used as a floor (or ceiling bump) */ + if(d->type==ENT_PLAYER && ((collidewall.z>=SLOPEZ && dir.z<0) || (collidewall.z<=-SLOPEZ && dir.z>0)) && (dir.x || dir.y) && collide(d, vec(dir.x, dir.y, 0))) + { + if(collidewall.dot(dir) >= 0) slidecollide = true; + obstacle = collidewall; + } + d->o = old; + d->o.z -= STAIRHEIGHT; + d->zmargin = -STAIRHEIGHT; + if(d->physstate == PHYS_SLOPE || d->physstate == PHYS_FLOOR || (collide(d, vec(0, 0, -1), SLOPEZ) && (d->physstate==PHYS_STEP_UP || d->physstate==PHYS_STEP_DOWN || collidewall.z>=FLOORZ))) + { + d->o = old; + d->zmargin = 0; + if(trystepup(d, dir, obstacle, STAIRHEIGHT, d->physstate == PHYS_SLOPE || d->physstate == PHYS_FLOOR ? d->floor : vec(collidewall))) return true; + } + else + { + d->o = old; + d->zmargin = 0; + } + /* can't step over the obstacle, so just slide against it */ + collided = true; + } + else if(d->physstate == PHYS_STEP_UP) + { + if(collide(d, vec(0, 0, -1), SLOPEZ)) + { + d->o = old; + if(trystepup(d, dir, vec(0, 0, 1), STAIRHEIGHT, vec(collidewall))) return true; + d->o.add(dir); + } + } + else if(d->physstate == PHYS_STEP_DOWN && dir.dot(d->floor) <= 1e-6f) + { + vec moved(d->o); + d->o = old; + if(trystepdown(d, dir)) return true; + d->o = moved; + } + vec floor(0, 0, 0); + bool slide = collided, + found = findfloor(d, collided, obstacle, slide, floor); + if(slide || (!collided && floor.z > 0 && floor.z < WALLZ)) + { + slideagainst(d, dir, slide ? obstacle : floor, found, slidecollide); + //if(d->type == ENT_AI || d->type == ENT_INANIMATE) + d->blocked = true; + } + if(found) landing(d, dir, floor, collided); + else falling(d, dir, floor); + return !collided; +} + +bool bounce(physent *d, float secs, float elasticity, float waterfric, float grav) +{ + // make sure bouncers don't start inside geometry + if(d->physstate!=PHYS_BOUNCE && collide(d, vec(0, 0, 0), 0, false)) return true; + int mat = lookupmaterial(vec(d->o.x, d->o.y, d->o.z + (d->aboveeye - d->eyeheight)/2)); + bool water = isliquid(mat); + if(water) + { + d->vel.z -= grav*GRAVITY/16*secs; + d->vel.mul(max(1.0f - secs/waterfric, 0.0f)); + } + else d->vel.z -= grav*GRAVITY*secs; + vec old(d->o); + loopi(2) + { + vec dir(d->vel); + dir.mul(secs); + d->o.add(dir); + if(!collide(d, dir, 0, true, true)) + { + if(collideinside) + { + d->o = old; + d->vel.mul(-elasticity); + } + break; + } + else if(collideplayer) break; + d->o = old; + game::bounced(d, collidewall); + float c = collidewall.dot(d->vel), + k = 1.0f + (1.0f-elasticity)*c/d->vel.magnitude(); + d->vel.mul(k); + d->vel.sub(vec(collidewall).mul(elasticity*2.0f*c)); + } + if(d->physstate!=PHYS_BOUNCE) + { + // make sure bouncers don't start inside geometry + if(d->o == old) return !collideplayer; + d->physstate = PHYS_BOUNCE; + } + return collideplayer!=NULL; +} + +void avoidcollision(physent *d, const vec &dir, physent *obstacle, float space) +{ + float rad = obstacle->radius+d->radius; + vec bbmin(obstacle->o); + bbmin.x -= rad; + bbmin.y -= rad; + bbmin.z -= obstacle->eyeheight+d->aboveeye; + bbmin.sub(space); + vec bbmax(obstacle->o); + bbmax.x += rad; + bbmax.y += rad; + bbmax.z += obstacle->aboveeye+d->eyeheight; + bbmax.add(space); + + loopi(3) if(d->o[i] <= bbmin[i] || d->o[i] >= bbmax[i]) return; + + float mindist = 1e16f; + loopi(3) if(dir[i] != 0) + { + float dist = ((dir[i] > 0 ? bbmax[i] : bbmin[i]) - d->o[i]) / dir[i]; + mindist = min(mindist, dist); + } + if(mindist >= 0.0f && mindist < 1e15f) d->o.add(vec(dir).mul(mindist)); +} + +bool movecamera(physent *pl, const vec &dir, float dist, float stepdist) +{ + int steps = (int)ceil(dist/stepdist); + if(steps <= 0) return true; + + vec d(dir); + d.mul(dist/steps); + loopi(steps) + { + vec oldpos(pl->o); + pl->o.add(d); + if(collide(pl, vec(0, 0, 0), 0, false)) + { + pl->o = oldpos; + return false; + } + } + return true; +} + +bool droptofloor(vec &o, float radius, float height) +{ + static struct dropent : physent + { + dropent() + { + type = ENT_BOUNCE; + vel = vec(0, 0, -1); + } + } d; + d.o = o; + if(!insideworld(d.o)) + { + if(d.o.z < worldsize) return false; + d.o.z = worldsize - 1e-3f; + if(!insideworld(d.o)) return false; + } + vec v(0.0001f, 0.0001f, -1); + v.normalize(); + if(raycube(d.o, v, worldsize) >= worldsize) return false; + d.radius = d.xradius = d.yradius = radius; + d.eyeheight = height; + d.aboveeye = radius; + if(!movecamera(&d, d.vel, worldsize, 1)) + { + o = d.o; + return true; + } + return false; +} + +float dropheight(entity &e) +{ + switch(e.type) + { + case ET_PARTICLES: + case ET_MAPMODEL: return 0.0f; + default: + if(e.type >= ET_GAMESPECIFIC) return entities::dropheight(e); + return 4.0f; + } +} + +void dropenttofloor(entity *e) +{ + droptofloor(e->o, 1.0f, dropheight(*e)); +} + +void phystest() +{ + static const char * const states[] = {"float", "fall", "slide", "slope", "floor", "step up", "step down", "bounce"}; + printf ("PHYS(pl): %s, air %d, floor: (%f, %f, %f), vel: (%f, %f, %f), g: (%f, %f, %f)\n", states[player->physstate], player->timeinair, player->floor.x, player->floor.y, player->floor.z, player->vel.x, player->vel.y, player->vel.z, player->falling.x, player->falling.y, player->falling.z); + printf ("PHYS(cam): %s, air %d, floor: (%f, %f, %f), vel: (%f, %f, %f), g: (%f, %f, %f)\n", states[camera1->physstate], camera1->timeinair, camera1->floor.x, camera1->floor.y, camera1->floor.z, camera1->vel.x, camera1->vel.y, camera1->vel.z, camera1->falling.x, camera1->falling.y, camera1->falling.z); +} + +COMMAND(phystest, ""); + +void vecfromyawpitch(float yaw, float pitch, int move, int strafe, vec &m) +{ + if(move) + { + m.x = move*-sinf(RAD*yaw); + m.y = move*cosf(RAD*yaw); + } + else m.x = m.y = 0; + + if(pitch) + { + m.x *= cosf(RAD*pitch); + m.y *= cosf(RAD*pitch); + m.z = move*sinf(RAD*pitch); + } + else m.z = 0; + + if(strafe) + { + m.x += strafe*cosf(RAD*yaw); + m.y += strafe*sinf(RAD*yaw); + } +} + +void vectoyawpitch(const vec &v, float &yaw, float &pitch) +{ + if(v.iszero()) yaw = pitch = 0; + else + { + yaw = -atan2(v.x, v.y)/RAD; + pitch = asin(v.z/v.magnitude())/RAD; + } +} + +#define PHYSFRAMETIME 5 + +VARP(maxroll, 0, 0, 20); +FVAR(straferoll, 0, 0.033f, 90); +FVAR(faderoll, 0, 0.95f, 1); +VAR(floatspeed, 1, 100, 10000); + +void modifyvelocity(physent *pl, bool local, bool water, bool floating, int curtime) +{ + bool allowmove = game::allowmove(pl); + if(floating) + { + if(pl->jumping && allowmove) + { + pl->jumping = false; + pl->vel.z = max(pl->vel.z, JUMPVEL); + } + } + else if(pl->physstate >= PHYS_SLOPE || water) + { + if(water && !pl->inwater) pl->vel.div(8); + if(pl->jumping && allowmove) + { + pl->jumping = false; + + pl->vel.z = max(pl->vel.z, JUMPVEL); // physics impulse upwards + if(water) { pl->vel.x /= 8.0f; pl->vel.y /= 8.0f; } // dampen velocity change even harder, gives correct water feel + + game::physicstrigger(pl, local, 1, 0); + } + } + if(!floating && pl->physstate == PHYS_FALL) pl->timeinair = min(pl->timeinair + curtime, 1000); + + vec m(0.0f, 0.0f, 0.0f); + if((pl->move || pl->strafe) && allowmove) + { + vecfromyawpitch(pl->yaw, floating || water || pl->type==ENT_CAMERA ? pl->pitch : 0, pl->move, pl->strafe, m); + + if(!floating && pl->physstate >= PHYS_SLOPE) + { + /* move up or down slopes in air + * but only move up slopes in water + */ + float dz = -(m.x*pl->floor.x + m.y*pl->floor.y)/pl->floor.z; + m.z = water ? max(m.z, dz) : dz; + } + + m.normalize(); + } + + vec d(m); + d.mul(pl->maxspeed); + if(pl->type==ENT_PLAYER) + { + if(floating) + { + if(pl==player) d.mul(floatspeed/100.0f); + } + else if(!water && allowmove) d.mul((pl->move && !pl->strafe ? 1.3f : 1.0f) * (pl->physstate < PHYS_SLOPE ? 1.3f : 1.0f)); + } + float fric = water && !floating ? 20.0f : (pl->physstate >= PHYS_SLOPE || floating ? 6.0f : 30.0f); + pl->vel.lerp(d, pl->vel, pow(1 - 1/fric, curtime/20.0f)); +// old fps friction +// float friction = water && !floating ? 20.0f : (pl->physstate >= PHYS_SLOPE || floating ? 6.0f : 30.0f); +// float fpsfric = min(curtime/(20.0f*friction), 1.0f); +// pl->vel.lerp(pl->vel, d, fpsfric); +} + +void modifygravity(physent *pl, bool water, int curtime) +{ + float secs = curtime/1000.0f; + vec g(0, 0, 0); + if(pl->physstate == PHYS_FALL) g.z -= GRAVITY*secs; + else if(pl->floor.z > 0 && pl->floor.z < FLOORZ) + { + g.z = -1; + g.project(pl->floor); + g.normalize(); + g.mul(GRAVITY*secs); + } + if(!water || !game::allowmove(pl) || (!pl->move && !pl->strafe)) pl->falling.add(g); + + if(water || pl->physstate >= PHYS_SLOPE) + { + float fric = water ? 2.0f : 6.0f, + c = water ? 1.0f : clamp((pl->floor.z - SLOPEZ)/(FLOORZ-SLOPEZ), 0.0f, 1.0f); + pl->falling.mul(pow(1 - c/fric, curtime/20.0f)); +// old fps friction +// float friction = water ? 2.0f : 6.0f, +// fpsfric = friction/curtime*20.0f, +// c = water ? 1.0f : clamp((pl->floor.z - SLOPEZ)/(FLOORZ-SLOPEZ), 0.0f, 1.0f); +// pl->falling.mul(1 - c/fpsfric); + } +} + +// main physics routine, moves a player/monster for a curtime step +// moveres indicated the physics precision (which is lower for monsters and multiplayer prediction) +// local is false for multiplayer prediction + +bool moveplayer(physent *pl, int moveres, bool local, int curtime) +{ + int material = lookupmaterial(vec(pl->o.x, pl->o.y, pl->o.z + (3*pl->aboveeye - pl->eyeheight)/4)); + bool water = isliquid(material&MATF_VOLUME); + bool floating = pl->type==ENT_PLAYER && (pl->state==CS_EDITING || pl->state==CS_SPECTATOR); + float secs = curtime/1000.f; + + // apply gravity + if(!floating) modifygravity(pl, water, curtime); + // apply any player generated changes in velocity + modifyvelocity(pl, local, water, floating, curtime); + + vec d(pl->vel); + if(!floating && water) d.mul(0.5f); + d.add(pl->falling); + d.mul(secs); + + pl->blocked = false; + + if(floating) // just apply velocity + { + if(pl->physstate != PHYS_FLOAT) + { + pl->physstate = PHYS_FLOAT; + pl->timeinair = 0; + pl->falling = vec(0, 0, 0); + } + pl->o.add(d); + } + else // apply velocity with collision + { + const float f = 1.0f/moveres; + const int timeinair = pl->timeinair; + int collisions = 0; + + d.mul(f); + loopi(moveres) if(!move(pl, d) && ++collisions<5) i--; // discrete steps collision detection & sliding + if(timeinair > 800 && !pl->timeinair && !water) // if we land after long time must have been a high jump, make thud sound + { + game::physicstrigger(pl, local, -1, 0); + } + } + + if(pl->state==CS_ALIVE) updatedynentcache(pl); + + // automatically apply smooth roll when strafing + + if(pl->strafe && maxroll) pl->roll = clamp(pl->roll - pow(clamp(1.0f + pl->strafe*pl->roll/maxroll, 0.0f, 1.0f), 0.33f)*pl->strafe*curtime*straferoll, -maxroll, maxroll); + else pl->roll *= curtime == PHYSFRAMETIME ? faderoll : pow(faderoll, curtime/float(PHYSFRAMETIME)); + + // play sounds on water transitions + + if(pl->inwater && !water) + { + material = lookupmaterial(vec(pl->o.x, pl->o.y, pl->o.z + (pl->aboveeye - pl->eyeheight)/2)); + water = isliquid(material&MATF_VOLUME); + } + if(!pl->inwater && water) game::physicstrigger(pl, local, 0, -1, material&MATF_VOLUME); + else if(pl->inwater && !water) game::physicstrigger(pl, local, 0, 1, pl->inwater); + pl->inwater = water ? material&MATF_VOLUME : MAT_AIR; + + if(pl->state==CS_ALIVE && (pl->o.z < 0 || material&MAT_DEATH)) game::suicide(pl); + + return true; +} + +int physsteps = 0, physframetime = PHYSFRAMETIME, lastphysframe = 0; + +void physicsframe() // optimally schedule physics frames inside the graphics frames +{ + int diff = lastmillis - lastphysframe; + if(diff <= 0) physsteps = 0; + else + { + physframetime = clamp(game::scaletime(PHYSFRAMETIME)/100, 1, PHYSFRAMETIME); + physsteps = (diff + physframetime - 1)/physframetime; + lastphysframe += physsteps * physframetime; + } + cleardynentcache(); +} + +VAR(physinterp, 0, 1, 1); + +void interppos(physent *pl) +{ + pl->o = pl->newpos; + + int diff = lastphysframe - lastmillis; + if(diff <= 0 || !physinterp) return; + + vec deltapos(pl->deltapos); + deltapos.mul(min(diff, physframetime)/float(physframetime)); + pl->o.add(deltapos); +} + +void moveplayer(physent *pl, int moveres, bool local) +{ + if(physsteps <= 0) + { + if(local) interppos(pl); + return; + } + + if(local) pl->o = pl->newpos; + loopi(physsteps-1) moveplayer(pl, moveres, local, physframetime); + if(local) pl->deltapos = pl->o; + moveplayer(pl, moveres, local, physframetime); + if(local) + { + pl->newpos = pl->o; + pl->deltapos.sub(pl->newpos); + interppos(pl); + } +} + +bool bounce(physent *d, float elasticity, float waterfric, float grav) +{ + if(physsteps <= 0) + { + interppos(d); + return false; + } + + d->o = d->newpos; + bool hitplayer = false; + loopi(physsteps-1) + { + if(bounce(d, physframetime/1000.0f, elasticity, waterfric, grav)) hitplayer = true; + } + d->deltapos = d->o; + if(bounce(d, physframetime/1000.0f, elasticity, waterfric, grav)) hitplayer = true; + d->newpos = d->o; + d->deltapos.sub(d->newpos); + interppos(d); + return hitplayer; +} + +void updatephysstate(physent *d) +{ + if(d->physstate == PHYS_FALL) return; + d->timeinair = 0; + vec old(d->o); + /* Attempt to reconstruct the floor state. + * May be inaccurate since movement collisions are not considered. + * If good floor is not found, just keep the old floor and hope it's correct enough. + */ + switch(d->physstate) + { + case PHYS_SLOPE: + case PHYS_FLOOR: + case PHYS_STEP_DOWN: + d->o.z -= 0.15f; + if(collide(d, vec(0, 0, -1), d->physstate == PHYS_SLOPE || d->physstate == PHYS_STEP_DOWN ? SLOPEZ : FLOORZ)) + d->floor = collidewall; + break; + + case PHYS_STEP_UP: + d->o.z -= STAIRHEIGHT+0.15f; + if(collide(d, vec(0, 0, -1), SLOPEZ)) + d->floor = collidewall; + break; + + case PHYS_SLIDE: + d->o.z -= 0.15f; + if(collide(d, vec(0, 0, -1)) && collidewall.z < SLOPEZ) + d->floor = collidewall; + break; + } + if(d->physstate > PHYS_FALL && d->floor.z <= 0) d->floor = vec(0, 0, 1); + d->o = old; +} + +const float PLATFORMMARGIN = 0.2f; +const float PLATFORMBORDER = 10.0f; + +struct platforment +{ + physent *d; + int stacks, chains; + + platforment() {} + platforment(physent *d) : d(d), stacks(-1), chains(-1) {} + + bool operator==(const physent *o) const { return d == o; } +}; + +struct platformcollision +{ + platforment *ent; + int next; + + platformcollision() {} + platformcollision(platforment *ent, int next) : ent(ent), next(next) {} +}; + +template +static inline bool platformcollide(physent *d, const vec &dir, physent *o, float margin) +{ + E entvol(d); + O obvol(o, margin); + vec cp; + if(mpr::collide(entvol, obvol, NULL, NULL, &cp)) + { + vec wn = vec(cp).sub(obvol.center()); + return !obvol.contactface(wn, dir.iszero() ? vec(wn).neg() : dir).iszero(); + } + return false; +} + +bool platformcollide(physent *d, physent *o, const vec &dir, float margin = 0) +{ + if(d->collidetype == COLLIDE_OBB) + { + if(o->collidetype == COLLIDE_OBB) return platformcollide(d, dir, o, margin); + else return platformcollide(d, dir, o, margin); + + } + else if(o->collidetype == COLLIDE_OBB) return ellipseboxcollide(d, dir, o->o, vec(0, 0, 0), o->yaw, o->xradius, o->yradius, o->aboveeye, o->eyeheight + margin); + else return ellipsecollide(d, dir, o->o, vec(0, 0, 0), o->yaw, o->xradius, o->yradius, o->aboveeye, o->eyeheight + margin); +} + +bool moveplatform(physent *p, const vec &dir) +{ + if(!insideworld(p->newpos)) return false; + + vec oldpos(p->o); + (p->o = p->newpos).add(dir); + if(collide(p, dir, 0, dir.z<=0)) + { + p->o = oldpos; + return false; + } + p->o = oldpos; + + static vector ents; + ents.setsize(0); + for(int x = int(max(p->o.x-p->radius-PLATFORMBORDER, 0.0f))>>dynentsize, ex = int(min(p->o.x+p->radius+PLATFORMBORDER, worldsize-1.0f))>>dynentsize; x <= ex; x++) + for(int y = int(max(p->o.y-p->radius-PLATFORMBORDER, 0.0f))>>dynentsize, ey = int(min(p->o.y+p->radius+PLATFORMBORDER, worldsize-1.0f))>>dynentsize; y <= ey; y++) + { + const vector &dynents = checkdynentcache(x, y); + loopv(dynents) + { + physent *d = dynents[i]; + if(p==d || d->o.z-d->eyeheight < p->o.z+p->aboveeye || p->o.reject(d->o, p->radius+PLATFORMBORDER+d->radius) || ents.find(d) >= 0) continue; + ents.add(d); + } + } + static vector passengers, colliders; + passengers.setsize(0); + colliders.setsize(0); + static vector collisions; + collisions.setsize(0); + // build up collision DAG of colliders to be pushed off, and DAG of stacked passengers + loopv(ents) + { + platforment &ent = ents[i]; + physent *d = ent.d; + // check if the dynent is on top of the platform + if(platformcollide(p, d, vec(0, 0, 1), PLATFORMMARGIN)) passengers.add(&ent); + vec doldpos(d->o); + (d->o = d->newpos).add(dir); + if(collide(d, dir, 0, false)) colliders.add(&ent); + d->o = doldpos; + loopvj(ents) + { + platforment &o = ents[j]; + if(platformcollide(d, o.d, dir)) + { + collisions.add(platformcollision(&ent, o.chains)); + o.chains = collisions.length() - 1; + } + if(d->o.z < o.d->o.z && platformcollide(d, o.d, vec(0, 0, 1), PLATFORMMARGIN)) + { + collisions.add(platformcollision(&o, ent.stacks)); + ent.stacks = collisions.length() - 1; + } + } + } + loopv(colliders) // propagate collisions + { + platforment *ent = colliders[i]; + for(int n = ent->chains; n>=0; n = collisions[n].next) + { + platforment *o = collisions[n].ent; + if(colliders.find(o)<0) colliders.add(o); + } + } + if(dir.z>0) + { + loopv(passengers) // if any stacked passengers collide, stop the platform + { + platforment *ent = passengers[i]; + if(colliders.find(ent)>=0) return false; + for(int n = ent->stacks; n>=0; n = collisions[n].next) + { + platforment *o = collisions[n].ent; + if(passengers.find(o)<0) passengers.add(o); + } + } + loopv(passengers) + { + physent *d = passengers[i]->d; + d->o.add(dir); + d->newpos.add(dir); + if(dir.x || dir.y) updatedynentcache(d); + } + } + else loopv(passengers) // move any stacked passengers who aren't colliding with non-passengers + { + platforment *ent = passengers[i]; + if(colliders.find(ent)>=0) continue; + + physent *d = ent->d; + d->o.add(dir); + d->newpos.add(dir); + if(dir.x || dir.y) updatedynentcache(d); + + for(int n = ent->stacks; n>=0; n = collisions[n].next) + { + platforment *o = collisions[n].ent; + if(passengers.find(o)<0) passengers.add(o); + } + } + + p->o.add(dir); + p->newpos.add(dir); + if(dir.x || dir.y) updatedynentcache(p); + + return true; +} + +#define dir(name,v,d,s,os) ICOMMAND(name, "D", (int *down), { player->s = *down!=0; player->v = player->s ? d : (player->os ? -(d) : 0); }); + +dir(backward, move, -1, k_down, k_up); +dir(forward, move, 1, k_up, k_down); +dir(left, strafe, 1, k_left, k_right); +dir(right, strafe, -1, k_right, k_left); + +ICOMMAND(jump, "D", (int *down), { if(!*down || game::canjump()) player->jumping = *down!=0; }); +ICOMMAND(attack, "D", (int *down), { game::doattack(*down!=0); }); + +bool entinmap(dynent *d, bool avoidplayers) // brute force but effective way to find a free spawn spot in the map +{ + d->o.z += d->eyeheight; // pos specified is at feet + vec orig = d->o; + loopi(100) // try max 100 times + { + if(i) + { + d->o = orig; + d->o.x += (rnd(21)-10)*i/5; // increasing distance + d->o.y += (rnd(21)-10)*i/5; + d->o.z += (rnd(21)-10)*i/5; + } + + if(!collide(d) && !collideinside) + { + if(collideplayer) + { + if(!avoidplayers) continue; + d->o = orig; + d->resetinterp(); + return false; + } + + d->resetinterp(); + return true; + } + } + // leave ent at original pos, possibly stuck + d->o = orig; + d->resetinterp(); + conoutf(CON_WARN, "can't find entity spawn spot! (%.1f, %.1f, %.1f)", d->o.x, d->o.y, d->o.z); + return false; +} + diff --git a/src/engine/pvs.cpp b/src/engine/pvs.cpp new file mode 100644 index 0000000..0f29bf5 --- /dev/null +++ b/src/engine/pvs.cpp @@ -0,0 +1,1315 @@ +#include "engine.h" + +enum +{ + PVS_HIDE_GEOM = 1<<0, + PVS_HIDE_BB = 1<<1 +}; + +struct pvsnode +{ + bvec edges; + uchar flags; + uint children; +}; + +static vector origpvsnodes; + +static bool mergepvsnodes(pvsnode &p, pvsnode *children) +{ + loopi(7) if(children[i].flags!=children[7].flags) return false; + bvec bbs[4]; + loop(x, 2) loop(y, 2) + { + const bvec &lo = children[octaindex(2, x, y, 0)].edges, + &hi = children[octaindex(2, x, y, 1)].edges; + if(lo.x!=0xFF && (lo.x&0x11 || lo.y&0x11 || lo.z&0x11)) return false; + if(hi.x!=0xFF && (hi.x&0x11 || hi.y&0x11 || hi.z&0x11)) return false; + +#define MERGEBBS(res, coord, row, col) \ + if(lo.coord==0xFF) \ + { \ + if(hi.coord!=0xFF) \ + { \ + res.coord = ((hi.coord&~0x11)>>1) + 0x44; \ + res.row = hi.row; \ + res.col = hi.col; \ + } \ + } \ + else if(hi.coord==0xFF) \ + { \ + res.coord = (lo.coord&0xEE)>>1; \ + res.row = lo.row; \ + res.col = lo.col; \ + } \ + else if(lo.row!=hi.row || lo.col!=hi.col || (lo.coord&0xF0)!=0x80 || (hi.coord&0xF)!=0) return false; \ + else \ + { \ + res.coord = ((lo.coord&~0xF1)>>1) | (((hi.coord&~0x1F)>>1) + 0x40); \ + res.row = lo.row; \ + res.col = lo.col; \ + } + + bvec &res = bbs[x + 2*y]; + MERGEBBS(res, z, x, y); + res.x = lo.x; + res.y = lo.y; + } + loop(x, 2) + { + bvec &lo = bbs[x], &hi = bbs[x+2]; + MERGEBBS(lo, y, x, z); + } + bvec &lo = bbs[0], &hi = bbs[1]; + MERGEBBS(p.edges, x, y, z); + + return true; +} + +static void genpvsnodes(cube *c, int parent = 0, const ivec &co = ivec(0, 0, 0), int size = worldsize/2) +{ + int index = origpvsnodes.length(); + loopi(8) + { + ivec o(i, co, size); + pvsnode &n = origpvsnodes.add(); + n.flags = 0; + n.children = 0; + if(c[i].children || isempty(c[i]) || c[i].material&MAT_ALPHA) memset(n.edges.v, 0xFF, 3); + else loopk(3) + { + uint face = c[i].faces[k]; + if(face==F_SOLID) n.edges[k] = 0x80; + else + { + uchar low = max(max(face&0xF, (face>>8)&0xF), max((face>>16)&0xF, (face>>24)&0xF)), + high = min(min((face>>4)&0xF, (face>>12)&0xF), min((face>>20)&0xF, (face>>28)&0xF)); + if(size<8) + { + if(low&((8/size)-1)) { low += 8/size - (low&((8/size)-1)); } + if(high&((8/size)-1)) high &= ~(8/size-1); + } + if(low >= high) { memset(n.edges.v, 0xFF, 3); break; } + n.edges[k] = low | (high<<4); + } + } + } + int branches = 0; + loopi(8) if(c[i].children) + { + ivec o(i, co, size); + genpvsnodes(c[i].children, index+i, o, size>>1); + if(origpvsnodes[index+i].children) branches++; + } + if(!branches && mergepvsnodes(origpvsnodes[parent], &origpvsnodes[index])) origpvsnodes.setsize(index); + else origpvsnodes[parent].children = index; +} + +struct shaftplane +{ + float r, c, offset; + uchar rnear, cnear, rfar, cfar; +}; + +struct shaftbb +{ + union + { + ushort v[6]; + struct { usvec min, max; }; + }; + + shaftbb() {} + shaftbb(const ivec &o, int size) + { + min.x = o.x; + min.y = o.y; + min.z = o.z; + max.x = o.x + size; + max.y = o.y + size; + max.z = o.z + size; + } + shaftbb(const ivec &o, int size, const bvec &edges) + { + min.x = o.x + (size*(edges.x&0xF))/8; + min.y = o.y + (size*(edges.y&0xF))/8; + min.z = o.z + (size*(edges.z&0xF))/8; + max.x = o.x + (size*(edges.x>>4))/8; + max.y = o.y + (size*(edges.y>>4))/8; + max.z = o.z + (size*(edges.z>>4))/8; + } + + ushort &operator[](int i) { return v[i]; } + ushort operator[](int i) const { return v[i]; } + + bool contains(const shaftbb &o) const + { + return min.x<=o.min.x && min.y<=o.min.y && min.z<=o.min.z && + max.x>=o.max.x && max.y>=o.max.y && max.z>=o.max.z; + } + + bool outside(const ivec &o, int size) const + { + return o.x>=max.x || o.y>=max.y || o.z>=max.z || + o.x+size<=min.x || o.y+size<=min.y || o.z+size<=min.z; + } + + bool outside(const shaftbb &o) const + { + return o.min.x>max.x || o.min.y>max.y || o.min.z>max.z || + o.max.xmax.x || o.max.y>max.y || o.max.z>max.z; + } +}; + +struct shaft +{ + shaftbb bounds; + shaftplane planes[8]; + int numplanes; + + shaft(const shaftbb &from, const shaftbb &to) + { + calcshaft(from, to); + } + + void calcshaft(const shaftbb &from, const shaftbb &to) + { + uchar match = 0, color = 0; + loopi(3) + { + if(to.min[i] < from.min[i]) { color |= 1< from.min[i]) bounds.min[i] = to.min[i]+1; + else { match |= 1< from.max[i]) { color |= 8<>i)^(color>>j))&1) + { + int r = i%3, c = j%3, d = (r+1)%3; + if(d==c) d = (c+1)%3; + shaftplane &p = planes[numplanes++]; + p.r = from[j] - to[j]; + if(i<3 ? p.r >= 0 : p.r < 0) + { + p.r = -p.r; + p.c = from[i] - to[i]; + } + else p.c = to[i] - from[i]; + p.offset = -(from[i]*p.r + from[j]*p.c); + p.rnear = p.r >= 0 ? r : 3+r; + p.cnear = p.c >= 0 ? c : 3+c; + p.rfar = p.r < 0 ? r : 3+r; + p.cfar = p.c < 0 ? c : 3+c; + } + } + + bool outside(const shaftbb &o) const + { + if(bounds.outside(o)) return true; + + for(const shaftplane *p = planes; p < &planes[numplanes]; p++) + { + if(o[p->rnear]*p->r + o[p->cnear]*p->c + p->offset > 0) return true; + } + return false; + } + + bool inside(const shaftbb &o) const + { + if(bounds.notinside(o)) return false; + + for(const shaftplane *p = planes; p < &planes[numplanes]; p++) + { + if(o[p->rfar]*p->r + o[p->cfar]*p->c + p->offset > 0) return false; + } + return true; + } +}; + +struct pvsdata +{ + int offset, len; + + pvsdata() {} + pvsdata(int offset, int len) : offset(offset), len(len) {} +}; + +static vector pvsbuf; + +static inline uint hthash(const pvsdata &k) +{ + uint h = 5381; + loopi(k.len) h = ((h<<5)+h)^pvsbuf[k.offset+i]; + return h; +} + +static inline bool htcmp(const pvsdata &x, const pvsdata &y) +{ + return x.len==y.len && !memcmp(&pvsbuf[x.offset], &pvsbuf[y.offset], x.len); +} + +static SDL_mutex *pvsmutex = NULL; +static hashtable pvscompress; +static vector pvs; + +static SDL_mutex *viewcellmutex = NULL; +struct viewcellrequest +{ + int *result; + ivec o; + int size; +}; +static vector viewcellrequests; + +static bool genpvs_canceled = false; +static int numviewcells = 0; + +VAR(maxpvsblocker, 1, 512, 1<<16); +VAR(pvsleafsize, 1, 64, 1024); + +#define MAXWATERPVS 32 + +static struct +{ + int height; + vector matsurfs; +} waterplanes[MAXWATERPVS]; +static vector waterfalls; +uint numwaterplanes = 0; + +struct pvsworker +{ + pvsworker() : thread(NULL), pvsnodes(new pvsnode[origpvsnodes.length()]) + { + } + ~pvsworker() + { + delete[] pvsnodes; + } + + SDL_Thread *thread; + pvsnode *pvsnodes; + + shaftbb viewcellbb; + + pvsnode *levels[32]; + int curlevel; + ivec origin; + + void resetlevels() + { + curlevel = worldscale; + levels[curlevel] = &pvsnodes[0]; + origin = ivec(0, 0, 0); + } + + int hasvoxel(const ivec &p, int coord, int dir, int ocoord = 0, int odir = 0, int *omin = NULL) + { + uint diff = (origin.x^p.x)|(origin.y^p.y)|(origin.z^p.z); + if(diff >= uint(worldsize)) return 0; + diff >>= curlevel; + while(diff) + { + curlevel++; + diff >>= 1; + } + + pvsnode *cur = levels[curlevel]; + while(cur->children && !(cur->flags&PVS_HIDE_BB)) + { + cur = &pvsnodes[cur->children]; + curlevel--; + cur += ((p.z>>(curlevel-2))&4) | ((p.y>>(curlevel-1))&2) | ((p.x>>curlevel)&1); + levels[curlevel] = cur; + } + + origin = ivec(p.x&(~0U<flags&PVS_HIDE_BB || cur->edges==bvec(0x80, 0x80, 0x80)) + { + if(omin) + { + int step = origin[ocoord] + (odir< *omin) *omin = step; + } + return origin[coord] + (dir<edges.x==0xFF) return 0; + ivec bbp(p); + bbp.sub(origin); + ivec bbmin, bbmax; + bbmin.x = ((cur->edges.x&0xF)<edges.x>>4)<= bbmax.x) return 0; + bbmin.y = ((cur->edges.y&0xF)<edges.y>>4)<= bbmax.y) return 0; + bbmin.z = ((cur->edges.z&0xF)<edges.z>>4)<= bbmax.z) return 0; + + if(omin) + { + int step = (odir ? bbmax[ocoord] : bbmin[ocoord]) - bbp[ocoord] + (odir - 1); + if(odir ? step < *omin : step > *omin) *omin = step; + } + return (dir ? bbmax[coord] : bbmin[coord]) - bbp[coord] + (dir - 1); + } + + void hidepvs(pvsnode &p) + { + if(p.children) + { + pvsnode *children = &pvsnodes[p.children]; + loopi(8) hidepvs(children[i]); + p.flags |= PVS_HIDE_BB; + return; + } + p.flags |= PVS_HIDE_BB; + if(p.edges.x!=0xFF) p.flags |= PVS_HIDE_GEOM; + } + + void shaftcullpvs(shaft &s, pvsnode &p, const ivec &co = ivec(0, 0, 0), int size = worldsize) + { + if(p.flags&PVS_HIDE_BB) return; + shaftbb bb(co, size); + if(s.outside(bb)) return; + if(s.inside(bb)) { hidepvs(p); return; } + if(p.children) + { + pvsnode *children = &pvsnodes[p.children]; + uchar flags = 0xFF; + loopi(8) + { + ivec o(i, co, size>>1); + shaftcullpvs(s, children[i], o, size>>1); + flags &= children[i].flags; + } + if(flags & PVS_HIDE_BB) p.flags |= PVS_HIDE_BB; + return; + } + if(p.edges.x==0xFF) return; + shaftbb geom(co, size, p.edges); + if(s.inside(geom)) p.flags |= PVS_HIDE_GEOM; + } + + queue prevblockers; + + struct cullorder + { + int index, dist; + + cullorder() {} + cullorder(int index, int dist) : index(index), dist(dist) {} + }; + + void cullpvs(pvsnode &p, const ivec &co = ivec(0, 0, 0), int size = worldsize) + { + if(p.flags&(PVS_HIDE_BB | PVS_HIDE_GEOM) || genpvs_canceled) return; + if(p.children && !(p.flags&PVS_HIDE_BB)) + { + pvsnode *children = &pvsnodes[p.children]; + int csize = size>>1; + ivec dmin = ivec(co).add(csize>>1).sub(ivec(viewcellbb.min).add(ivec(viewcellbb.max)).shr(1)), dmax = ivec(dmin).add(csize); + dmin.mul(dmin); + dmax.mul(dmax); + ivec diff = ivec(dmax).sub(dmin); + cullorder order[8]; + int dir = 0; + if(diff.x < 0) { diff.x = -diff.x; dir |= 1; } + if(diff.y < 0) { diff.y = -diff.y; dir |= 2; } + if(diff.z < 0) { diff.z = -diff.z; dir |= 4; } + order[0] = cullorder(0, 0); + order[7] = cullorder(7, diff.x + diff.y + diff.z); + order[1] = cullorder(1, diff.x); + order[2] = cullorder(2, diff.y); + order[3] = cullorder(4, diff.z); + if(order[2].dist < order[1].dist) swap(order[1], order[2]); + if(order[3].dist < order[2].dist) swap(order[2], order[3]); + if(order[2].dist < order[1].dist) swap(order[1], order[2]); + cullorder dxy(order[1].index|order[2].index, order[1].dist+order[2].dist), + dxz(order[1].index|order[3].index, order[1].dist+order[3].dist), + dyz(order[2].index|order[3].index, order[2].dist+order[3].dist); + int j; + for(j = 4; j > 0 && dxy.dist < order[j-1].dist; --j) order[j] = order[j-1]; + order[j] = dxy; + for(j = 5; j > 0 && dxz.dist < order[j-1].dist; --j) order[j] = order[j-1]; + order[j] = dxz; + for(j = 6; j > 0 && dyz.dist < order[j-1].dist; --j) order[j] = order[j-1]; + order[j] = dyz; + loopi(8) + { + int index = order[i].index^dir; + ivec o(index, co, csize); + cullpvs(children[index], o, csize); + } + if(!(p.flags & PVS_HIDE_BB)) return; + } + bvec edges = p.children ? bvec(0x80, 0x80, 0x80) : p.edges; + if(edges.x==0xFF) return; + shaftbb geom(co, size, edges); + ivec diff = ivec(geom.max).sub(ivec(viewcellbb.min)).abs(); + cullorder order[3] = { cullorder(0, diff.x), cullorder(1, diff.y), cullorder(2, diff.z) }; + if(order[1].dist > order[0].dist) swap(order[0], order[1]); + if(order[2].dist > order[1].dist) swap(order[1], order[2]); + if(order[1].dist > order[0].dist) swap(order[0], order[1]); + loopi(6) + { + int dim = order[i >= 3 ? i-3 : i].index, dc = (i >= 3) != (geom.max[dim] <= viewcellbb.min[dim]) ? 1 : 0, r = R[dim], c = C[dim]; + int ccenter = geom.min[c]; + if(geom.min[r]==geom.max[r] || geom.min[c]==geom.max[c]) continue; + while(ccenter < geom.max[c]) + { + ivec rmin; + rmin[dim] = geom[dim + 3*dc] + (dc ? -1 : 0); + rmin[r] = geom.min[r]; + rmin[c] = ccenter; + ivec rmax = rmin; + rmax[r] = geom.max[r] - 1; + int rcenter = (rmin[r] + rmax[r])/2; + resetlevels(); + for(int minstep = -1, maxstep = 1; (minstep || maxstep) && rmax[r] - rmin[r] < maxpvsblocker;) + { + if(minstep) minstep = hasvoxel(rmin, r, 0); + if(maxstep) maxstep = hasvoxel(rmax, r, 1); + rmin[r] += minstep; + rmax[r] += maxstep; + } + rmin[r] = rcenter + (rmin[r] - rcenter)/2; + rmax[r] = rcenter + (rmax[r] - rcenter)/2; + if(rmin[r]>=geom.min[r] && rmax[r]=geom.min[r] && rmax[r]=geom.min[c] && cmax[c]geom.min[r]) emin[r] = geom.min[r]; + if(emax[r]= viewcellbb.max[dim] || bb.max[dim] <= viewcellbb.min[dim]) + { + int ddir = bb.min[dim] >= viewcellbb.max[dim] ? 1 : -1, + dval = ddir>0 ? USHRT_MAX-1 : 0, + dlimit = maxpvsblocker, + numsides = 0; + loopj(4) + { + ivec dmax; + int odim = j < 2 ? c : r; + if(j&1) + { + if(bb.max[odim] >= viewcellbb.max[odim]) continue; + dmax[odim] = bb.max[odim]-1; + } + else + { + if(bb.min[odim] <= viewcellbb.min[odim]) continue; + dmax[odim] = bb.min[odim]; + } + numsides++; + dmax[dim] = bb.min[dim]; + int stepdim = j < 2 ? r : c, stepstart = bb.min[stepdim], stepend = bb.max[stepdim]; + int dstep = ddir; + for(; dstep && ddir*(dmax[dim] - (int)bb.min[dim]) < dlimit;) + { + dmax[dim] += dstep; dstep = ddir > 0 ? INT_MAX : INT_MIN; + dmax[stepdim] = stepstart; + resetlevels(); + for(int step = 1; step && dmax[stepdim] < stepend;) + { + step = hasvoxel(dmax, stepdim, 1, dim, (ddir+1)/2, &dstep); + dmax[stepdim] += step; + } + if(dmax[stepdim] < stepend) dstep = 0; + } + dlimit = min(dlimit, ddir*(dmax[dim] - (int)bb.min[dim])); + if(!dstep) dmax[dim] -= ddir; + if(ddir>0) dval = min(dval, dmax[dim]); + else dval = max(dval, dmax[dim]); + } + if(numsides>0) + { + if(ddir>0) bb.max[dim] = dval+1; + else bb.min[dim] = dval; + } + //printf("(%d,%d,%d) x %d,%d,%d, side %d, ccenter = %d, origin = (%d,%d,%d), size = %d\n", bb.min.x, bb.min.y, bb.min.z, bb.max.x-bb.min.x, bb.max.y-bb.min.y, bb.max.z-bb.min.z, i, ccenter, co.x, co.y, co.z, size); + } + bool dup = false; + loopvj(prevblockers) + { + if(prevblockers[j].contains(bb)) { dup = true; break; } + } + if(!dup) + { + shaft s(viewcellbb, bb); + shaftcullpvs(s, pvsnodes[0]); + prevblockers.add(bb); + } + if(bb.contains(geom)) return; + ccenter = cmax[c] + 1; + } + } + } + + bool compresspvs(pvsnode &p, int size, int threshold) + { + if(!p.children) return true; + if(p.flags&PVS_HIDE_BB) { p.children = 0; return true; } + pvsnode *children = &pvsnodes[p.children]; + bool canreduce = true; + loopi(8) + { + if(!compresspvs(children[i], size/2, threshold)) canreduce = false; + } + if(canreduce) + { + int hide = children[7].flags&PVS_HIDE_BB; + loopi(7) if((children[i].flags&PVS_HIDE_BB)!=hide) canreduce = false; + if(canreduce) + { + p.flags = (p.flags & ~PVS_HIDE_BB) | hide; + p.children = 0; + return true; + } + } + if(size <= threshold) + { + p.children = 0; + return true; + } + return false; + } + + vector outbuf; + + bool serializepvs(pvsnode &p, int storage = -1) + { + if(!p.children) + { + outbuf.add(0xFF); + loopi(8) outbuf.add(p.flags&PVS_HIDE_BB ? 0xFF : 0); + return true; + } + int index = outbuf.length(); + pvsnode *children = &pvsnodes[p.children]; + int i = 0; + uchar leafvalues = 0; + if(storage>=0) + { + for(; i < 8; i++) + { + pvsnode &child = children[i]; + if(child.flags&PVS_HIDE_BB) leafvalues |= 1<255) { outbuf[storage] = 0; return false; } + outbuf[storage] = uchar(offset); + } + outbuf.add(0); + loopj(8) outbuf.add(leafvalues&(1< &matsurfs) + { + if(pvsnodes[0].flags & PVS_HIDE_BB) return true; + if(!pvsnodes[0].children) return false; + loopv(matsurfs) + { + materialsurface &m = *matsurfs[i]; + ivec bbmin(m.o), bbmax(m.o); + int dim = dimension(m.orient); + bbmin[dim] += dimcoord(m.orient) ? -2 : 2; + bbmax[C[dim]] += m.csize; + bbmax[R[dim]] += m.rsize; + if(!materialoccluded(pvsnodes[0], ivec(0, 0, 0), worldsize/2, bbmin, bbmax)) return false; + } + return true; + } + + int wateroccluded, waterbytes; + + void calcpvs(const ivec &co, int size) + { + loopk(3) + { + viewcellbb.min[k] = co[k]; + viewcellbb.max[k] = co[k]+size; + } + memcpy(pvsnodes, origpvsnodes.getbuf(), origpvsnodes.length()*sizeof(pvsnode)); + prevblockers.clear(); + cullpvs(pvsnodes[0]); + + wateroccluded = 0; + loopi(numwaterplanes) + { + if(waterplanes[i].height < 0) + { + if(waterfalls.length() && materialoccluded(waterfalls)) wateroccluded |= 1<>(i*8))&0xFF); + pvsbuf.put(outbuf.getbuf(), outbuf.length()); + int *val = pvscompress.access(key); + if(val) pvsbuf.setsize(key.offset); + else + { + val = &pvscompress[key]; + *val = pvs.length(); + pvs.add(key); + } + if(pvsmutex) SDL_UnlockMutex(pvsmutex); + return *val; + } + + static int run(void *data) + { + pvsworker *w = (pvsworker *)data; + SDL_LockMutex(viewcellmutex); + while(viewcellrequests.length()) + { + viewcellrequest req = viewcellrequests.pop(); + SDL_UnlockMutex(viewcellmutex); + int result = w->genviewcell(req.o, req.size); + SDL_LockMutex(viewcellmutex); + *req.result = result; + } + SDL_UnlockMutex(viewcellmutex); + return 0; + } +}; + +struct viewcellnode +{ + uchar leafmask; + union viewcellchild + { + int pvs; + viewcellnode *node; + } children[8]; + + viewcellnode() : leafmask(0xFF) + { + loopi(8) children[i].pvs = -1; + } + ~viewcellnode() + { + loopi(8) if(!(leafmask&(1< pvsworkers; + +static volatile bool check_genpvs_progress = false; + +static Uint32 genpvs_timer(Uint32 interval, void *param) +{ + check_genpvs_progress = true; + return interval; +} + +static int totalviewcells = 0; + +static void show_genpvs_progress(int unique = pvs.length(), int processed = numviewcells) +{ + float bar1 = float(processed) / float(totalviewcells>0 ? totalviewcells : 1); + + defformatstring(text1, "%d%% - %d of %d view cells (%d unique)", int(bar1 * 100), processed, totalviewcells, unique); + + renderprogress(bar1, text1); + + if(interceptkey(SDLK_ESCAPE)) genpvs_canceled = true; + check_genpvs_progress = false; +} + +static shaftbb pvsbounds; + +static void calcpvsbounds() +{ + loopk(3) pvsbounds.min[k] = USHRT_MAX; + loopk(3) pvsbounds.max[k] = 0; + loopv(valist) + { + vtxarray *va = valist[i]; + loopk(3) + { + if(va->geommin[k]>va->geommax[k]) continue; + pvsbounds.min[k] = min(pvsbounds.min[k], (ushort)va->geommin[k]); + pvsbounds.max[k] = max(pvsbounds.max[k], (ushort)va->geommax[k]); + } + } +} + +static inline bool isallclip(cube *c) +{ + loopi(8) + { + cube &h = c[i]; + if(h.children ? !isallclip(h.children) : (!isentirelysolid(h) && (h.material&MATF_CLIP)!=MAT_CLIP)) + return false; + } + return true; +} + +static int countviewcells(cube *c, const ivec &co, int size, int threshold) +{ + int count = 0; + loopi(8) + { + ivec o(i, co, size); + if(pvsbounds.outside(o, size)) continue; + cube &h = c[i]; + if(h.children) + { + if(size>threshold) + { + count += countviewcells(h.children, o, size>>1, threshold); + continue; + } + if(isallclip(h.children)) continue; + } + else if(isentirelysolid(h) || (h.material&MATF_CLIP)==MAT_CLIP) continue; + count++; + } + return count; +} + +static void genviewcells(viewcellnode &p, cube *c, const ivec &co, int size, int threshold) +{ + if(genpvs_canceled) return; + loopi(8) + { + ivec o(i, co, size); + if(pvsbounds.outside(o, size)) continue; + cube &h = c[i]; + if(h.children) + { + if(size>threshold) + { + p.leafmask &= ~(1<>1, threshold); + continue; + } + if(isallclip(h.children)) continue; + } + else if(isentirelysolid(h) || (h.material&MATF_CLIP)==MAT_CLIP) continue; + if(pvsworkers.length()) + { + if(genpvs_canceled) return; + p.children[i].pvs = pvsworkers[0]->genviewcell(o, size); + if(check_genpvs_progress) show_genpvs_progress(); + } + else + { + viewcellrequest &req = viewcellrequests.add(); + req.result = &p.children[i].pvs; + req.o = o; + req.size = size; + } + } +} + +static viewcellnode *viewcells = NULL; +static int lockedwaterplanes[MAXWATERPVS]; +static uchar *curpvs = NULL, *lockedpvs = NULL; +static int curwaterpvs = 0, lockedwaterpvs = 0; + +static inline pvsdata *lookupviewcell(const vec &p) +{ + uint x = uint(floor(p.x)), y = uint(floor(p.y)), z = uint(floor(p.z)); + if(!viewcells || (x|y|z)>=uint(worldsize)) return NULL; + viewcellnode *vc = viewcells; + for(int scale = worldscale-1; scale>=0; scale--) + { + int i = octastep(x, y, z, scale); + if(vc->leafmask&(1<children[i].pvs>=0 ? &pvs[vc->children[i].pvs] : NULL; + } + vc = vc->children[i].node; + } + return NULL; +} + +static void lockpvs_(bool lock) +{ + if(lockedpvs) DELETEA(lockedpvs); + if(!lock) return; + pvsdata *d = lookupviewcell(camera1->o); + if(!d) return; + int wbytes = d->len%9, len = d->len - wbytes; + lockedpvs = new uchar[len]; + memcpy(lockedpvs, &pvsbuf[d->offset + wbytes], len); + lockedwaterpvs = 0; + loopi(wbytes) lockedwaterpvs |= pvsbuf[d->offset + i] << (i*8); + loopi(MAXWATERPVS) lockedwaterplanes[i] = waterplanes[i].height; + conoutf("locked view cell at %.1f, %.1f, %.1f", camera1->o.x, camera1->o.y, camera1->o.z); +} + +VARF(lockpvs, 0, 0, 1, lockpvs_(lockpvs!=0)); + +VARN(pvs, usepvs, 0, 1, 1); +VARN(waterpvs, usewaterpvs, 0, 1, 1); + +void setviewcell(const vec &p) +{ + if(!usepvs) curpvs = NULL; + else if(lockedpvs) + { + curpvs = lockedpvs; + curwaterpvs = lockedwaterpvs; + } + else + { + pvsdata *d = lookupviewcell(p); + curpvs = d ? &pvsbuf[d->offset] : NULL; + curwaterpvs = 0; + if(d) + { + loopi(d->len%9) curwaterpvs |= *curpvs++ << (i*8); + } + } + if(!usepvs || !usewaterpvs) curwaterpvs = 0; +} + +void clearpvs() +{ + DELETEP(viewcells); + pvs.setsize(0); + pvsbuf.setsize(0); + curpvs = NULL; + numwaterplanes = 0; + lockpvs = 0; + lockpvs_(false); +} + +COMMAND(clearpvs, ""); + +static void findwaterplanes() +{ + loopi(MAXWATERPVS) + { + waterplanes[i].height = -1; + waterplanes[i].matsurfs.setsize(0); + } + waterfalls.setsize(0); + numwaterplanes = 0; + loopv(valist) + { + vtxarray *va = valist[i]; + loopj(va->matsurfs) + { + materialsurface &m = va->matbuf[j]; + if((m.material&MATF_VOLUME)!=MAT_WATER || m.orient==O_BOTTOM) { j += m.skip; continue; } + if(m.orient!=O_TOP) + { + waterfalls.add(&m); + continue; + } + loopk(numwaterplanes) if(waterplanes[k].height == m.o.z) + { + waterplanes[k].matsurfs.add(&m); + goto nextmatsurf; + } + if(numwaterplanes < MAXWATERPVS) + { + waterplanes[numwaterplanes].height = m.o.z; + waterplanes[numwaterplanes].matsurfs.add(&m); + numwaterplanes++; + } + nextmatsurf:; + } + } + if(waterfalls.length() > 0 && numwaterplanes < MAXWATERPVS) numwaterplanes++; +} + +void testpvs(int *vcsize) +{ + lockpvs_(false); + + uint oldnumwaterplanes = numwaterplanes; + int oldwaterplanes[MAXWATERPVS]; + loopi(numwaterplanes) oldwaterplanes[i] = waterplanes[i].height; + + findwaterplanes(); + + pvsnode &root = origpvsnodes.add(); + memset(root.edges.v, 0xFF, 3); + root.flags = 0; + root.children = 0; + genpvsnodes(worldroot); + + genpvs_canceled = false; + check_genpvs_progress = false; + + int size = *vcsize>0 ? *vcsize : 32; + for(int mask = 1; mask < size; mask <<= 1) size &= ~mask; + + ivec o = ivec(camera1->o).mask(~(size-1)); + pvsworker w; + int len; + lockedpvs = w.testviewcell(o, size, &lockedwaterpvs, &len); + loopi(MAXWATERPVS) lockedwaterplanes[i] = waterplanes[i].height; + lockpvs = 1; + conoutf("generated test view cell of size %d at %.1f, %.1f, %.1f (%d B)", size, camera1->o.x, camera1->o.y, camera1->o.z, len); + + origpvsnodes.setsize(0); + numwaterplanes = oldnumwaterplanes; + loopi(numwaterplanes) waterplanes[i].height = oldwaterplanes[i]; +} + +COMMAND(testpvs, "i"); + +void genpvs(int *viewcellsize) +{ + if(worldsize > 1<<15) + { + conoutf(CON_ERROR, "map is too large for PVS"); + return; + } + + renderbackground("generating PVS (esc to abort)"); + genpvs_canceled = false; + Uint32 start = SDL_GetTicks(); + + renderprogress(0, "finding view cells"); + + clearpvs(); + calcpvsbounds(); + findwaterplanes(); + + pvsnode &root = origpvsnodes.add(); + memset(root.edges.v, 0xFF, 3); + root.flags = 0; + root.children = 0; + genpvsnodes(worldroot); + + totalviewcells = countviewcells(worldroot, ivec(0, 0, 0), worldsize>>1, *viewcellsize>0 ? *viewcellsize : 32); + numviewcells = 0; + genpvs_canceled = false; + check_genpvs_progress = false; + SDL_TimerID timer = 0; + int numthreads = pvsthreads > 0 ? pvsthreads : numcpus; + if(numthreads<=1) + { + pvsworkers.add(new pvsworker); + timer = SDL_AddTimer(500, genpvs_timer, NULL); + } + viewcells = new viewcellnode; + genviewcells(*viewcells, worldroot, ivec(0, 0, 0), worldsize>>1, *viewcellsize>0 ? *viewcellsize : 32); + if(numthreads<=1) + { + SDL_RemoveTimer(timer); + } + else + { + renderprogress(0, "creating threads"); + if(!pvsmutex) pvsmutex = SDL_CreateMutex(); + if(!viewcellmutex) viewcellmutex = SDL_CreateMutex(); + loopi(numthreads) + { + pvsworker *w = pvsworkers.add(new pvsworker); + w->thread = SDL_CreateThread(pvsworker::run, "pvs worker", w); + } + show_genpvs_progress(0, 0); + while(!genpvs_canceled) + { + SDL_Delay(500); + SDL_LockMutex(viewcellmutex); + int unique = pvs.length(), processed = numviewcells, remaining = viewcellrequests.length(); + SDL_UnlockMutex(viewcellmutex); + show_genpvs_progress(unique, processed); + if(!remaining) break; + } + SDL_LockMutex(viewcellmutex); + viewcellrequests.setsize(0); + SDL_UnlockMutex(viewcellmutex); + loopv(pvsworkers) SDL_WaitThread(pvsworkers[i]->thread, NULL); + } + pvsworkers.deletecontents(); + + origpvsnodes.setsize(0); + pvscompress.clear(); + + Uint32 end = SDL_GetTicks(); + if(genpvs_canceled) + { + clearpvs(); + conoutf("genpvs aborted"); + } + else conoutf("generated %d unique view cells totaling %.1f kB and averaging %d B (%.1f seconds)", + pvs.length(), pvsbuf.length()/1024.0f, pvsbuf.length()/max(pvs.length(), 1), (end - start) / 1000.0f); +} + +COMMAND(genpvs, "i"); + +void pvsstats() +{ + conoutf("%d unique view cells totaling %.1f kB and averaging %d B", + pvs.length(), pvsbuf.length()/1024.0f, pvsbuf.length()/max(pvs.length(), 1)); +} + +COMMAND(pvsstats, ""); + +static inline bool pvsoccluded(uchar *buf, const ivec &co, int size, const ivec &bbmin, const ivec &bbmax) +{ + uchar leafmask = buf[0]; + loopoctabox(co, size, bbmin, bbmax) + { + ivec o(i, co, size); + if(leafmask&(1<>1, bbmin, bbmax)&~leafvalues)) + return false; + } + else if(!pvsoccluded(buf+9*buf[1+i], o, size>>1, bbmin, bbmax)) return false; + } + return true; +} + +static inline bool pvsoccluded(uchar *buf, const ivec &bbmin, const ivec &bbmax) +{ + int diff = (bbmin.x^bbmax.x) | (bbmin.y^bbmax.y) | (bbmin.z^bbmax.z); + if(diff&~((1<putchar(p.leafmask); + loopi(8) + { + if(p.leafmask&(1<putlil(p.children[i].pvs); + else saveviewcells(f, *p.children[i].node); + } +} + +void savepvs(stream *f) +{ + uint totallen = pvsbuf.length() | (numwaterplanes>0 ? 0x80000000U : 0); + f->putlil(totallen); + if(numwaterplanes>0) + { + f->putlil(numwaterplanes); + loopi(numwaterplanes) + { + f->putlil(waterplanes[i].height); + if(waterplanes[i].height < 0) break; + } + } + loopv(pvs) f->putlil(pvs[i].len); + f->write(pvsbuf.getbuf(), pvsbuf.length()); + saveviewcells(f, *viewcells); +} + +viewcellnode *loadviewcells(stream *f) +{ + viewcellnode *p = new viewcellnode; + p->leafmask = f->getchar(); + loopi(8) + { + if(p->leafmask&(1<children[i].pvs = f->getlil(); + else p->children[i].node = loadviewcells(f); + } + return p; +} + +void loadpvs(stream *f, int numpvs) +{ + uint totallen = f->getlil(); + if(totallen & 0x80000000U) + { + totallen &= ~0x80000000U; + numwaterplanes = f->getlil(); + loopi(numwaterplanes) waterplanes[i].height = f->getlil(); + } + int offset = 0; + loopi(numpvs) + { + ushort len = f->getlil(); + pvs.add(pvsdata(offset, len)); + offset += len; + } + f->read(pvsbuf.reserve(totallen).buf, totallen); + pvsbuf.advance(totallen); + viewcells = loadviewcells(f); +} + +int getnumviewcells() { return pvs.length(); } + diff --git a/src/engine/ragdoll.h b/src/engine/ragdoll.h new file mode 100644 index 0000000..9f6f641 --- /dev/null +++ b/src/engine/ragdoll.h @@ -0,0 +1,534 @@ +struct ragdollskel +{ + struct vert + { + vec pos; + float radius, weight; + }; + + struct tri + { + int vert[3]; + + bool shareverts(const tri &t) const + { + loopi(3) loopj(3) if(vert[i] == t.vert[j]) return true; + return false; + } + }; + + struct distlimit + { + int vert[2]; + float mindist, maxdist; + }; + + struct rotlimit + { + int tri[2]; + float maxangle; + matrix3 middle; + }; + + struct rotfriction + { + int tri[2]; + matrix3 middle; + }; + + struct joint + { + int bone, tri, vert[3]; + float weight; + matrix4x3 orient; + }; + + struct reljoint + { + int bone, parent; + }; + + bool loaded, animjoints; + int eye; + vector verts; + vector tris; + vector distlimits; + vector rotlimits; + vector rotfrictions; + vector joints; + vector reljoints; + + ragdollskel() : loaded(false), animjoints(false), eye(-1) {} + + void setupjoints() + { + loopv(verts) verts[i].weight = 0; + loopv(joints) + { + joint &j = joints[i]; + j.weight = 0; + vec pos(0, 0, 0); + loopk(3) if(j.vert[k]>=0) + { + pos.add(verts[j.vert[k]].pos); + j.weight++; + verts[j.vert[k]].weight++; + } + if(j.weight) j.weight = 1/j.weight; + pos.mul(j.weight); + + tri &t = tris[j.tri]; + matrix4x3 &m = j.orient; + const vec &v1 = verts[t.vert[0]].pos, + &v2 = verts[t.vert[1]].pos, + &v3 = verts[t.vert[2]].pos; + m.a = vec(v2).sub(v1).normalize(); + m.c.cross(m.a, vec(v3).sub(v1)).normalize(); + m.b.cross(m.c, m.a); + m.d = pos; + m.transpose(); + } + loopv(verts) if(verts[i].weight) verts[i].weight = 1/verts[i].weight; + reljoints.shrink(0); + } + + void setuprotfrictions() + { + rotfrictions.shrink(0); + loopv(tris) for(int j = i+1; j < tris.length(); j++) if(tris[i].shareverts(tris[j])) + { + rotfriction &r = rotfrictions.add(); + r.tri[0] = i; + r.tri[1] = j; + } + } + + void setup() + { + setupjoints(); + setuprotfrictions(); + + loaded = true; + } + + void addreljoint(int bone, int parent) + { + reljoint &r = reljoints.add(); + r.bone = bone; + r.parent = parent; + } +}; + +struct ragdolldata +{ + struct vert + { + vec oldpos, pos, newpos; + float weight; + bool collided, stuck; + + vert() : pos(0, 0, 0), newpos(0, 0, 0), weight(0), collided(false), stuck(true) {} + }; + + ragdollskel *skel; + int millis, collidemillis, collisions, floating, lastmove, unsticks; + vec offset, center; + float radius, timestep, scale; + vert *verts; + matrix3 *tris; + matrix4x3 *animjoints; + dualquat *reljoints; + + ragdolldata(ragdollskel *skel, float scale = 1) + : skel(skel), + millis(lastmillis), + collidemillis(0), + collisions(0), + floating(0), + lastmove(lastmillis), + unsticks(INT_MAX), + radius(0), + timestep(0), + scale(scale), + verts(new vert[skel->verts.length()]), + tris(new matrix3[skel->tris.length()]), + animjoints(!skel->animjoints || skel->joints.empty() ? NULL : new matrix4x3[skel->joints.length()]), + reljoints(skel->reljoints.empty() ? NULL : new dualquat[skel->reljoints.length()]) + { + } + + ~ragdolldata() + { + delete[] verts; + delete[] tris; + if(animjoints) delete[] animjoints; + if(reljoints) delete[] reljoints; + } + + void calcanimjoint(int i, const matrix4x3 &anim) + { + if(!animjoints) return; + ragdollskel::joint &j = skel->joints[i]; + vec pos(0, 0, 0); + loopk(3) if(j.vert[k]>=0) pos.add(verts[j.vert[k]].pos); + pos.mul(j.weight); + + ragdollskel::tri &t = skel->tris[j.tri]; + matrix4x3 m; + const vec &v1 = verts[t.vert[0]].pos, + &v2 = verts[t.vert[1]].pos, + &v3 = verts[t.vert[2]].pos; + m.a = vec(v2).sub(v1).normalize(); + m.c.cross(m.a, vec(v3).sub(v1)).normalize(); + m.b.cross(m.c, m.a); + m.d = pos; + animjoints[i].transposemul(m, anim); + } + + void calctris() + { + loopv(skel->tris) + { + ragdollskel::tri &t = skel->tris[i]; + matrix3 &m = tris[i]; + const vec &v1 = verts[t.vert[0]].pos, + &v2 = verts[t.vert[1]].pos, + &v3 = verts[t.vert[2]].pos; + m.a = vec(v2).sub(v1).normalize(); + m.c.cross(m.a, vec(v3).sub(v1)).normalize(); + m.b.cross(m.c, m.a); + } + } + + void calcboundsphere() + { + center = vec(0, 0, 0); + loopv(skel->verts) center.add(verts[i].pos); + center.div(skel->verts.length()); + radius = 0; + loopv(skel->verts) radius = max(radius, verts[i].pos.dist(center)); + } + + void init(dynent *d) + { + extern int ragdolltimestepmin; + float ts = ragdolltimestepmin/1000.0f; + loopv(skel->verts) (verts[i].oldpos = verts[i].pos).sub(vec(d->vel).add(d->falling).mul(ts)); + timestep = ts; + + calctris(); + calcboundsphere(); + offset = d->o; + offset.sub(skel->eye >= 0 ? verts[skel->eye].pos : center); + offset.z += (d->eyeheight + d->aboveeye)/2; + } + + void move(dynent *pl, float ts); + void updatepos(); + void constrain(); + void constraindist(); + void applyrotlimit(ragdollskel::tri &t1, ragdollskel::tri &t2, float angle, const vec &axis); + void constrainrot(); + void calcrotfriction(); + void applyrotfriction(float ts); + void tryunstick(float speed); + + static inline bool collidevert(const vec &pos, const vec &dir, float radius) + { + static struct vertent : physent + { + vertent() + { + type = ENT_BOUNCE; + radius = xradius = yradius = eyeheight = aboveeye = 1; + } + } v; + v.o = pos; + if(v.radius != radius) v.radius = v.xradius = v.yradius = v.eyeheight = v.aboveeye = radius; + return collide(&v, dir, 0, false); + } +}; + +/* + seed particle position = avg(modelview * base2anim * spherepos) + mapped transform = invert(curtri) * origtrig + parented transform = parent{invert(curtri) * origtrig} * (invert(parent{base2anim}) * base2anim) +*/ + +void ragdolldata::constraindist() +{ + float invscale = 1.0f/scale; + loopv(skel->distlimits) + { + ragdollskel::distlimit &d = skel->distlimits[i]; + vert &v1 = verts[d.vert[0]], &v2 = verts[d.vert[1]]; + vec dir = vec(v2.pos).sub(v1.pos); + float dist = dir.magnitude()*invscale, cdist; + if(dist < d.mindist) cdist = d.mindist; + else if(dist > d.maxdist) cdist = d.maxdist; + else continue; + if(dist > 1e-4f) dir.mul(cdist*0.5f/dist); + else dir = vec(0, 0, cdist*0.5f/invscale); + vec center = vec(v1.pos).add(v2.pos).mul(0.5f); + v1.newpos.add(vec(center).sub(dir)); + v1.weight++; + v2.newpos.add(vec(center).add(dir)); + v2.weight++; + } +} + +inline void ragdolldata::applyrotlimit(ragdollskel::tri &t1, ragdollskel::tri &t2, float angle, const vec &axis) +{ + vert &v1a = verts[t1.vert[0]], &v1b = verts[t1.vert[1]], &v1c = verts[t1.vert[2]], + &v2a = verts[t2.vert[0]], &v2b = verts[t2.vert[1]], &v2c = verts[t2.vert[2]]; + vec m1 = vec(v1a.pos).add(v1b.pos).add(v1c.pos).div(3), + m2 = vec(v2a.pos).add(v2b.pos).add(v2c.pos).div(3), + q1a, q1b, q1c, q2a, q2b, q2c; + float w1 = q1a.cross(axis, vec(v1a.pos).sub(m1)).magnitude() + + q1b.cross(axis, vec(v1b.pos).sub(m1)).magnitude() + + q1c.cross(axis, vec(v1c.pos).sub(m1)).magnitude(), + w2 = q2a.cross(axis, vec(v2a.pos).sub(m2)).magnitude() + + q2b.cross(axis, vec(v2b.pos).sub(m2)).magnitude() + + q2c.cross(axis, vec(v2c.pos).sub(m2)).magnitude(); + angle /= w1 + w2 + 1e-9f; + float a1 = angle*w2, a2 = -angle*w1, + s1 = sinf(a1), s2 = sinf(a2); + vec c1 = vec(axis).mul(1 - cosf(a1)), c2 = vec(axis).mul(1 - cosf(a2)); + v1a.newpos.add(vec().cross(c1, q1a).madd(q1a, s1).add(v1a.pos)); + v1a.weight++; + v1b.newpos.add(vec().cross(c1, q1b).madd(q1b, s1).add(v1b.pos)); + v1b.weight++; + v1c.newpos.add(vec().cross(c1, q1c).madd(q1c, s1).add(v1c.pos)); + v1c.weight++; + v2a.newpos.add(vec().cross(c2, q2a).madd(q2a, s2).add(v2a.pos)); + v2a.weight++; + v2b.newpos.add(vec().cross(c2, q2b).madd(q2b, s2).add(v2b.pos)); + v2b.weight++; + v2c.newpos.add(vec().cross(c2, q2c).madd(q2c, s2).add(v2c.pos)); + v2c.weight++; +} + +void ragdolldata::constrainrot() +{ + loopv(skel->rotlimits) + { + ragdollskel::rotlimit &r = skel->rotlimits[i]; + matrix3 rot; + rot.mul(tris[r.tri[0]], r.middle); + rot.multranspose(tris[r.tri[1]]); + + vec axis; + float angle; + if(!rot.calcangleaxis(angle, axis)) continue; + angle = r.maxangle - fabs(angle); + if(angle >= 0) continue; + angle += 1e-3f; + + applyrotlimit(skel->tris[r.tri[0]], skel->tris[r.tri[1]], angle, axis); + } +} + +VAR(ragdolltimestepmin, 1, 5, 50); +VAR(ragdolltimestepmax, 1, 10, 50); +FVAR(ragdollrotfric, 0, 0.85f, 1); +FVAR(ragdollrotfricstop, 0, 0.1f, 1); + +void ragdolldata::calcrotfriction() +{ + loopv(skel->rotfrictions) + { + ragdollskel::rotfriction &r = skel->rotfrictions[i]; + r.middle.transposemul(tris[r.tri[0]], tris[r.tri[1]]); + } +} + +void ragdolldata::applyrotfriction(float ts) +{ + calctris(); + float stopangle = 2*M_PI*ts*ragdollrotfricstop, rotfric = 1.0f - pow(ragdollrotfric, ts*1000.0f/ragdolltimestepmin); + loopv(skel->rotfrictions) + { + ragdollskel::rotfriction &r = skel->rotfrictions[i]; + matrix3 rot; + rot.mul(tris[r.tri[0]], r.middle); + rot.multranspose(tris[r.tri[1]]); + + vec axis; + float angle; + if(rot.calcangleaxis(angle, axis)) + { + angle *= -(fabs(angle) >= stopangle ? rotfric : 1.0f); + applyrotlimit(skel->tris[r.tri[0]], skel->tris[r.tri[1]], angle, axis); + } + } + loopv(skel->verts) + { + vert &v = verts[i]; + if(v.weight) v.pos = v.newpos.div(v.weight); + v.newpos = vec(0, 0, 0); + v.weight = 0; + } +} + +void ragdolldata::tryunstick(float speed) +{ + vec unstuck(0, 0, 0); + int stuck = 0; + loopv(skel->verts) + { + vert &v = verts[i]; + if(v.stuck) + { + if(collidevert(v.pos, vec(0, 0, 0), skel->verts[i].radius)) { stuck++; continue; } + v.stuck = false; + } + unstuck.add(v.pos); + } + unsticks = 0; + if(!stuck || stuck >= skel->verts.length()) return; + unstuck.div(skel->verts.length() - stuck); + loopv(skel->verts) + { + vert &v = verts[i]; + if(v.stuck) + { + v.pos.add(vec(unstuck).sub(v.pos).rescale(speed)); + unsticks++; + } + } +} + +void ragdolldata::updatepos() +{ + loopv(skel->verts) + { + vert &v = verts[i]; + if(v.weight) + { + v.newpos.div(v.weight); + if(!collidevert(v.newpos, vec(v.newpos).sub(v.pos), skel->verts[i].radius)) v.pos = v.newpos; + else + { + vec dir = vec(v.newpos).sub(v.oldpos); + if(dir.dot(collidewall) < 0) v.oldpos = vec(v.pos).sub(dir.reflect(collidewall)); + v.collided = true; + } + } + v.newpos = vec(0, 0, 0); + v.weight = 0; + } +} + +VAR(ragdollconstrain, 1, 5, 100); + +void ragdolldata::constrain() +{ + loopi(ragdollconstrain) + { + constraindist(); + updatepos(); + + calctris(); + constrainrot(); + updatepos(); + } +} + +FVAR(ragdollbodyfric, 0, 0.95f, 1); +FVAR(ragdollbodyfricscale, 0, 2, 10); +FVAR(ragdollwaterfric, 0, 0.85f, 1); +FVAR(ragdollgroundfric, 0, 0.8f, 1); +FVAR(ragdollairfric, 0, 0.996f, 1); +FVAR(ragdollunstick, 0, 10, 1e3f); +VAR(ragdollexpireoffset, 0, 1500, 30000); +VAR(ragdollwaterexpireoffset, 0, 3000, 30000); + +void ragdolldata::move(dynent *pl, float ts) +{ + extern const float GRAVITY; + if(collidemillis && lastmillis > collidemillis) return; + + int material = lookupmaterial(vec(center.x, center.y, center.z + radius/2)); + bool water = isliquid(material&MATF_VOLUME); + if(!pl->inwater && water) game::physicstrigger(pl, true, 0, -1, material&MATF_VOLUME); + else if(pl->inwater && !water) + { + material = lookupmaterial(center); + water = isliquid(material&MATF_VOLUME); + if(!water) game::physicstrigger(pl, true, 0, 1, pl->inwater); + } + pl->inwater = water ? material&MATF_VOLUME : MAT_AIR; + + calcrotfriction(); + float tsfric = timestep ? ts/timestep : 1, + airfric = ragdollairfric + min((ragdollbodyfricscale*collisions)/skel->verts.length(), 1.0f)*(ragdollbodyfric - ragdollairfric); + collisions = 0; + loopv(skel->verts) + { + vert &v = verts[i]; + vec dpos = vec(v.pos).sub(v.oldpos); + dpos.z -= GRAVITY*ts*ts; + if(water) dpos.z += 0.25f*sinf(detrnd(size_t(this)+i, 360)*RAD + lastmillis/10000.0f*M_PI)*ts; + dpos.mul(pow((water ? ragdollwaterfric : 1.0f) * (v.collided ? ragdollgroundfric : airfric), ts*1000.0f/ragdolltimestepmin)*tsfric); + v.oldpos = v.pos; + v.pos.add(dpos); + } + applyrotfriction(ts); + loopv(skel->verts) + { + vert &v = verts[i]; + if(v.pos.z < 0) { v.pos.z = 0; v.oldpos = v.pos; collisions++; } + vec dir = vec(v.pos).sub(v.oldpos); + v.collided = collidevert(v.pos, dir, skel->verts[i].radius); + if(v.collided) + { + v.pos = v.oldpos; + v.oldpos.sub(dir.reflect(collidewall)); + collisions++; + } + } + + if(unsticks && ragdollunstick) tryunstick(ts*ragdollunstick); + + timestep = ts; + if(collisions) + { + floating = 0; + if(!collidemillis) collidemillis = lastmillis + (water ? ragdollwaterexpireoffset : ragdollexpireoffset); + } + else if(++floating > 1 && lastmillis < collidemillis) collidemillis = 0; + + constrain(); + calctris(); + calcboundsphere(); +} + +FVAR(ragdolleyesmooth, 0, 0.5f, 1); +VAR(ragdolleyesmoothmillis, 1, 250, 10000); + +void moveragdoll(dynent *d) +{ + if(!curtime || !d->ragdoll) return; + + if(!d->ragdoll->collidemillis || lastmillis < d->ragdoll->collidemillis) + { + int lastmove = d->ragdoll->lastmove; + while(d->ragdoll->lastmove + (lastmove == d->ragdoll->lastmove ? ragdolltimestepmin : ragdolltimestepmax) <= lastmillis) + { + int timestep = min(ragdolltimestepmax, lastmillis - d->ragdoll->lastmove); + d->ragdoll->move(d, timestep/1000.0f); + d->ragdoll->lastmove += timestep; + } + } + + vec eye = d->ragdoll->skel->eye >= 0 ? d->ragdoll->verts[d->ragdoll->skel->eye].pos : d->ragdoll->center; + eye.add(d->ragdoll->offset); + float k = pow(ragdolleyesmooth, float(curtime)/ragdolleyesmoothmillis); + d->o.mul(k).add(eye.mul(1-k)); +} + +void cleanragdoll(dynent *d) +{ + DELETEP(d->ragdoll); +} + diff --git a/src/engine/rendergl.cpp b/src/engine/rendergl.cpp new file mode 100644 index 0000000..6049951 --- /dev/null +++ b/src/engine/rendergl.cpp @@ -0,0 +1,2388 @@ +// rendergl.cpp: core opengl rendering stuff + +#include "engine.h" + +bool hasVAO = false, hasFBO = false, hasAFBO = false, hasDS = false, hasTF = false, hasTRG = false, hasTSW = false, hasS3TC = false, hasFXT1 = false, hasLATC = false, hasRGTC = false, hasAF = false, hasFBB = false, hasUBO = false, hasMBR = false; + +VAR(glversion, 1, 0, 0); +VAR(glslversion, 1, 0, 0); +VAR(glcompat, 1, 0, 0); + +// OpenGL 1.3 +#ifdef WIN32 +PFNGLACTIVETEXTUREPROC glActiveTexture_ = NULL; + +PFNGLBLENDEQUATIONEXTPROC glBlendEquation_ = NULL; +PFNGLBLENDCOLOREXTPROC glBlendColor_ = NULL; + +PFNGLTEXIMAGE3DPROC glTexImage3D_ = NULL; +PFNGLTEXSUBIMAGE3DPROC glTexSubImage3D_ = NULL; +PFNGLCOPYTEXSUBIMAGE3DPROC glCopyTexSubImage3D_ = NULL; + +PFNGLCOMPRESSEDTEXIMAGE3DPROC glCompressedTexImage3D_ = NULL; +PFNGLCOMPRESSEDTEXIMAGE2DPROC glCompressedTexImage2D_ = NULL; +PFNGLCOMPRESSEDTEXSUBIMAGE3DPROC glCompressedTexSubImage3D_ = NULL; +PFNGLCOMPRESSEDTEXSUBIMAGE2DPROC glCompressedTexSubImage2D_ = NULL; +PFNGLGETCOMPRESSEDTEXIMAGEPROC glGetCompressedTexImage_ = NULL; + +PFNGLDRAWRANGEELEMENTSPROC glDrawRangeElements_ = NULL; +#endif + +// OpenGL 2.0 +#ifndef __APPLE__ +PFNGLMULTIDRAWARRAYSPROC glMultiDrawArrays_ = NULL; +PFNGLMULTIDRAWELEMENTSPROC glMultiDrawElements_ = NULL; + +PFNGLBLENDFUNCSEPARATEPROC glBlendFuncSeparate_ = NULL; +PFNGLBLENDEQUATIONSEPARATEPROC glBlendEquationSeparate_ = NULL; +PFNGLSTENCILOPSEPARATEPROC glStencilOpSeparate_ = NULL; +PFNGLSTENCILFUNCSEPARATEPROC glStencilFuncSeparate_ = NULL; +PFNGLSTENCILMASKSEPARATEPROC glStencilMaskSeparate_ = NULL; + +PFNGLGENBUFFERSPROC glGenBuffers_ = NULL; +PFNGLBINDBUFFERPROC glBindBuffer_ = NULL; +PFNGLMAPBUFFERPROC glMapBuffer_ = NULL; +PFNGLUNMAPBUFFERPROC glUnmapBuffer_ = NULL; +PFNGLBUFFERDATAPROC glBufferData_ = NULL; +PFNGLBUFFERSUBDATAPROC glBufferSubData_ = NULL; +PFNGLDELETEBUFFERSPROC glDeleteBuffers_ = NULL; +PFNGLGETBUFFERSUBDATAPROC glGetBufferSubData_ = NULL; + +PFNGLGENQUERIESPROC glGenQueries_ = NULL; +PFNGLDELETEQUERIESPROC glDeleteQueries_ = NULL; +PFNGLBEGINQUERYPROC glBeginQuery_ = NULL; +PFNGLENDQUERYPROC glEndQuery_ = NULL; +PFNGLGETQUERYIVPROC glGetQueryiv_ = NULL; +PFNGLGETQUERYOBJECTIVPROC glGetQueryObjectiv_ = NULL; +PFNGLGETQUERYOBJECTUIVPROC glGetQueryObjectuiv_ = NULL; + +PFNGLCREATEPROGRAMPROC glCreateProgram_ = NULL; +PFNGLDELETEPROGRAMPROC glDeleteProgram_ = NULL; +PFNGLUSEPROGRAMPROC glUseProgram_ = NULL; +PFNGLCREATESHADERPROC glCreateShader_ = NULL; +PFNGLDELETESHADERPROC glDeleteShader_ = NULL; +PFNGLSHADERSOURCEPROC glShaderSource_ = NULL; +PFNGLCOMPILESHADERPROC glCompileShader_ = NULL; +PFNGLGETSHADERIVPROC glGetShaderiv_ = NULL; +PFNGLGETPROGRAMIVPROC glGetProgramiv_ = NULL; +PFNGLATTACHSHADERPROC glAttachShader_ = NULL; +PFNGLGETPROGRAMINFOLOGPROC glGetProgramInfoLog_ = NULL; +PFNGLGETSHADERINFOLOGPROC glGetShaderInfoLog_ = NULL; +PFNGLLINKPROGRAMPROC glLinkProgram_ = NULL; +PFNGLGETUNIFORMLOCATIONPROC glGetUniformLocation_ = NULL; +PFNGLUNIFORM1FPROC glUniform1f_ = NULL; +PFNGLUNIFORM2FPROC glUniform2f_ = NULL; +PFNGLUNIFORM3FPROC glUniform3f_ = NULL; +PFNGLUNIFORM4FPROC glUniform4f_ = NULL; +PFNGLUNIFORM1FVPROC glUniform1fv_ = NULL; +PFNGLUNIFORM2FVPROC glUniform2fv_ = NULL; +PFNGLUNIFORM3FVPROC glUniform3fv_ = NULL; +PFNGLUNIFORM4FVPROC glUniform4fv_ = NULL; +PFNGLUNIFORM1IPROC glUniform1i_ = NULL; +PFNGLUNIFORM2IPROC glUniform2i_ = NULL; +PFNGLUNIFORM3IPROC glUniform3i_ = NULL; +PFNGLUNIFORM4IPROC glUniform4i_ = NULL; +PFNGLUNIFORM1IVPROC glUniform1iv_ = NULL; +PFNGLUNIFORM2IVPROC glUniform2iv_ = NULL; +PFNGLUNIFORM3IVPROC glUniform3iv_ = NULL; +PFNGLUNIFORM4IVPROC glUniform4iv_ = NULL; +PFNGLUNIFORMMATRIX2FVPROC glUniformMatrix2fv_ = NULL; +PFNGLUNIFORMMATRIX3FVPROC glUniformMatrix3fv_ = NULL; +PFNGLUNIFORMMATRIX4FVPROC glUniformMatrix4fv_ = NULL; +PFNGLBINDATTRIBLOCATIONPROC glBindAttribLocation_ = NULL; +PFNGLGETACTIVEUNIFORMPROC glGetActiveUniform_ = NULL; +PFNGLENABLEVERTEXATTRIBARRAYPROC glEnableVertexAttribArray_ = NULL; +PFNGLDISABLEVERTEXATTRIBARRAYPROC glDisableVertexAttribArray_ = NULL; + +PFNGLVERTEXATTRIB1FPROC glVertexAttrib1f_ = NULL; +PFNGLVERTEXATTRIB1FVPROC glVertexAttrib1fv_ = NULL; +PFNGLVERTEXATTRIB1SPROC glVertexAttrib1s_ = NULL; +PFNGLVERTEXATTRIB1SVPROC glVertexAttrib1sv_ = NULL; +PFNGLVERTEXATTRIB2FPROC glVertexAttrib2f_ = NULL; +PFNGLVERTEXATTRIB2FVPROC glVertexAttrib2fv_ = NULL; +PFNGLVERTEXATTRIB2SPROC glVertexAttrib2s_ = NULL; +PFNGLVERTEXATTRIB2SVPROC glVertexAttrib2sv_ = NULL; +PFNGLVERTEXATTRIB3FPROC glVertexAttrib3f_ = NULL; +PFNGLVERTEXATTRIB3FVPROC glVertexAttrib3fv_ = NULL; +PFNGLVERTEXATTRIB3SPROC glVertexAttrib3s_ = NULL; +PFNGLVERTEXATTRIB3SVPROC glVertexAttrib3sv_ = NULL; +PFNGLVERTEXATTRIB4FPROC glVertexAttrib4f_ = NULL; +PFNGLVERTEXATTRIB4FVPROC glVertexAttrib4fv_ = NULL; +PFNGLVERTEXATTRIB4SPROC glVertexAttrib4s_ = NULL; +PFNGLVERTEXATTRIB4SVPROC glVertexAttrib4sv_ = NULL; +PFNGLVERTEXATTRIB4BVPROC glVertexAttrib4bv_ = NULL; +PFNGLVERTEXATTRIB4IVPROC glVertexAttrib4iv_ = NULL; +PFNGLVERTEXATTRIB4UBVPROC glVertexAttrib4ubv_ = NULL; +PFNGLVERTEXATTRIB4UIVPROC glVertexAttrib4uiv_ = NULL; +PFNGLVERTEXATTRIB4USVPROC glVertexAttrib4usv_ = NULL; +PFNGLVERTEXATTRIB4NBVPROC glVertexAttrib4Nbv_ = NULL; +PFNGLVERTEXATTRIB4NIVPROC glVertexAttrib4Niv_ = NULL; +PFNGLVERTEXATTRIB4NUBPROC glVertexAttrib4Nub_ = NULL; +PFNGLVERTEXATTRIB4NUBVPROC glVertexAttrib4Nubv_ = NULL; +PFNGLVERTEXATTRIB4NUIVPROC glVertexAttrib4Nuiv_ = NULL; +PFNGLVERTEXATTRIB4NUSVPROC glVertexAttrib4Nusv_ = NULL; +PFNGLVERTEXATTRIBPOINTERPROC glVertexAttribPointer_ = NULL; + +PFNGLDRAWBUFFERSPROC glDrawBuffers_ = NULL; +#endif + +// OpenGL 3.0 +PFNGLGETSTRINGIPROC glGetStringi_ = NULL; +PFNGLBINDFRAGDATALOCATIONPROC glBindFragDataLocation_ = NULL; + +// GL_EXT_framebuffer_object +PFNGLBINDRENDERBUFFERPROC glBindRenderbuffer_ = NULL; +PFNGLDELETERENDERBUFFERSPROC glDeleteRenderbuffers_ = NULL; +PFNGLGENFRAMEBUFFERSPROC glGenRenderbuffers_ = NULL; +PFNGLRENDERBUFFERSTORAGEPROC glRenderbufferStorage_ = NULL; +PFNGLCHECKFRAMEBUFFERSTATUSPROC glCheckFramebufferStatus_ = NULL; +PFNGLBINDFRAMEBUFFERPROC glBindFramebuffer_ = NULL; +PFNGLDELETEFRAMEBUFFERSPROC glDeleteFramebuffers_ = NULL; +PFNGLGENFRAMEBUFFERSPROC glGenFramebuffers_ = NULL; +PFNGLFRAMEBUFFERTEXTURE2DPROC glFramebufferTexture2D_ = NULL; +PFNGLFRAMEBUFFERRENDERBUFFERPROC glFramebufferRenderbuffer_ = NULL; +PFNGLGENERATEMIPMAPPROC glGenerateMipmap_ = NULL; + +// GL_EXT_framebuffer_blit +PFNGLBLITFRAMEBUFFERPROC glBlitFramebuffer_ = NULL; + +// GL_ARB_uniform_buffer_object +PFNGLGETUNIFORMINDICESPROC glGetUniformIndices_ = NULL; +PFNGLGETACTIVEUNIFORMSIVPROC glGetActiveUniformsiv_ = NULL; +PFNGLGETUNIFORMBLOCKINDEXPROC glGetUniformBlockIndex_ = NULL; +PFNGLGETACTIVEUNIFORMBLOCKIVPROC glGetActiveUniformBlockiv_ = NULL; +PFNGLUNIFORMBLOCKBINDINGPROC glUniformBlockBinding_ = NULL; +PFNGLBINDBUFFERBASEPROC glBindBufferBase_ = NULL; +PFNGLBINDBUFFERRANGEPROC glBindBufferRange_ = NULL; + +// GL_ARB_map_buffer_range +PFNGLMAPBUFFERRANGEPROC glMapBufferRange_ = NULL; +PFNGLFLUSHMAPPEDBUFFERRANGEPROC glFlushMappedBufferRange_ = NULL; + +// GL_ARB_vertex_array_object +PFNGLBINDVERTEXARRAYPROC glBindVertexArray_ = NULL; +PFNGLDELETEVERTEXARRAYSPROC glDeleteVertexArrays_ = NULL; +PFNGLGENVERTEXARRAYSPROC glGenVertexArrays_ = NULL; +PFNGLISVERTEXARRAYPROC glIsVertexArray_ = NULL; + +void *getprocaddress(const char *name) +{ + return SDL_GL_GetProcAddress(name); +} + +VARP(ati_skybox_bug, 0, 0, 1); +VAR(ati_minmax_bug, 0, 0, 1); +VAR(ati_cubemap_bug, 0, 0, 1); +VAR(intel_vertexarray_bug, 0, 0, 1); +VAR(intel_mapbufferrange_bug, 0, 0, 1); +VAR(mesa_swap_bug, 0, 0, 1); +VAR(minimizetcusage, 1, 0, 0); +VAR(useubo, 1, 0, 0); +VAR(usetexcompress, 1, 0, 0); +VAR(rtscissor, 0, 1, 1); +VAR(blurtile, 0, 1, 1); +VAR(rtsharefb, 0, 1, 1); + +VAR(dbgexts, 0, 0, 1); + +hashset glexts; + +void parseglexts() +{ + if(glversion >= 300) + { + GLint numexts = 0; + glGetIntegerv(GL_NUM_EXTENSIONS, &numexts); + loopi(numexts) + { + const char *ext = (const char *)glGetStringi_(GL_EXTENSIONS, i); + glexts.add(newstring(ext)); + } + } + else + { + const char *exts = (const char *)glGetString(GL_EXTENSIONS); + for(;;) + { + while(*exts == ' ') exts++; + if(!*exts) break; + const char *ext = exts; + while(*exts && *exts != ' ') exts++; + if(exts > ext) glexts.add(newstring(ext, size_t(exts-ext))); + } + } +} + +bool hasext(const char *ext) +{ + return glexts.access(ext)!=NULL; +} + +void gl_checkextensions() +{ + const char *vendor = (const char *)glGetString(GL_VENDOR); + const char *renderer = (const char *)glGetString(GL_RENDERER); + const char *version = (const char *)glGetString(GL_VERSION); + conoutf(CON_INIT, "Renderer: %s (%s)", renderer, vendor); + conoutf(CON_INIT, "Driver: %s", version); + + bool mesa = false, intel = false, ati = false, nvidia = false; + if(strstr(renderer, "Mesa") || strstr(version, "Mesa")) + { + mesa = true; + if(strstr(renderer, "Intel")) intel = true; + } + else if(strstr(vendor, "NVIDIA")) + nvidia = true; + else if(strstr(vendor, "ATI") || strstr(vendor, "Advanced Micro Devices")) + ati = true; + else if(strstr(vendor, "Intel")) + intel = true; + + uint glmajorversion, glminorversion; + if(sscanf(version, " %u.%u", &glmajorversion, &glminorversion) != 2) glversion = 100; + else glversion = glmajorversion*100 + glminorversion*10; + + if(glversion < 200) fatal("OpenGL 2.0 or greater is required!"); + +#ifdef WIN32 + glActiveTexture_ = (PFNGLACTIVETEXTUREPROC) getprocaddress("glActiveTexture"); + + glBlendEquation_ = (PFNGLBLENDEQUATIONPROC) getprocaddress("glBlendEquation"); + glBlendColor_ = (PFNGLBLENDCOLORPROC) getprocaddress("glBlendColor"); + + glTexImage3D_ = (PFNGLTEXIMAGE3DPROC) getprocaddress("glTexImage3D"); + glTexSubImage3D_ = (PFNGLTEXSUBIMAGE3DPROC) getprocaddress("glTexSubImage3D"); + glCopyTexSubImage3D_ = (PFNGLCOPYTEXSUBIMAGE3DPROC) getprocaddress("glCopyTexSubImage3D"); + + glCompressedTexImage3D_ = (PFNGLCOMPRESSEDTEXIMAGE3DPROC) getprocaddress("glCompressedTexImage3D"); + glCompressedTexImage2D_ = (PFNGLCOMPRESSEDTEXIMAGE2DPROC) getprocaddress("glCompressedTexImage2D"); + glCompressedTexSubImage3D_ = (PFNGLCOMPRESSEDTEXSUBIMAGE3DPROC) getprocaddress("glCompressedTexSubImage3D"); + glCompressedTexSubImage2D_ = (PFNGLCOMPRESSEDTEXSUBIMAGE2DPROC) getprocaddress("glCompressedTexSubImage2D"); + glGetCompressedTexImage_ = (PFNGLGETCOMPRESSEDTEXIMAGEPROC) getprocaddress("glGetCompressedTexImage"); + + glDrawRangeElements_ = (PFNGLDRAWRANGEELEMENTSPROC) getprocaddress("glDrawRangeElements"); +#endif + +#ifndef __APPLE__ + glMultiDrawArrays_ = (PFNGLMULTIDRAWARRAYSPROC) getprocaddress("glMultiDrawArrays"); + glMultiDrawElements_ = (PFNGLMULTIDRAWELEMENTSPROC) getprocaddress("glMultiDrawElements"); + + glBlendFuncSeparate_ = (PFNGLBLENDFUNCSEPARATEPROC) getprocaddress("glBlendFuncSeparate"); + glBlendEquationSeparate_ = (PFNGLBLENDEQUATIONSEPARATEPROC) getprocaddress("glBlendEquationSeparate"); + glStencilOpSeparate_ = (PFNGLSTENCILOPSEPARATEPROC) getprocaddress("glStencilOpSeparate"); + glStencilFuncSeparate_ = (PFNGLSTENCILFUNCSEPARATEPROC) getprocaddress("glStencilFuncSeparate"); + glStencilMaskSeparate_ = (PFNGLSTENCILMASKSEPARATEPROC) getprocaddress("glStencilMaskSeparate"); + + glGenBuffers_ = (PFNGLGENBUFFERSPROC) getprocaddress("glGenBuffers"); + glBindBuffer_ = (PFNGLBINDBUFFERPROC) getprocaddress("glBindBuffer"); + glMapBuffer_ = (PFNGLMAPBUFFERPROC) getprocaddress("glMapBuffer"); + glUnmapBuffer_ = (PFNGLUNMAPBUFFERPROC) getprocaddress("glUnmapBuffer"); + glBufferData_ = (PFNGLBUFFERDATAPROC) getprocaddress("glBufferData"); + glBufferSubData_ = (PFNGLBUFFERSUBDATAPROC) getprocaddress("glBufferSubData"); + glDeleteBuffers_ = (PFNGLDELETEBUFFERSPROC) getprocaddress("glDeleteBuffers"); + glGetBufferSubData_ = (PFNGLGETBUFFERSUBDATAPROC) getprocaddress("glGetBufferSubData"); + + glGetQueryiv_ = (PFNGLGETQUERYIVPROC) getprocaddress("glGetQueryiv"); + glGenQueries_ = (PFNGLGENQUERIESPROC) getprocaddress("glGenQueries"); + glDeleteQueries_ = (PFNGLDELETEQUERIESPROC) getprocaddress("glDeleteQueries"); + glBeginQuery_ = (PFNGLBEGINQUERYPROC) getprocaddress("glBeginQuery"); + glEndQuery_ = (PFNGLENDQUERYPROC) getprocaddress("glEndQuery"); + glGetQueryObjectiv_ = (PFNGLGETQUERYOBJECTIVPROC) getprocaddress("glGetQueryObjectiv"); + glGetQueryObjectuiv_ = (PFNGLGETQUERYOBJECTUIVPROC) getprocaddress("glGetQueryObjectuiv"); + + glCreateProgram_ = (PFNGLCREATEPROGRAMPROC) getprocaddress("glCreateProgram"); + glDeleteProgram_ = (PFNGLDELETEPROGRAMPROC) getprocaddress("glDeleteProgram"); + glUseProgram_ = (PFNGLUSEPROGRAMPROC) getprocaddress("glUseProgram"); + glCreateShader_ = (PFNGLCREATESHADERPROC) getprocaddress("glCreateShader"); + glDeleteShader_ = (PFNGLDELETESHADERPROC) getprocaddress("glDeleteShader"); + glShaderSource_ = (PFNGLSHADERSOURCEPROC) getprocaddress("glShaderSource"); + glCompileShader_ = (PFNGLCOMPILESHADERPROC) getprocaddress("glCompileShader"); + glGetShaderiv_ = (PFNGLGETSHADERIVPROC) getprocaddress("glGetShaderiv"); + glGetProgramiv_ = (PFNGLGETPROGRAMIVPROC) getprocaddress("glGetProgramiv"); + glAttachShader_ = (PFNGLATTACHSHADERPROC) getprocaddress("glAttachShader"); + glGetProgramInfoLog_ = (PFNGLGETPROGRAMINFOLOGPROC) getprocaddress("glGetProgramInfoLog"); + glGetShaderInfoLog_ = (PFNGLGETSHADERINFOLOGPROC) getprocaddress("glGetShaderInfoLog"); + glLinkProgram_ = (PFNGLLINKPROGRAMPROC) getprocaddress("glLinkProgram"); + glGetUniformLocation_ = (PFNGLGETUNIFORMLOCATIONPROC) getprocaddress("glGetUniformLocation"); + glUniform1f_ = (PFNGLUNIFORM1FPROC) getprocaddress("glUniform1f"); + glUniform2f_ = (PFNGLUNIFORM2FPROC) getprocaddress("glUniform2f"); + glUniform3f_ = (PFNGLUNIFORM3FPROC) getprocaddress("glUniform3f"); + glUniform4f_ = (PFNGLUNIFORM4FPROC) getprocaddress("glUniform4f"); + glUniform1fv_ = (PFNGLUNIFORM1FVPROC) getprocaddress("glUniform1fv"); + glUniform2fv_ = (PFNGLUNIFORM2FVPROC) getprocaddress("glUniform2fv"); + glUniform3fv_ = (PFNGLUNIFORM3FVPROC) getprocaddress("glUniform3fv"); + glUniform4fv_ = (PFNGLUNIFORM4FVPROC) getprocaddress("glUniform4fv"); + glUniform1i_ = (PFNGLUNIFORM1IPROC) getprocaddress("glUniform1i"); + glUniform2i_ = (PFNGLUNIFORM2IPROC) getprocaddress("glUniform2i"); + glUniform3i_ = (PFNGLUNIFORM3IPROC) getprocaddress("glUniform3i"); + glUniform4i_ = (PFNGLUNIFORM4IPROC) getprocaddress("glUniform4i"); + glUniform1iv_ = (PFNGLUNIFORM1IVPROC) getprocaddress("glUniform1iv"); + glUniform2iv_ = (PFNGLUNIFORM2IVPROC) getprocaddress("glUniform2iv"); + glUniform3iv_ = (PFNGLUNIFORM3IVPROC) getprocaddress("glUniform3iv"); + glUniform4iv_ = (PFNGLUNIFORM4IVPROC) getprocaddress("glUniform4iv"); + glUniformMatrix2fv_ = (PFNGLUNIFORMMATRIX2FVPROC) getprocaddress("glUniformMatrix2fv"); + glUniformMatrix3fv_ = (PFNGLUNIFORMMATRIX3FVPROC) getprocaddress("glUniformMatrix3fv"); + glUniformMatrix4fv_ = (PFNGLUNIFORMMATRIX4FVPROC) getprocaddress("glUniformMatrix4fv"); + glBindAttribLocation_ = (PFNGLBINDATTRIBLOCATIONPROC) getprocaddress("glBindAttribLocation"); + glGetActiveUniform_ = (PFNGLGETACTIVEUNIFORMPROC) getprocaddress("glGetActiveUniform"); + glEnableVertexAttribArray_ = (PFNGLENABLEVERTEXATTRIBARRAYPROC) getprocaddress("glEnableVertexAttribArray"); + glDisableVertexAttribArray_ = (PFNGLDISABLEVERTEXATTRIBARRAYPROC) getprocaddress("glDisableVertexAttribArray"); + + glVertexAttrib1f_ = (PFNGLVERTEXATTRIB1FPROC) getprocaddress("glVertexAttrib1f"); + glVertexAttrib1fv_ = (PFNGLVERTEXATTRIB1FVPROC) getprocaddress("glVertexAttrib1fv"); + glVertexAttrib1s_ = (PFNGLVERTEXATTRIB1SPROC) getprocaddress("glVertexAttrib1s"); + glVertexAttrib1sv_ = (PFNGLVERTEXATTRIB1SVPROC) getprocaddress("glVertexAttrib1sv"); + glVertexAttrib2f_ = (PFNGLVERTEXATTRIB2FPROC) getprocaddress("glVertexAttrib2f"); + glVertexAttrib2fv_ = (PFNGLVERTEXATTRIB2FVPROC) getprocaddress("glVertexAttrib2fv"); + glVertexAttrib2s_ = (PFNGLVERTEXATTRIB2SPROC) getprocaddress("glVertexAttrib2s"); + glVertexAttrib2sv_ = (PFNGLVERTEXATTRIB2SVPROC) getprocaddress("glVertexAttrib2sv"); + glVertexAttrib3f_ = (PFNGLVERTEXATTRIB3FPROC) getprocaddress("glVertexAttrib3f"); + glVertexAttrib3fv_ = (PFNGLVERTEXATTRIB3FVPROC) getprocaddress("glVertexAttrib3fv"); + glVertexAttrib3s_ = (PFNGLVERTEXATTRIB3SPROC) getprocaddress("glVertexAttrib3s"); + glVertexAttrib3sv_ = (PFNGLVERTEXATTRIB3SVPROC) getprocaddress("glVertexAttrib3sv"); + glVertexAttrib4f_ = (PFNGLVERTEXATTRIB4FPROC) getprocaddress("glVertexAttrib4f"); + glVertexAttrib4fv_ = (PFNGLVERTEXATTRIB4FVPROC) getprocaddress("glVertexAttrib4fv"); + glVertexAttrib4s_ = (PFNGLVERTEXATTRIB4SPROC) getprocaddress("glVertexAttrib4s"); + glVertexAttrib4sv_ = (PFNGLVERTEXATTRIB4SVPROC) getprocaddress("glVertexAttrib4sv"); + glVertexAttrib4bv_ = (PFNGLVERTEXATTRIB4BVPROC) getprocaddress("glVertexAttrib4bv"); + glVertexAttrib4iv_ = (PFNGLVERTEXATTRIB4IVPROC) getprocaddress("glVertexAttrib4iv"); + glVertexAttrib4ubv_ = (PFNGLVERTEXATTRIB4UBVPROC) getprocaddress("glVertexAttrib4ubv"); + glVertexAttrib4uiv_ = (PFNGLVERTEXATTRIB4UIVPROC) getprocaddress("glVertexAttrib4uiv"); + glVertexAttrib4usv_ = (PFNGLVERTEXATTRIB4USVPROC) getprocaddress("glVertexAttrib4usv"); + glVertexAttrib4Nbv_ = (PFNGLVERTEXATTRIB4NBVPROC) getprocaddress("glVertexAttrib4Nbv"); + glVertexAttrib4Niv_ = (PFNGLVERTEXATTRIB4NIVPROC) getprocaddress("glVertexAttrib4Niv"); + glVertexAttrib4Nub_ = (PFNGLVERTEXATTRIB4NUBPROC) getprocaddress("glVertexAttrib4Nub"); + glVertexAttrib4Nubv_ = (PFNGLVERTEXATTRIB4NUBVPROC) getprocaddress("glVertexAttrib4Nubv"); + glVertexAttrib4Nuiv_ = (PFNGLVERTEXATTRIB4NUIVPROC) getprocaddress("glVertexAttrib4Nuiv"); + glVertexAttrib4Nusv_ = (PFNGLVERTEXATTRIB4NUSVPROC) getprocaddress("glVertexAttrib4Nusv"); + glVertexAttribPointer_ = (PFNGLVERTEXATTRIBPOINTERPROC) getprocaddress("glVertexAttribPointer"); + + glDrawBuffers_ = (PFNGLDRAWBUFFERSPROC) getprocaddress("glDrawBuffers"); +#endif + + if(glversion >= 300) + { + glGetStringi_ = (PFNGLGETSTRINGIPROC) getprocaddress("glGetStringi"); + glBindFragDataLocation_ = (PFNGLBINDFRAGDATALOCATIONPROC)getprocaddress("glBindFragDataLocation"); + } + + const char *glslstr = (const char *)glGetString(GL_SHADING_LANGUAGE_VERSION); + uint glslmajorversion, glslminorversion; + if(glslstr && sscanf(glslstr, " %u.%u", &glslmajorversion, &glslminorversion) == 2) glslversion = glslmajorversion*100 + glslminorversion; + + if(glslversion < 120) fatal("GLSL 1.20 or greater is required!"); + + parseglexts(); + + GLint val; + glGetIntegerv(GL_MAX_TEXTURE_SIZE, &val); + hwtexsize = val; + glGetIntegerv(GL_MAX_CUBE_MAP_TEXTURE_SIZE, &val); + hwcubetexsize = val; + + if(glversion >= 300 || hasext("GL_ARB_texture_float") || hasext("GL_ATI_texture_float")) + { + hasTF = true; + if(glversion < 300 && dbgexts) conoutf(CON_INIT, "Using GL_ARB_texture_float extension."); + shadowmap = 1; + extern int smoothshadowmappeel; + smoothshadowmappeel = 1; + } + + if(glversion >= 300 || hasext("GL_ARB_texture_rg")) + { + hasTRG = true; + if(glversion < 300 && dbgexts) conoutf(CON_INIT, "Using GL_ARB_texture_rg extension."); + } + + if(glversion >= 300 || hasext("GL_ARB_framebuffer_object")) + { + glBindRenderbuffer_ = (PFNGLBINDRENDERBUFFERPROC) getprocaddress("glBindRenderbuffer"); + glDeleteRenderbuffers_ = (PFNGLDELETERENDERBUFFERSPROC) getprocaddress("glDeleteRenderbuffers"); + glGenRenderbuffers_ = (PFNGLGENFRAMEBUFFERSPROC) getprocaddress("glGenRenderbuffers"); + glRenderbufferStorage_ = (PFNGLRENDERBUFFERSTORAGEPROC) getprocaddress("glRenderbufferStorage"); + glCheckFramebufferStatus_ = (PFNGLCHECKFRAMEBUFFERSTATUSPROC) getprocaddress("glCheckFramebufferStatus"); + glBindFramebuffer_ = (PFNGLBINDFRAMEBUFFERPROC) getprocaddress("glBindFramebuffer"); + glDeleteFramebuffers_ = (PFNGLDELETEFRAMEBUFFERSPROC) getprocaddress("glDeleteFramebuffers"); + glGenFramebuffers_ = (PFNGLGENFRAMEBUFFERSPROC) getprocaddress("glGenFramebuffers"); + glFramebufferTexture2D_ = (PFNGLFRAMEBUFFERTEXTURE2DPROC) getprocaddress("glFramebufferTexture2D"); + glFramebufferRenderbuffer_ = (PFNGLFRAMEBUFFERRENDERBUFFERPROC)getprocaddress("glFramebufferRenderbuffer"); + glGenerateMipmap_ = (PFNGLGENERATEMIPMAPPROC) getprocaddress("glGenerateMipmap"); + glBlitFramebuffer_ = (PFNGLBLITFRAMEBUFFERPROC) getprocaddress("glBlitFramebuffer"); + hasAFBO = hasFBO = hasFBB = hasDS = true; + if(glversion < 300 && dbgexts) conoutf(CON_INIT, "Using GL_ARB_framebuffer_object extension."); + } + else if(hasext("GL_EXT_framebuffer_object")) + { + glBindRenderbuffer_ = (PFNGLBINDRENDERBUFFERPROC) getprocaddress("glBindRenderbufferEXT"); + glDeleteRenderbuffers_ = (PFNGLDELETERENDERBUFFERSPROC) getprocaddress("glDeleteRenderbuffersEXT"); + glGenRenderbuffers_ = (PFNGLGENFRAMEBUFFERSPROC) getprocaddress("glGenRenderbuffersEXT"); + glRenderbufferStorage_ = (PFNGLRENDERBUFFERSTORAGEPROC) getprocaddress("glRenderbufferStorageEXT"); + glCheckFramebufferStatus_ = (PFNGLCHECKFRAMEBUFFERSTATUSPROC) getprocaddress("glCheckFramebufferStatusEXT"); + glBindFramebuffer_ = (PFNGLBINDFRAMEBUFFERPROC) getprocaddress("glBindFramebufferEXT"); + glDeleteFramebuffers_ = (PFNGLDELETEFRAMEBUFFERSPROC) getprocaddress("glDeleteFramebuffersEXT"); + glGenFramebuffers_ = (PFNGLGENFRAMEBUFFERSPROC) getprocaddress("glGenFramebuffersEXT"); + glFramebufferTexture2D_ = (PFNGLFRAMEBUFFERTEXTURE2DPROC) getprocaddress("glFramebufferTexture2DEXT"); + glFramebufferRenderbuffer_ = (PFNGLFRAMEBUFFERRENDERBUFFERPROC)getprocaddress("glFramebufferRenderbufferEXT"); + glGenerateMipmap_ = (PFNGLGENERATEMIPMAPPROC) getprocaddress("glGenerateMipmapEXT"); + hasFBO = true; + if(dbgexts) conoutf(CON_INIT, "Using GL_EXT_framebuffer_object extension."); + + if(hasext("GL_EXT_framebuffer_blit")) + { + glBlitFramebuffer_ = (PFNGLBLITFRAMEBUFFERPROC) getprocaddress("glBlitFramebufferEXT"); + hasFBB = true; + if(dbgexts) conoutf(CON_INIT, "Using GL_EXT_framebuffer_blit extension."); + } + + if(hasext("GL_EXT_packed_depth_stencil") || hasext("GL_NV_packed_depth_stencil")) + { + hasDS = true; + if(dbgexts) conoutf(CON_INIT, "Using GL_EXT_packed_depth_stencil extension."); + } + } + else fatal("Framebuffer object support is required!"); + + extern int fpdepthfx; + if(ati) + { + //conoutf(CON_WARN, "WARNING: ATI cards may show garbage in skybox. (use \"/ati_skybox_bug 1\" to fix)"); + + minimizetcusage = 1; + if(hasTF && hasTRG) fpdepthfx = 1; + // On Catalyst 10.2, issuing an occlusion query on the first draw using a given cubemap texture causes a nasty crash + ati_cubemap_bug = 1; + } + else if(nvidia) + { + reservevpparams = 10; + rtsharefb = 0; // work-around for strange driver stalls involving when using many FBOs + extern int filltjoints; + if(glversion < 300 && !hasext("GL_EXT_gpu_shader4")) filltjoints = 0; // DX9 or less NV cards seem to not cause many sparklies + + if(hasTF && hasTRG) fpdepthfx = 1; + } + else + { + if(intel) + { +#ifdef WIN32 + intel_vertexarray_bug = 1; + // MapBufferRange is buggy on older Intel drivers on Windows + if(glversion <= 310) intel_mapbufferrange_bug = 1; +#endif + } + + reservevpparams = 20; + + if(mesa) mesa_swap_bug = 1; + } + + if(glversion >= 300 || hasext("GL_ARB_map_buffer_range")) + { + glMapBufferRange_ = (PFNGLMAPBUFFERRANGEPROC) getprocaddress("glMapBufferRange"); + glFlushMappedBufferRange_ = (PFNGLFLUSHMAPPEDBUFFERRANGEPROC)getprocaddress("glFlushMappedBufferRange"); + hasMBR = true; + if(glversion < 300 && dbgexts) conoutf(CON_INIT, "Using GL_ARB_map_buffer_range."); + } + + if(glversion >= 310 || hasext("GL_ARB_uniform_buffer_object")) + { + glGetUniformIndices_ = (PFNGLGETUNIFORMINDICESPROC) getprocaddress("glGetUniformIndices"); + glGetActiveUniformsiv_ = (PFNGLGETACTIVEUNIFORMSIVPROC) getprocaddress("glGetActiveUniformsiv"); + glGetUniformBlockIndex_ = (PFNGLGETUNIFORMBLOCKINDEXPROC) getprocaddress("glGetUniformBlockIndex"); + glGetActiveUniformBlockiv_ = (PFNGLGETACTIVEUNIFORMBLOCKIVPROC)getprocaddress("glGetActiveUniformBlockiv"); + glUniformBlockBinding_ = (PFNGLUNIFORMBLOCKBINDINGPROC) getprocaddress("glUniformBlockBinding"); + glBindBufferBase_ = (PFNGLBINDBUFFERBASEPROC) getprocaddress("glBindBufferBase"); + glBindBufferRange_ = (PFNGLBINDBUFFERRANGEPROC) getprocaddress("glBindBufferRange"); + + useubo = 1; + hasUBO = true; + if(glversion < 310 && dbgexts) conoutf(CON_INIT, "Using GL_ARB_uniform_buffer_object extension."); + } + + if(glversion >= 300 || hasext("GL_ARB_vertex_array_object")) + { + glBindVertexArray_ = (PFNGLBINDVERTEXARRAYPROC) getprocaddress("glBindVertexArray"); + glDeleteVertexArrays_ = (PFNGLDELETEVERTEXARRAYSPROC)getprocaddress("glDeleteVertexArrays"); + glGenVertexArrays_ = (PFNGLGENVERTEXARRAYSPROC) getprocaddress("glGenVertexArrays"); + glIsVertexArray_ = (PFNGLISVERTEXARRAYPROC) getprocaddress("glIsVertexArray"); + hasVAO = true; + if(glversion < 300 && dbgexts) conoutf(CON_INIT, "Using GL_ARB_vertex_array_object extension."); + } + else if(hasext("GL_APPLE_vertex_array_object")) + { + glBindVertexArray_ = (PFNGLBINDVERTEXARRAYPROC) getprocaddress("glBindVertexArrayAPPLE"); + glDeleteVertexArrays_ = (PFNGLDELETEVERTEXARRAYSPROC)getprocaddress("glDeleteVertexArraysAPPLE"); + glGenVertexArrays_ = (PFNGLGENVERTEXARRAYSPROC) getprocaddress("glGenVertexArraysAPPLE"); + glIsVertexArray_ = (PFNGLISVERTEXARRAYPROC) getprocaddress("glIsVertexArrayAPPLE"); + hasVAO = true; + if(dbgexts) conoutf(CON_INIT, "Using GL_APPLE_vertex_array_object extension."); + } + + if(glversion >= 330 || hasext("GL_ARB_texture_swizzle") || hasext("GL_EXT_texture_swizzle")) + { + hasTSW = true; + if(glversion < 330 && dbgexts) conoutf(CON_INIT, "Using GL_ARB_texture_swizzle extension."); + } + + if(hasext("GL_EXT_texture_compression_s3tc")) + { + hasS3TC = true; +#ifdef __APPLE__ + usetexcompress = 1; +#else + if(!mesa) usetexcompress = 2; +#endif + if(dbgexts) conoutf(CON_INIT, "Using GL_EXT_texture_compression_s3tc extension."); + } + else if(hasext("GL_EXT_texture_compression_dxt1") && hasext("GL_ANGLE_texture_compression_dxt3") && hasext("GL_ANGLE_texture_compression_dxt5")) + { + hasS3TC = true; + if(dbgexts) conoutf(CON_INIT, "Using GL_EXT_texture_compression_dxt1 extension."); + } + if(hasext("GL_3DFX_texture_compression_FXT1")) + { + hasFXT1 = true; + if(mesa) usetexcompress = max(usetexcompress, 1); + if(dbgexts) conoutf(CON_INIT, "Using GL_3DFX_texture_compression_FXT1."); + } + if(hasext("GL_EXT_texture_compression_latc")) + { + hasLATC = true; + if(dbgexts) conoutf(CON_INIT, "Using GL_EXT_texture_compression_latc extension."); + } + if(glversion >= 300 || hasext("GL_ARB_texture_compression_rgtc") || hasext("GL_EXT_texture_compression_rgtc")) + { + hasRGTC = true; + if(glversion < 300 && dbgexts) conoutf(CON_INIT, "Using GL_ARB_texture_compression_rgtc extension."); + } + + if(hasext("GL_EXT_texture_filter_anisotropic")) + { + GLint val; + glGetIntegerv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &val); + hwmaxaniso = val; + hasAF = true; + if(dbgexts) conoutf(CON_INIT, "Using GL_EXT_texture_filter_anisotropic extension."); + } + + if(glversion >= 300 || hasext("GL_EXT_gpu_shader4")) + { + // on DX10 or above class cards (i.e. GF8 or RadeonHD) enable expensive features + extern int grass, glare, maxdynlights, depthfxsize, blurdepthfx, texcompress; + grass = 1; + waterfallrefract = 1; + glare = 1; + maxdynlights = MAXDYNLIGHTS; + depthfxsize = 10; + blurdepthfx = 0; + texcompress = max(texcompress, 1024 + 1); + } +} + +void glext(char *ext) +{ + intret(hasext(ext) ? 1 : 0); +} +COMMAND(glext, "s"); + +void gl_resize() +{ + glViewport(0, 0, screenw, screenh); +} + +void gl_init() +{ + glClearColor(0, 0, 0, 0); + glClearDepth(1); + glDepthFunc(GL_LESS); + glDisable(GL_DEPTH_TEST); + + glEnable(GL_LINE_SMOOTH); + //glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); + + glFrontFace(GL_CW); + glCullFace(GL_BACK); + glDisable(GL_CULL_FACE); + + gle::setup(); + + setupshaders(); + + setuptexcompress(); + + gl_resize(); +} + +VAR(wireframe, 0, 0, 1); + +ICOMMAND(getcamyaw, "", (), floatret(camera1->yaw)); +ICOMMAND(getcampitch, "", (), floatret(camera1->pitch)); +ICOMMAND(getcamroll, "", (), floatret(camera1->roll)); +ICOMMAND(getcampos, "", (), +{ + defformatstring(pos, "%s %s %s", floatstr(camera1->o.x), floatstr(camera1->o.y), floatstr(camera1->o.z)); + result(pos); +}); + +vec worldpos, camdir, camright, camup; + +void setcammatrix() +{ + // move from RH to Z-up LH quake style worldspace + cammatrix = viewmatrix; + cammatrix.rotate_around_y(camera1->roll*RAD); + cammatrix.rotate_around_x(camera1->pitch*-RAD); + cammatrix.rotate_around_z(camera1->yaw*-RAD); + cammatrix.translate(vec(camera1->o).neg()); + + cammatrix.transposedtransformnormal(vec(viewmatrix.b), camdir); + cammatrix.transposedtransformnormal(vec(viewmatrix.a).neg(), camright); + cammatrix.transposedtransformnormal(vec(viewmatrix.c), camup); + + if(!drawtex) + { + if(raycubepos(camera1->o, camdir, worldpos, 0, RAY_CLIPMAT|RAY_SKIPFIRST) == -1) + worldpos = vec(camdir).mul(2*worldsize).add(camera1->o); // if nothing is hit, just far away in the view direction + } +} + +void setcamprojmatrix(bool init = true, bool flush = false) +{ + if(init) + { + setcammatrix(); + } + + camprojmatrix.muld(projmatrix, cammatrix); + + if(init) + { + invcammatrix.invert(cammatrix); + invcamprojmatrix.invert(camprojmatrix); + } + + GLOBALPARAM(camprojmatrix, camprojmatrix); + + if(fogging) + { + vec fogplane(cammatrix.c); + fogplane.x /= projmatrix.a.x; + fogplane.y /= projmatrix.b.y; + fogplane.z /= projmatrix.c.w; + GLOBALPARAMF(fogplane, fogplane.x, fogplane.y, 0, fogplane.z); + } + else + { + vec2 lineardepthscale = projmatrix.lineardepthscale(); + GLOBALPARAMF(fogplane, 0, 0, lineardepthscale.x, lineardepthscale.y); + } + + if(flush && Shader::lastshader) Shader::lastshader->flushparams(); +} + +matrix4 hudmatrix, hudmatrixstack[64]; +int hudmatrixpos = 0; + +void resethudmatrix() +{ + hudmatrixpos = 0; + GLOBALPARAM(hudmatrix, hudmatrix); +} + +void pushhudmatrix() +{ + if(hudmatrixpos >= 0 && hudmatrixpos < int(sizeof(hudmatrixstack)/sizeof(hudmatrixstack[0]))) hudmatrixstack[hudmatrixpos] = hudmatrix; + ++hudmatrixpos; +} + +void flushhudmatrix(bool flushparams) +{ + GLOBALPARAM(hudmatrix, hudmatrix); + if(flushparams && Shader::lastshader) Shader::lastshader->flushparams(); +} + +void pophudmatrix(bool flush, bool flushparams) +{ + --hudmatrixpos; + if(hudmatrixpos >= 0 && hudmatrixpos < int(sizeof(hudmatrixstack)/sizeof(hudmatrixstack[0]))) + { + hudmatrix = hudmatrixstack[hudmatrixpos]; + if(flush) flushhudmatrix(flushparams); + } +} + +void pushhudscale(float sx, float sy) +{ + if(!sy) sy = sx; + pushhudmatrix(); + hudmatrix.scale(sx, sy, 1); + flushhudmatrix(); +} + +void pushhudtranslate(float tx, float ty, float sx, float sy) +{ + if(!sy) sy = sx; + pushhudmatrix(); + hudmatrix.translate(tx, ty, 0); + if(sy) hudmatrix.scale(sx, sy, 1); + flushhudmatrix(); +} + +float curfov = 100, curavatarfov = 65, fovy, aspect; +int farplane; +VARP(zoominvel, 0, 250, 5000); +VARP(zoomoutvel, 0, 100, 5000); +VARP(zoomfov, 10, 35, 60); +VARP(fov, 10, 100, 150); +VAR(avatarzoomfov, 10, 25, 60); +VAR(avatarfov, 10, 65, 150); +FVAR(avatardepth, 0, 0.5f, 1); +FVARNP(aspect, forceaspect, 0, 0, 1e3f); + +static float zoomprogress = 0; +VAR(zoom, -1, 0, 1); + +void disablezoom() +{ + zoom = 0; + zoomprogress = 0; +} + +void computezoom() +{ + if(!zoom) { zoomprogress = 0; curfov = fov; curavatarfov = avatarfov; return; } + if(zoom > 0) zoomprogress = zoominvel ? min(zoomprogress + float(elapsedtime) / zoominvel, 1.0f) : 1; + else + { + zoomprogress = zoomoutvel ? max(zoomprogress - float(elapsedtime) / zoomoutvel, 0.0f) : 0; + if(zoomprogress <= 0) zoom = 0; + } + curfov = zoomfov*zoomprogress + fov*(1 - zoomprogress); + curavatarfov = avatarzoomfov*zoomprogress + avatarfov*(1 - zoomprogress); +} + +FVARP(zoomsens, 1e-3f, 1, 1000); +FVARP(zoomaccel, 0, 0, 1000); +VARP(zoomautosens, 0, 1, 1); +FVARP(sensitivity, 1e-3f, 3, 1000); +FVARP(sensitivityscale, 1e-3f, 1, 1000); +VARP(invmouse, 0, 0, 1); +FVARP(mouseaccel, 0, 0, 1000); + +VAR(thirdperson, 0, 0, 2); +FVAR(thirdpersondistance, 0, 20, 50); +FVAR(thirdpersonup, -25, 0, 25); +FVAR(thirdpersonside, -25, 0, 25); +physent *camera1 = NULL; +bool detachedcamera = false; +bool isthirdperson() { return player!=camera1 || detachedcamera || reflecting; } + +void fixcamerarange() +{ + const float MAXPITCH = 90.0f; + if(camera1->pitch>MAXPITCH) camera1->pitch = MAXPITCH; + if(camera1->pitch<-MAXPITCH) camera1->pitch = -MAXPITCH; + while(camera1->yaw<0.0f) camera1->yaw += 360.0f; + while(camera1->yaw>=360.0f) camera1->yaw -= 360.0f; +} + +void mousemove(int dx, int dy) +{ + if(!game::allowmouselook()) return; + float cursens = sensitivity, curaccel = mouseaccel; + if(zoom) + { + if(zoomautosens) + { + cursens = float(sensitivity*zoomfov)/fov; + curaccel = float(mouseaccel*zoomfov)/fov; + } + else + { + cursens = zoomsens; + curaccel = zoomaccel; + } + } + if(curaccel && curtime && (dx || dy)) cursens += curaccel * sqrtf(dx*dx + dy*dy)/curtime; + cursens /= 33.0f*sensitivityscale; + camera1->yaw += dx*cursens; + camera1->pitch -= dy*cursens*(invmouse ? -1 : 1); + fixcamerarange(); + if(camera1!=player && !detachedcamera) + { + player->yaw = camera1->yaw; + player->pitch = camera1->pitch; + } +} + +void recomputecamera() +{ + game::setupcamera(); + computezoom(); + + bool allowthirdperson = game::allowthirdperson(); + bool shoulddetach = (allowthirdperson && thirdperson > 1) || game::detachcamera(); + if((!allowthirdperson || !thirdperson) && !shoulddetach) + { + camera1 = player; + detachedcamera = false; + } + else + { + static physent tempcamera; + camera1 = &tempcamera; + if(detachedcamera && shoulddetach) camera1->o = player->o; + else + { + *camera1 = *player; + detachedcamera = shoulddetach; + } + camera1->reset(); + camera1->type = ENT_CAMERA; + camera1->move = -1; + camera1->eyeheight = camera1->aboveeye = camera1->radius = camera1->xradius = camera1->yradius = 2; + + matrix3 orient; + orient.identity(); + orient.rotate_around_z(camera1->yaw*RAD); + orient.rotate_around_x(camera1->pitch*RAD); + orient.rotate_around_y(camera1->roll*-RAD); + vec dir = vec(orient.b).neg(), side = vec(orient.a).neg(), up = orient.c; + + if(game::collidecamera()) + { + movecamera(camera1, dir, thirdpersondistance, 1); + movecamera(camera1, dir, clamp(thirdpersondistance - camera1->o.dist(player->o), 0.0f, 1.0f), 0.1f); + if(thirdpersonup) + { + vec pos = camera1->o; + float dist = fabs(thirdpersonup); + if(thirdpersonup < 0) up.neg(); + movecamera(camera1, up, dist, 1); + movecamera(camera1, up, clamp(dist - camera1->o.dist(pos), 0.0f, 1.0f), 0.1f); + } + if(thirdpersonside) + { + vec pos = camera1->o; + float dist = fabs(thirdpersonside); + if(thirdpersonside < 0) side.neg(); + movecamera(camera1, side, dist, 1); + movecamera(camera1, side, clamp(dist - camera1->o.dist(pos), 0.0f, 1.0f), 0.1f); + } + } + else + { + camera1->o.add(vec(dir).mul(thirdpersondistance)); + if(thirdpersonup) camera1->o.add(vec(up).mul(thirdpersonup)); + if(thirdpersonside) camera1->o.add(vec(side).mul(thirdpersonside)); + } + } + + setviewcell(camera1->o); +} + +extern const matrix4 viewmatrix(vec(-1, 0, 0), vec(0, 0, 1), vec(0, -1, 0)); +matrix4 cammatrix, projmatrix, camprojmatrix, invcammatrix, invcamprojmatrix; + +FVAR(nearplane, 0.01f, 0.54f, 2.0f); + +vec calcavatarpos(const vec &pos, float dist) +{ + vec eyepos; + cammatrix.transform(pos, eyepos); + GLdouble ydist = nearplane * tan(curavatarfov/2*RAD), xdist = ydist * aspect; + vec4 scrpos; + scrpos.x = eyepos.x*nearplane/xdist; + scrpos.y = eyepos.y*nearplane/ydist; + scrpos.z = (eyepos.z*(farplane + nearplane) - 2*nearplane*farplane) / (farplane - nearplane); + scrpos.w = -eyepos.z; + + vec worldpos = invcamprojmatrix.perspectivetransform(scrpos); + vec dir = vec(worldpos).sub(camera1->o).rescale(dist); + return dir.add(camera1->o); +} + +VAR(reflectclip, 0, 6, 64); +VAR(reflectclipavatar, -64, 0, 64); + +matrix4 clipmatrix, noclipmatrix; + +void renderavatar() +{ + if(isthirdperson()) return; + + matrix4 oldprojmatrix = projmatrix; + projmatrix.perspective(curavatarfov, aspect, nearplane, farplane); + projmatrix.scalez(avatardepth); + setcamprojmatrix(false); + + game::renderavatar(); + + projmatrix = oldprojmatrix; + setcamprojmatrix(false); +} + +FVAR(polygonoffsetfactor, -1e4f, -3.0f, 1e4f); +FVAR(polygonoffsetunits, -1e4f, -3.0f, 1e4f); +FVAR(depthoffset, -1e4f, 0.01f, 1e4f); + +matrix4 nooffsetmatrix; + +void enablepolygonoffset(GLenum type) +{ + if(!depthoffset) + { + glPolygonOffset(polygonoffsetfactor, polygonoffsetunits); + glEnable(type); + return; + } + + bool clipped = reflectz < 1e15f && reflectclip; + + nooffsetmatrix = projmatrix; + projmatrix.d.z += depthoffset * (clipped ? noclipmatrix.c.z : projmatrix.c.z); + setcamprojmatrix(false, true); +} + +void disablepolygonoffset(GLenum type) +{ + if(!depthoffset) + { + glDisable(type); + return; + } + + projmatrix = nooffsetmatrix; + setcamprojmatrix(false, true); +} + +void calcspherescissor(const vec ¢er, float size, float &sx1, float &sy1, float &sx2, float &sy2) +{ + vec worldpos(center), e; + if(reflecting) worldpos.z = 2*reflectz - worldpos.z; + cammatrix.transform(worldpos, e); + if(e.z > 2*size) { sx1 = sy1 = 1; sx2 = sy2 = -1; return; } + float zzrr = e.z*e.z - size*size, + dx = e.x*e.x + zzrr, dy = e.y*e.y + zzrr, + focaldist = 1.0f/tan(fovy*0.5f*RAD); + sx1 = sy1 = -1; + sx2 = sy2 = 1; + #define CHECKPLANE(c, dir, focaldist, low, high) \ + do { \ + float nzc = (cz*cz + 1) / (cz dir drt) - cz, \ + pz = (d##c)/(nzc*e.c - e.z); \ + if(pz > 0) \ + { \ + float c = (focaldist)*nzc, \ + pc = pz*nzc; \ + if(pc < e.c) low = c; \ + else if(pc > e.c) high = c; \ + } \ + } while(0) + if(dx > 0) + { + float cz = e.x/e.z, drt = sqrtf(dx)/size; + CHECKPLANE(x, -, focaldist/aspect, sx1, sx2); + CHECKPLANE(x, +, focaldist/aspect, sx1, sx2); + } + if(dy > 0) + { + float cz = e.y/e.z, drt = sqrtf(dy)/size; + CHECKPLANE(y, -, focaldist, sy1, sy2); + CHECKPLANE(y, +, focaldist, sy1, sy2); + } +} + +static int scissoring = 0; +static GLint oldscissor[4]; + +int pushscissor(float sx1, float sy1, float sx2, float sy2) +{ + scissoring = 0; + + if(sx1 <= -1 && sy1 <= -1 && sx2 >= 1 && sy2 >= 1) return 0; + + sx1 = max(sx1, -1.0f); + sy1 = max(sy1, -1.0f); + sx2 = min(sx2, 1.0f); + sy2 = min(sy2, 1.0f); + + GLint viewport[4]; + glGetIntegerv(GL_VIEWPORT, viewport); + int sx = viewport[0] + int(floor((sx1+1)*0.5f*viewport[2])), + sy = viewport[1] + int(floor((sy1+1)*0.5f*viewport[3])), + sw = viewport[0] + int(ceil((sx2+1)*0.5f*viewport[2])) - sx, + sh = viewport[1] + int(ceil((sy2+1)*0.5f*viewport[3])) - sy; + if(sw <= 0 || sh <= 0) return 0; + + if(glIsEnabled(GL_SCISSOR_TEST)) + { + glGetIntegerv(GL_SCISSOR_BOX, oldscissor); + sw += sx; + sh += sy; + sx = max(sx, int(oldscissor[0])); + sy = max(sy, int(oldscissor[1])); + sw = min(sw, int(oldscissor[0] + oldscissor[2])) - sx; + sh = min(sh, int(oldscissor[1] + oldscissor[3])) - sy; + if(sw <= 0 || sh <= 0) return 0; + scissoring = 2; + } + else scissoring = 1; + + glScissor(sx, sy, sw, sh); + if(scissoring<=1) glEnable(GL_SCISSOR_TEST); + + return scissoring; +} + +void popscissor() +{ + if(scissoring>1) glScissor(oldscissor[0], oldscissor[1], oldscissor[2], oldscissor[3]); + else if(scissoring) glDisable(GL_SCISSOR_TEST); + scissoring = 0; +} + +static GLuint screenquadvbo = 0; + +static void setupscreenquad() +{ + if(!screenquadvbo) + { + glGenBuffers_(1, &screenquadvbo); + gle::bindvbo(screenquadvbo); + vec2 verts[4] = { vec2(1, -1), vec2(-1, -1), vec2(1, 1), vec2(-1, 1) }; + glBufferData_(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW); + gle::clearvbo(); + } +} + +static void cleanupscreenquad() +{ + if(screenquadvbo) { glDeleteBuffers_(1, &screenquadvbo); screenquadvbo = 0; } +} + +void screenquad() +{ + setupscreenquad(); + gle::bindvbo(screenquadvbo); + gle::enablevertex(); + gle::vertexpointer(sizeof(vec2), (const vec2 *)0, GL_FLOAT, 2); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + gle::disablevertex(); + gle::clearvbo(); +} + +static LocalShaderParam screentexcoord[2] = { LocalShaderParam("screentexcoord0"), LocalShaderParam("screentexcoord1") }; + +static inline void setscreentexcoord(int i, float w, float h, float x = 0, float y = 0) +{ + screentexcoord[i].setf(w*0.5f, h*0.5f, x + w*0.5f, y + fabs(h)*0.5f); +} + +void screenquad(float sw, float sh) +{ + setscreentexcoord(0, sw, sh); + screenquad(); +} + +void screenquadflipped(float sw, float sh) +{ + setscreentexcoord(0, sw, -sh); + screenquad(); +} + +void screenquad(float sw, float sh, float sw2, float sh2) +{ + setscreentexcoord(0, sw, sh); + setscreentexcoord(1, sw2, sh2); + screenquad(); +} + +void screenquadoffset(float x, float y, float w, float h) +{ + setscreentexcoord(0, w, h, x, y); + screenquad(); +} + +void screenquadoffset(float x, float y, float w, float h, float x2, float y2, float w2, float h2) +{ + setscreentexcoord(0, w, h, x, y); + setscreentexcoord(1, w2, h2, x2, y2); + screenquad(); +} + +#define HUDQUAD(x1, y1, x2, y2, sx1, sy1, sx2, sy2) { \ + gle::defvertex(2); \ + gle::deftexcoord0(); \ + gle::begin(GL_TRIANGLE_STRIP); \ + gle::attribf(x2, y1); gle::attribf(sx2, sy1); \ + gle::attribf(x1, y1); gle::attribf(sx1, sy1); \ + gle::attribf(x2, y2); gle::attribf(sx2, sy2); \ + gle::attribf(x1, y2); gle::attribf(sx1, sy2); \ + gle::end(); \ +} + +void hudquad(float x, float y, float w, float h, float tx, float ty, float tw, float th) +{ + HUDQUAD(x, y, x+w, y+h, tx, ty, tx+tw, ty+th); +} + +VARR(fog, 16, 4000, 1000024); +bvec fogcolor(0x80, 0x99, 0xB3); +HVARFR(fogcolour, 0, 0x8099B3, 0xFFFFFF, +{ + fogcolor = bvec((fogcolour>>16)&0xFF, (fogcolour>>8)&0xFF, fogcolour&0xFF); +}); + +static float findsurface(int fogmat, const vec &v, int &abovemat) +{ + fogmat &= MATF_VOLUME; + ivec o(v), co; + int csize; + do + { + cube &c = lookupcube(o, 0, co, csize); + int mat = c.material&MATF_VOLUME; + if(mat != fogmat) + { + abovemat = isliquid(mat) ? c.material : MAT_AIR; + return o.z; + } + o.z = co.z + csize; + } + while(o.z < worldsize); + abovemat = MAT_AIR; + return worldsize; +} + +static void blendfog(int fogmat, float blend, float logblend, float &start, float &end, vec &fogc) +{ + switch(fogmat&MATF_VOLUME) + { + case MAT_WATER: + { + const bvec &wcol = getwatercolor(fogmat); + int wfog = getwaterfog(fogmat); + fogc.madd(wcol.tocolor(), blend); + end += logblend*min(fog, max(wfog*4, 32)); + break; + } + + case MAT_LAVA: + { + const bvec &lcol = getlavacolor(fogmat); + int lfog = getlavafog(fogmat); + fogc.madd(lcol.tocolor(), blend); + end += logblend*min(fog, max(lfog*4, 32)); + break; + } + + default: + fogc.madd(fogcolor.tocolor(), blend); + start += logblend*(fog+64)/8; + end += logblend*fog; + break; + } +} + +vec oldfogcolor(0, 0, 0), curfogcolor(0, 0, 0); +float oldfogstart = 0, oldfogend = 1000000, curfogstart = 0, curfogend = 1000000; + +void setfogcolor(const vec &v) +{ + GLOBALPARAM(fogcolor, v); +} + +void zerofogcolor() +{ + setfogcolor(vec(0, 0, 0)); +} + +void resetfogcolor() +{ + setfogcolor(curfogcolor); +} + +void pushfogcolor(const vec &v) +{ + oldfogcolor = curfogcolor; + curfogcolor = v; + resetfogcolor(); +} + +void popfogcolor() +{ + curfogcolor = oldfogcolor; + resetfogcolor(); +} + +void setfogdist(float start, float end) +{ + GLOBALPARAMF(fogparams, 1/(end - start), end/(end - start)); +} + +void clearfogdist() +{ + setfogdist(0, 1000000); +} + +void resetfogdist() +{ + setfogdist(curfogstart, curfogend); +} + +void pushfogdist(float start, float end) +{ + oldfogstart = curfogstart; + oldfogend = curfogend; + curfogstart = start; + curfogend = end; + resetfogdist(); +} + +void popfogdist() +{ + curfogstart = oldfogstart; + curfogend = oldfogend; + resetfogdist(); +} + +static void resetfog() +{ + resetfogcolor(); + resetfogdist(); + + glClearColor(curfogcolor.r, curfogcolor.g, curfogcolor.b, 1.0f); +} + +static void setfog(int fogmat, float below = 1, int abovemat = MAT_AIR) +{ + float logscale = 256, logblend = log(1 + (logscale - 1)*below) / log(logscale); + + curfogstart = curfogend = 0; + curfogcolor = vec(0, 0, 0); + blendfog(fogmat, below, logblend, curfogstart, curfogend, curfogcolor); + if(below < 1) blendfog(abovemat, 1-below, 1-logblend, curfogstart, curfogend, curfogcolor); + + resetfog(); +} + +static void setnofog(const vec &color = vec(0, 0, 0)) +{ + curfogstart = 0; + curfogend = 1000000; + curfogcolor = color; + + resetfog(); +} + +static void blendfogoverlay(int fogmat, float blend, vec &overlay) +{ + float maxc; + switch(fogmat&MATF_VOLUME) + { + case MAT_WATER: + { + const bvec &wcol = getwatercolor(fogmat); + maxc = max(wcol.r, max(wcol.g, wcol.b)); + overlay.madd(vec(wcol.r, wcol.g, wcol.b).div(min(32.0f + maxc*7.0f/8.0f, 255.0f)).max(0.4f), blend); + break; + } + + case MAT_LAVA: + { + const bvec &lcol = getlavacolor(fogmat); + maxc = max(lcol.r, max(lcol.g, lcol.b)); + overlay.madd(vec(lcol.r, lcol.g, lcol.b).div(min(32.0f + maxc*7.0f/8.0f, 255.0f)).max(0.4f), blend); + break; + } + + default: + overlay.add(blend); + break; + } +} + +void drawfogoverlay(int fogmat, float fogblend, int abovemat) +{ + SETSHADER(fogoverlay); + + glEnable(GL_BLEND); + glBlendFunc(GL_ZERO, GL_SRC_COLOR); + vec overlay(0, 0, 0); + blendfogoverlay(fogmat, fogblend, overlay); + blendfogoverlay(abovemat, 1-fogblend, overlay); + + gle::color(overlay); + screenquad(); + + glDisable(GL_BLEND); +} + +bool renderedgame = false; + +void rendergame(bool mainpass) +{ + game::rendergame(mainpass); + if(!shadowmapping) renderedgame = true; +} + +VARP(skyboxglare, 0, 1, 1); + +void drawglare() +{ + glaring = true; + refracting = -1; + + pushfogcolor(vec(0, 0, 0)); + + glClearColor(0, 0, 0, 1); + glClear((skyboxglare && !shouldclearskyboxglare() ? 0 : GL_COLOR_BUFFER_BIT) | GL_DEPTH_BUFFER_BIT); + + rendergeom(); + + if(skyboxglare) drawskybox(farplane, false); + + renderreflectedmapmodels(); + rendergame(); + renderavatar(); + + renderwater(); + rendermaterials(); + renderalphageom(); + renderparticles(); + + popfogcolor(); + + refracting = 0; + glaring = false; +} + +VARP(reflectmms, 0, 1, 1); +VARR(refractsky, 0, 0, 1); + +matrix4 noreflectmatrix; + +void drawreflection(float z, bool refract, int fogdepth, const bvec &col) +{ + reflectz = z < 0 ? 1e16f : z; + reflecting = !refract; + refracting = refract ? (z < 0 || camera1->o.z >= z ? -1 : 1) : 0; + fading = waterrefract && waterfade && z>=0; + fogging = refracting<0 && z>=0; + refractfog = fogdepth; + + if(fogging) + { + pushfogdist(camera1->o.z - z, camera1->o.z - (z - max(refractfog, 1))); + pushfogcolor(col.tocolor()); + } + else + { + vec color(0, 0, 0); + float start = 0, end = 0; + blendfog(MAT_AIR, 1, 1, start, end, color); + pushfogdist(start, end); + pushfogcolor(color); + } + + if(fading) + { + float scale = fogging ? -0.25f : 0.25f, offset = 2*fabs(scale) - scale*z; + GLOBALPARAMF(waterfadeparams, scale, offset, -scale, offset + camera1->o.z*scale); + } + + if(reflecting) + { + noreflectmatrix = cammatrix; + cammatrix.reflectz(z); + + glFrontFace(GL_CCW); + } + + if(reflectclip && z>=0) + { + float zoffset = reflectclip/4.0f, zclip; + if(refracting<0) + { + zclip = z+zoffset; + if(camera1->o.z<=zclip) zclip = z; + } + else + { + zclip = z-zoffset; + if(camera1->o.z>=zclip && camera1->o.z<=z+4.0f) zclip = z; + if(reflecting) zclip = 2*z - zclip; + } + plane clipplane; + invcammatrix.transposedtransform(plane(0, 0, refracting>0 ? 1 : -1, refracting>0 ? -zclip : zclip), clipplane); + clipmatrix.clip(clipplane, projmatrix); + noclipmatrix = projmatrix; + projmatrix = clipmatrix; + } + + setcamprojmatrix(false, true); + + renderreflectedgeom(refracting<0 && z>=0 && caustics, fogging); + + if(reflecting || refracting>0 || (refracting<0 && refractsky) || z<0) + { + if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + if(reflectclip && z>=0) + { + projmatrix = noclipmatrix; + setcamprojmatrix(false, true); + } + drawskybox(farplane, false); + if(reflectclip && z>=0) + { + projmatrix = clipmatrix; + setcamprojmatrix(false, true); + } + if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE); + } + else if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE); + + renderdecals(); + + if(reflectmms) renderreflectedmapmodels(); + rendergame(); + + if(refracting && z>=0 && !isthirdperson() && fabs(camera1->o.z-z) <= 0.5f*(player->eyeheight + player->aboveeye)) + { + matrix4 oldprojmatrix = projmatrix, avatarproj; + avatarproj.perspective(curavatarfov, aspect, nearplane, farplane); + if(reflectclip) + { + matrix4 avatarclip; + plane clipplane; + invcammatrix.transposedtransform(plane(0, 0, refracting, reflectclipavatar/4.0f - refracting*z), clipplane); + avatarclip.clip(clipplane, avatarproj); + projmatrix = avatarclip; + } + else projmatrix = avatarproj; + setcamprojmatrix(false, true); + game::renderavatar(); + projmatrix = oldprojmatrix; + setcamprojmatrix(false, true); + } + + if(refracting) rendergrass(); + rendermaterials(); + renderalphageom(fogging); + renderparticles(); + + if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + + if(reflectclip && z>=0) projmatrix = noclipmatrix; + + if(reflecting) + { + cammatrix = noreflectmatrix; + + glFrontFace(GL_CW); + } + + popfogdist(); + popfogcolor(); + + reflectz = 1e16f; + refracting = 0; + reflecting = fading = fogging = false; + + setcamprojmatrix(false, true); +} + +int drawtex = 0; + +void drawcubemap(int size, const vec &o, float yaw, float pitch, const cubemapside &side, bool onlysky) +{ + drawtex = DRAWTEX_ENVMAP; + + physent *oldcamera = camera1; + static physent cmcamera; + cmcamera = *player; + cmcamera.reset(); + cmcamera.type = ENT_CAMERA; + cmcamera.o = o; + cmcamera.yaw = yaw; + cmcamera.pitch = pitch; + cmcamera.roll = 0; + camera1 = &cmcamera; + setviewcell(camera1->o); + + int fogmat = lookupmaterial(o)&(MATF_VOLUME|MATF_INDEX); + + setfog(fogmat); + + int farplane = worldsize*2; + + projmatrix.perspective(90.0f, 1.0f, nearplane, farplane); + if(!side.flipx || !side.flipy) projmatrix.scalexy(!side.flipx ? -1 : 1, !side.flipy ? -1 : 1); + if(side.swapxy) + { + swap(projmatrix.a.x, projmatrix.a.y); + swap(projmatrix.b.x, projmatrix.b.y); + swap(projmatrix.c.x, projmatrix.c.y); + swap(projmatrix.d.x, projmatrix.d.y); + } + setcamprojmatrix(); + + xtravertsva = xtraverts = glde = gbatches = 0; + + visiblecubes(); + + if(onlysky) drawskybox(farplane, false, true); + else + { + glClear(GL_DEPTH_BUFFER_BIT); + + glEnable(GL_CULL_FACE); + glEnable(GL_DEPTH_TEST); + + if(limitsky()) drawskybox(farplane, true); + + rendergeom(); + + if(!limitsky()) drawskybox(farplane, false); + +// queryreflections(); + + rendermapmodels(); + renderalphageom(); + +// drawreflections(); + +// renderwater(); +// rendermaterials(); + + glDisable(GL_DEPTH_TEST); + glDisable(GL_CULL_FACE); + } + + camera1 = oldcamera; + drawtex = 0; +} + +VAR(modelpreviewfov, 10, 20, 100); +VAR(modelpreviewpitch, -90, -15, 90); + +namespace modelpreview +{ + physent *oldcamera; + physent camera; + + float oldaspect, oldfovy, oldfov; + int oldfarplane; + matrix4 oldprojmatrix; + + void start(int x, int y, int w, int h, bool background) + { + drawtex = DRAWTEX_MODELPREVIEW; + + glViewport(x, y, w, h); + glScissor(x, y, w, h); + glEnable(GL_SCISSOR_TEST); + + oldcamera = camera1; + camera = *camera1; + camera.reset(); + camera.type = ENT_CAMERA; + camera.o = vec(0, 0, 0); + camera.yaw = 0; + camera.pitch = modelpreviewpitch; + camera.roll = 0; + camera1 = &camera; + + oldaspect = aspect; + oldfovy = fovy; + oldfov = curfov; + oldfarplane = farplane; + oldprojmatrix = projmatrix; + + aspect = w/float(h); + fovy = modelpreviewfov; + curfov = 2*atan2(tan(fovy/2*RAD), 1/aspect)/RAD; + farplane = 1024; + + clearfogdist(); + zerofogcolor(); + glClearColor(0, 0, 0, 1); + + glClear((background ? GL_COLOR_BUFFER_BIT : 0) | GL_DEPTH_BUFFER_BIT); + + projmatrix.perspective(fovy, aspect, nearplane, farplane); + setcamprojmatrix(); + + glEnable(GL_CULL_FACE); + glEnable(GL_DEPTH_TEST); + } + + void end() + { + glDisable(GL_CULL_FACE); + glDisable(GL_DEPTH_TEST); + + resetfogdist(); + resetfogcolor(); + glClearColor(curfogcolor.r, curfogcolor.g, curfogcolor.b, 1); + + aspect = oldaspect; + fovy = oldfovy; + curfov = oldfov; + farplane = oldfarplane; + + camera1 = oldcamera; + drawtex = 0; + + glDisable(GL_SCISSOR_TEST); + glViewport(0, 0, screenw, screenh); + + projmatrix = oldprojmatrix; + setcamprojmatrix(); + } +} + +vec calcmodelpreviewpos(const vec &radius, float &yaw) +{ + yaw = fmod(lastmillis/10000.0f*360.0f, 360.0f); + float dist = 1.15f*max(radius.magnitude2()/aspect, radius.magnitude())/sinf(fovy/2*RAD); + return vec(0, dist, 0).rotate_around_x(camera1->pitch*RAD); +} + +GLuint minimaptex = 0; +vec minimapcenter(0, 0, 0), minimapradius(0, 0, 0), minimapscale(0, 0, 0); + +void clearminimap() +{ + if(minimaptex) { glDeleteTextures(1, &minimaptex); minimaptex = 0; } +} + +VARR(minimapheight, 0, 0, 2<<16); +bvec minimapcolor(0, 0, 0); +HVARFR(minimapcolour, 0, 0, 0xFFFFFF, +{ + minimapcolor = bvec((minimapcolour>>16)&0xFF, (minimapcolour>>8)&0xFF, minimapcolour&0xFF); +}); +VARR(minimapclip, 0, 0, 1); +VARFP(minimapsize, 7, 8, 10, { if(minimaptex) drawminimap(); }); + +void bindminimap() +{ + glBindTexture(GL_TEXTURE_2D, minimaptex); +} + +void clipminimap(ivec &bbmin, ivec &bbmax, cube *c = worldroot, const ivec &co = ivec(0, 0, 0), int size = worldsize>>1) +{ + loopi(8) + { + ivec o(i, co, size); + if(c[i].children) clipminimap(bbmin, bbmax, c[i].children, o, size>>1); + else if(!isentirelysolid(c[i]) && (c[i].material&MATF_CLIP)!=MAT_CLIP) + { + loopk(3) bbmin[k] = min(bbmin[k], o[k]); + loopk(3) bbmax[k] = max(bbmax[k], o[k] + size); + } + } +} + +void drawminimap() +{ + if(!game::needminimap()) { clearminimap(); return; } + + renderprogress(0, "generating mini-map...", 0, !renderedframe); + + int size = 1< sizelimit) size /= 2; + if(!minimaptex) glGenTextures(1, &minimaptex); + + ivec bbmin(worldsize, worldsize, worldsize), bbmax(0, 0, 0); + loopv(valist) + { + vtxarray *va = valist[i]; + loopk(3) + { + if(va->geommin[k]>va->geommax[k]) continue; + bbmin[k] = min(bbmin[k], va->geommin[k]); + bbmax[k] = max(bbmax[k], va->geommax[k]); + } + } + if(minimapclip) + { + ivec clipmin(worldsize, worldsize, worldsize), clipmax(0, 0, 0); + clipminimap(clipmin, clipmax); + loopk(2) bbmin[k] = max(bbmin[k], clipmin[k]); + loopk(2) bbmax[k] = min(bbmax[k], clipmax[k]); + } + + minimapradius = vec(bbmax).sub(vec(bbmin)).mul(0.5f); + minimapcenter = vec(bbmin).add(minimapradius); + minimapradius.x = minimapradius.y = max(minimapradius.x, minimapradius.y); + minimapscale = vec((0.5f - 1.0f/size)/minimapradius.x, (0.5f - 1.0f/size)/minimapradius.y, 1.0f); + + drawtex = DRAWTEX_MINIMAP; + + physent *oldcamera = camera1; + static physent cmcamera; + cmcamera = *player; + cmcamera.reset(); + cmcamera.type = ENT_CAMERA; + cmcamera.o = vec(minimapcenter.x, minimapcenter.y, max(minimapcenter.z + minimapradius.z + 1, float(minimapheight))); + cmcamera.yaw = 0; + cmcamera.pitch = -90; + cmcamera.roll = 0; + camera1 = &cmcamera; + setviewcell(vec(-1, -1, -1)); + + projmatrix.ortho(-minimapradius.x, minimapradius.x, -minimapradius.y, minimapradius.y, 0, camera1->o.z + 1); + projmatrix.a.mul(-1); + setcamprojmatrix(); + + setnofog(minimapcolor.tocolor()); + + glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); + + glViewport(0, 0, size, size); + + glEnable(GL_CULL_FACE); + glEnable(GL_DEPTH_TEST); + + glFrontFace(GL_CCW); + + xtravertsva = xtraverts = glde = gbatches = 0; + + visiblecubes(false); + queryreflections(); + drawreflections(); + + loopi(minimapheight > 0 && minimapheight < minimapcenter.z + minimapradius.z ? 2 : 1) + { + if(i) + { + glClear(GL_DEPTH_BUFFER_BIT); + camera1->o.z = minimapheight; + setcamprojmatrix(); + } + rendergeom(); + rendermapmodels(); + renderwater(); + rendermaterials(); + renderalphageom(); + } + + glFrontFace(GL_CW); + + glDisable(GL_DEPTH_TEST); + glDisable(GL_CULL_FACE); + + glViewport(0, 0, screenw, screenh); + + camera1 = oldcamera; + drawtex = 0; + + glBindTexture(GL_TEXTURE_2D, minimaptex); + glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGB5, 0, 0, size, size, 0); + setuptexparameters(minimaptex, NULL, 3, 1, GL_RGB5, GL_TEXTURE_2D); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); + GLfloat border[4] = { minimapcolor.x/255.0f, minimapcolor.y/255.0f, minimapcolor.z/255.0f, 1.0f }; + glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border); + glBindTexture(GL_TEXTURE_2D, 0); +} + +bool deferdrawtextures = false; + +void drawtextures() +{ + if(minimized) { deferdrawtextures = true; return; } + deferdrawtextures = false; + genenvmaps(); + drawminimap(); +} + +GLuint motiontex = 0; +int motionw = 0, motionh = 0, lastmotion = 0; + +void cleanupmotionblur() +{ + if(motiontex) { glDeleteTextures(1, &motiontex); motiontex = 0; } + motionw = motionh = 0; + lastmotion = 0; +} + +VARFP(motionblur, 0, 0, 1, { if(!motionblur) cleanupmotionblur(); }); +VARP(motionblurmillis, 1, 5, 1000); +FVARP(motionblurscale, 0, 0.5f, 1); + +void addmotionblur() +{ + if(!motionblur || max(screenw, screenh) > hwtexsize) return; + + if(game::ispaused()) { lastmotion = 0; return; } + + if(!motiontex || motionw != screenw || motionh != screenh) + { + if(!motiontex) glGenTextures(1, &motiontex); + motionw = screenw; + motionh = screenh; + lastmotion = 0; + createtexture(motiontex, motionw, motionh, NULL, 3, 0, GL_RGB); + } + + glBindTexture(GL_TEXTURE_2D, motiontex); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + SETSHADER(screenrect); + + gle::colorf(1, 1, 1, lastmotion ? pow(motionblurscale, max(float(lastmillis - lastmotion)/motionblurmillis, 1.0f)) : 0); + screenquad(1, 1); + + glDisable(GL_BLEND); + + if(lastmillis - lastmotion >= motionblurmillis) + { + lastmotion = lastmillis - lastmillis%motionblurmillis; + + glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, screenw, screenh); + } +} + +bool dopostfx = false; + +void invalidatepostfx() +{ + dopostfx = false; +} + +int xtraverts, xtravertsva; + +void gl_drawframe() +{ + if(deferdrawtextures) drawtextures(); + + updatedynlights(); + + int w = screenw, h = screenh; + aspect = forceaspect ? forceaspect : w/float(h); + fovy = 2*atan2(tan(curfov/2*RAD), aspect)/RAD; + + int fogmat = lookupmaterial(camera1->o)&(MATF_VOLUME|MATF_INDEX), abovemat = MAT_AIR; + float fogblend = 1.0f, causticspass = 0.0f; + if(isliquid(fogmat&MATF_VOLUME)) + { + float z = findsurface(fogmat, camera1->o, abovemat) - WATER_OFFSET; + if(camera1->o.z < z + 1) fogblend = min(z + 1 - camera1->o.z, 1.0f); + else fogmat = abovemat; + if(caustics && (fogmat&MATF_VOLUME)==MAT_WATER && camera1->o.z < z) + causticspass = min(z - camera1->o.z, 1.0f); + } + else fogmat = MAT_AIR; + setfog(fogmat, fogblend, abovemat); + if(fogmat!=MAT_AIR) + { + float blend = abovemat==MAT_AIR ? fogblend : 1.0f; + fovy += blend*sinf(lastmillis/1000.0)*2.0f; + aspect += blend*sinf(lastmillis/1000.0+M_PI)*0.1f; + } + + farplane = worldsize*2; + + projmatrix.perspective(fovy, aspect, nearplane, farplane); + setcamprojmatrix(); + + glEnable(GL_CULL_FACE); + glEnable(GL_DEPTH_TEST); + + xtravertsva = xtraverts = glde = gbatches = 0; + + visiblecubes(); + + glClear(GL_DEPTH_BUFFER_BIT|(wireframe && editmode ? GL_COLOR_BUFFER_BIT : 0)); + + if(wireframe && editmode) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + + if(limitsky()) drawskybox(farplane, true); + + rendergeom(causticspass); + + extern int outline; + if(!wireframe && editmode && outline) renderoutline(); + + queryreflections(); + + generategrass(); + + if(!limitsky()) drawskybox(farplane, false); + + renderdecals(true); + + rendermapmodels(); + rendergame(true); + renderavatar(); + + if(wireframe && editmode) glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + + drawglaretex(); + drawdepthfxtex(); + drawreflections(); + + if(wireframe && editmode) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + + renderwater(); + rendergrass(); + + rendermaterials(); + renderalphageom(); + + if(wireframe && editmode) glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + + renderparticles(true); + + glDisable(GL_CULL_FACE); + glDisable(GL_DEPTH_TEST); + + addmotionblur(); + addglare(); + if(isliquid(fogmat&MATF_VOLUME)) drawfogoverlay(fogmat, fogblend, abovemat); + renderpostfx(); + + gl_drawhud(); + + renderedgame = false; +} + +void gl_drawmainmenu() +{ + xtravertsva = xtraverts = glde = gbatches = 0; + + renderbackground(NULL, NULL, NULL, NULL, true, true); + renderpostfx(); + + gl_drawhud(); +} + +VARNP(damagecompass, usedamagecompass, 0, 1, 1); +VARP(damagecompassfade, 1, 1000, 10000); +VARP(damagecompasssize, 1, 30, 100); +VARP(damagecompassalpha, 1, 25, 100); +VARP(damagecompassmin, 1, 25, 1000); +VARP(damagecompassmax, 1, 200, 1000); + +float damagedirs[8] = { 0, 0, 0, 0, 0, 0, 0, 0 }; + +void damagecompass(int n, const vec &loc) +{ + if(!usedamagecompass || minimized) return; + vec delta(loc); + delta.sub(camera1->o); + float yaw = 0, pitch; + if(delta.magnitude() > 4) + { + vectoyawpitch(delta, yaw, pitch); + yaw -= camera1->yaw; + } + if(yaw >= 360) yaw = fmod(yaw, 360); + else if(yaw < 0) yaw = 360 - fmod(-yaw, 360); + int dir = (int(yaw+22.5f)%360)/45; + damagedirs[dir] += max(n, damagecompassmin)/float(damagecompassmax); + if(damagedirs[dir]>1) damagedirs[dir] = 1; +} + +void drawdamagecompass(int w, int h) +{ + hudnotextureshader->set(); + + int dirs = 0; + float size = damagecompasssize/100.0f*min(h, w)/2.0f; + loopi(8) if(damagedirs[i]>0) + { + if(!dirs) + { + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + gle::colorf(1, 0, 0, damagecompassalpha/100.0f); + gle::defvertex(); + gle::begin(GL_TRIANGLES); + } + dirs++; + + float logscale = 32, + scale = log(1 + (logscale - 1)*damagedirs[i]) / log(logscale), + offset = -size/2.0f-min(h, w)/4.0f; + matrix4x3 m; + m.identity(); + m.settranslation(w/2, h/2, 0); + m.rotate_around_z(i*45*RAD); + m.translate(0, offset, 0); + m.scale(size*scale); + + gle::attrib(m.transform(vec2(1, 1))); + gle::attrib(m.transform(vec2(-1, 1))); + gle::attrib(m.transform(vec2(0, 0))); + + // fade in log space so short blips don't disappear too quickly + scale -= float(curtime)/damagecompassfade; + damagedirs[i] = scale > 0 ? (pow(logscale, scale) - 1) / (logscale - 1) : 0; + } + if(dirs) gle::end(); +} + +int damageblendmillis = 0; + +VARFP(damagescreen, 0, 1, 1, { if(!damagescreen) damageblendmillis = 0; }); +VARP(damagescreenfactor, 1, 7, 100); +VARP(damagescreenalpha, 1, 45, 100); +VARP(damagescreenfade, 0, 125, 1000); +VARP(damagescreenmin, 1, 10, 1000); +VARP(damagescreenmax, 1, 100, 1000); + +void damageblend(int n) +{ + if(!damagescreen || minimized) return; + if(lastmillis > damageblendmillis) damageblendmillis = lastmillis; + damageblendmillis += clamp(n, damagescreenmin, damagescreenmax)*damagescreenfactor; +} + +void drawdamagescreen(int w, int h) +{ + if(lastmillis >= damageblendmillis) return; + + hudshader->set(); + + static Texture *damagetex = NULL; + if(!damagetex) damagetex = textureload("packages/hud/damage.png", 3); + + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + glBindTexture(GL_TEXTURE_2D, damagetex->id); + float fade = damagescreenalpha/100.0f; + if(damageblendmillis - lastmillis < damagescreenfade) + fade *= float(damageblendmillis - lastmillis)/damagescreenfade; + gle::colorf(fade, fade, fade, fade); + + hudquad(0, 0, w, h); +} + +void cleardamagescreen() +{ + damageblendmillis = 0; + loopi(8) damagedirs[i] = 0; +} + +VAR(hidestats, 0, 0, 1); +VAR(hidehud, 0, 0, 1); + +VARP(crosshairsize, 0, 15, 50); +VARP(cursorsize, 0, 30, 50); +VARP(crosshairfx, 0, 1, 1); +VARP(crosshaircolors, 0, 1, 1); + +#define MAXCROSSHAIRS 4 +static Texture *crosshairs[MAXCROSSHAIRS] = { NULL, NULL, NULL, NULL }; + +void loadcrosshair(const char *name, int i) +{ + if(i < 0 || i >= MAXCROSSHAIRS) return; + crosshairs[i] = name ? textureload(name, 3, true) : notexture; + if(crosshairs[i] == notexture) + { + name = game::defaultcrosshair(i); + if(!name) name = "data/crosshair.png"; + crosshairs[i] = textureload(name, 3, true); + } +} + +void loadcrosshair_(const char *name, int *i) +{ + loadcrosshair(name, *i); +} + +COMMANDN(loadcrosshair, loadcrosshair_, "si"); + +ICOMMAND(getcrosshair, "i", (int *i), +{ + const char *name = ""; + if(*i >= 0 && *i < MAXCROSSHAIRS) + { + name = crosshairs[*i] ? crosshairs[*i]->name : game::defaultcrosshair(*i); + if(!name) name = "data/crosshair.png"; + } + result(name); +}); + +void writecrosshairs(stream *f) +{ + loopi(MAXCROSSHAIRS) if(crosshairs[i] && crosshairs[i]!=notexture) + f->printf("loadcrosshair %s %d\n", escapestring(crosshairs[i]->name), i); + f->printf("\n"); +} + +void drawcrosshair(int w, int h) +{ + bool windowhit = g3d_windowhit(true, false); + if(!windowhit && (hidehud || mainmenu)) return; //(hidehud || player->state==CS_SPECTATOR || player->state==CS_DEAD)) return; + + vec color(1, 1, 1); + float cx = 0.5f, cy = 0.5f, chsize; + Texture *crosshair; + if(windowhit) + { + static Texture *cursor = NULL; + if(!cursor) cursor = textureload("data/guicursor.png", 3, true); + crosshair = cursor; + chsize = cursorsize*w/900.0f; + g3d_cursorpos(cx, cy); + } + else + { + int index = game::selectcrosshair(color); + if(index < 0) return; + if(!crosshairfx) index = 0; + if(!crosshairfx || !crosshaircolors) color = vec(1, 1, 1); + crosshair = crosshairs[index]; + if(!crosshair) + { + loadcrosshair(NULL, index); + crosshair = crosshairs[index]; + } + chsize = crosshairsize*w/900.0f; + } + if(crosshair->type&Texture::ALPHA) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + else glBlendFunc(GL_ONE, GL_ONE); + float x = cx*w - (windowhit ? 0 : chsize/2.0f); + float y = cy*h - (windowhit ? 0 : chsize/2.0f); + glBindTexture(GL_TEXTURE_2D, crosshair->id); + + hudshader->set(); + gle::color(color); + hudquad(x, y, chsize, chsize); +} + +VARP(wallclock, 0, 0, 1); +VARP(wallclock24, 0, 0, 1); +VARP(wallclocksecs, 0, 0, 1); + +static time_t walltime = 0; + +VARP(showfps, 0, 1, 1); +VARP(showfpsrange, 0, 0, 1); +VAR(showeditstats, 0, 0, 1); +VAR(statrate, 1, 200, 1000); + +FVARP(conscale, 1e-3f, 0.33f, 1e3f); + +void gl_drawhud() +{ + g3d_render(); + + int w = screenw, h = screenh; + if(forceaspect) w = int(ceil(h*forceaspect)); + + if(editmode && !hidehud && !mainmenu) + { + glEnable(GL_DEPTH_TEST); + glDepthMask(GL_FALSE); + + renderblendbrush(); + + rendereditcursor(); + + glDepthMask(GL_TRUE); + glDisable(GL_DEPTH_TEST); + } + + gettextres(w, h); + + hudmatrix.ortho(0, w, h, 0, -1, 1); + resethudmatrix(); + + gle::colorf(1, 1, 1); + + extern int debugsm; + if(debugsm) + { + extern void viewshadowmap(); + viewshadowmap(); + } + + extern int debugglare; + if(debugglare) + { + extern void viewglaretex(); + viewglaretex(); + } + + extern int debugdepthfx; + if(debugdepthfx) + { + extern void viewdepthfxtex(); + viewdepthfxtex(); + } + + glEnable(GL_BLEND); + + extern void debugparticles(); + debugparticles(); + + if(!mainmenu) + { + drawdamagescreen(w, h); + drawdamagecompass(w, h); + } + + hudshader->set(); + + int conw = int(w/conscale), conh = int(h/conscale), abovehud = conh - FONTH, limitgui = abovehud; + if(!hidehud && !mainmenu) + { + if(!hidestats) + { + pushhudmatrix(); + hudmatrix.scale(conscale, conscale, 1); + flushhudmatrix(); + + int roffset = 0; + if(showfps) + { + static int lastfps = 0, prevfps[3] = { 0, 0, 0 }, curfps[3] = { 0, 0, 0 }; + if(totalmillis - lastfps >= statrate) + { + memcpy(prevfps, curfps, sizeof(prevfps)); + lastfps = totalmillis - (totalmillis%statrate); + } + int nextfps[3]; + getfps(nextfps[0], nextfps[1], nextfps[2]); + loopi(3) if(prevfps[i]==curfps[i]) curfps[i] = nextfps[i]; + if(showfpsrange) draw_textf("fps %d+%d-%d", conw-7*FONTH, conh-FONTH*3/2, curfps[0], curfps[1], curfps[2]); + else draw_textf("fps %d", conw-5*FONTH, conh-FONTH*3/2, curfps[0]); + roffset += FONTH; + } + + if(wallclock) + { + if(!walltime) { walltime = time(NULL); walltime -= totalmillis/1000; if(!walltime) walltime++; } + time_t walloffset = walltime + totalmillis/1000; + struct tm *localvals = localtime(&walloffset); + static string buf; + if(localvals && strftime(buf, sizeof(buf), wallclocksecs ? (wallclock24 ? "%H:%M:%S" : "%I:%M:%S%p") : (wallclock24 ? "%H:%M" : "%I:%M%p"), localvals)) + { + // hack because not all platforms (windows) support %P lowercase option + // also strip leading 0 from 12 hour time + char *dst = buf; + const char *src = &buf[!wallclock24 && buf[0]=='0' ? 1 : 0]; + while(*src) *dst++ = tolower(*src++); + *dst++ = '\0'; + draw_text(buf, conw-5*FONTH, conh-FONTH*3/2-roffset); + roffset += FONTH; + } + } + + if(editmode || showeditstats) + { + static int laststats = 0, prevstats[8] = { 0, 0, 0, 0, 0, 0, 0 }, curstats[8] = { 0, 0, 0, 0, 0, 0, 0 }; + if(totalmillis - laststats >= statrate) + { + memcpy(prevstats, curstats, sizeof(prevstats)); + laststats = totalmillis - (totalmillis%statrate); + } + int nextstats[8] = + { + vtris*100/max(wtris, 1), + vverts*100/max(wverts, 1), + xtraverts/1024, + xtravertsva/1024, + glde, + gbatches, + getnumqueries(), + rplanes + }; + loopi(8) if(prevstats[i]==curstats[i]) curstats[i] = nextstats[i]; + + abovehud -= 2*FONTH; + draw_textf("wtr:%dk(%d%%) wvt:%dk(%d%%) evt:%dk eva:%dk", FONTH/2, abovehud, wtris/1024, curstats[0], wverts/1024, curstats[1], curstats[2], curstats[3]); + draw_textf("ond:%d va:%d gl:%d(%d) oq:%d lm:%d rp:%d pvs:%d", FONTH/2, abovehud+FONTH, allocnodes*8, allocva, curstats[4], curstats[5], curstats[6], lightmaps.length(), curstats[7], getnumviewcells()); + limitgui = abovehud; + } + + if(editmode) + { + abovehud -= FONTH; + draw_textf("cube %s%d%s", FONTH/2, abovehud, selchildcount<0 ? "1/" : "", abs(selchildcount), showmat && selchildmat > 0 ? getmaterialdesc(selchildmat, ": ") : ""); + + if(char *editinfo = execidentstr("edithud")) + { + if(editinfo[0]) + { + int tw, th; + text_bounds(editinfo, tw, th); + th += FONTH-1; th -= th%FONTH; + abovehud -= max(th, FONTH); + draw_text(editinfo, FONTH/2, abovehud); + } + DELETEA(editinfo); + } + } + else if(char *gameinfo = execidentstr("gamehud")) + { + if(gameinfo[0]) + { + int tw, th; + text_bounds(gameinfo, tw, th); + th += FONTH-1; th -= th%FONTH; + roffset += max(th, FONTH); + draw_text(gameinfo, conw-max(5*FONTH, 2*FONTH+tw), conh-FONTH/2-roffset); + } + DELETEA(gameinfo); + } + + pophudmatrix(); + } + + if(hidestats || (!editmode && !showeditstats)) + { + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + game::gameplayhud(w, h); + limitgui = abovehud = min(abovehud, int(conh*game::abovegameplayhud(w, h))); + } + + rendertexturepanel(w, h); + } + + glDisable(GL_BLEND); + + g3d_limitscale((2*limitgui - conh) / float(conh)); + g3d_render2d(); + + glEnable(GL_BLEND); + + hudmatrix.ortho(0, w, h, 0, -1, 1); + resethudmatrix(); + + pushhudmatrix(); + hudmatrix.scale(conscale, conscale, 1); + flushhudmatrix(); + abovehud -= rendercommand(FONTH/2, abovehud - FONTH/2, conw-FONTH); + extern int fullconsole; + if(!hidehud || fullconsole) renderconsole(conw, conh, abovehud - FONTH/2); + pophudmatrix(); + + drawcrosshair(w, h); + + glDisable(GL_BLEND); +} + +void cleanupgl() +{ + cleanupmotionblur(); + + clearminimap(); + + cleanupscreenquad(); + + gle::cleanup(); +} + + diff --git a/src/engine/rendermodel.cpp b/src/engine/rendermodel.cpp new file mode 100644 index 0000000..dc05e69 --- /dev/null +++ b/src/engine/rendermodel.cpp @@ -0,0 +1,1142 @@ +#include "engine.h" + +VAR(oqdynent, 0, 1, 1); +VAR(animationinterpolationtime, 0, 150, 1000); + +model *loadingmodel = NULL; + +#include "ragdoll.h" +#include "animmodel.h" +#include "vertmodel.h" +#include "skelmodel.h" + +static model *(__cdecl *modeltypes[NUMMODELTYPES])(const char *); + +static int addmodeltype(int type, model *(__cdecl *loader)(const char *)) +{ + modeltypes[type] = loader; + return type; +} + +#define MODELTYPE(modeltype, modelclass) \ +static model *__loadmodel__##modelclass(const char *filename) \ +{ \ + return new modelclass(filename); \ +} \ +UNUSED static int __dummy__##modelclass = addmodeltype((modeltype), __loadmodel__##modelclass); + +#include "md2.h" +#include "md3.h" +#include "md5.h" +#include "obj.h" +#include "smd.h" +#include "iqm.h" + +MODELTYPE(MDL_MD2, md2); +MODELTYPE(MDL_MD3, md3); +MODELTYPE(MDL_MD5, md5); +MODELTYPE(MDL_OBJ, obj); +MODELTYPE(MDL_SMD, smd); +MODELTYPE(MDL_IQM, iqm); + +#define checkmdl if(!loadingmodel) { conoutf(CON_ERROR, "not loading a model"); return; } + +void mdlcullface(int *cullface) +{ + checkmdl; + loadingmodel->setcullface(*cullface!=0); +} + +COMMAND(mdlcullface, "i"); + +void mdlcollide(int *collide) +{ + checkmdl; + loadingmodel->collide = *collide!=0; +} + +COMMAND(mdlcollide, "i"); + +void mdlellipsecollide(int *collide) +{ + checkmdl; + loadingmodel->ellipsecollide = *collide!=0; +} + +COMMAND(mdlellipsecollide, "i"); + +void mdlspec(int *percent) +{ + checkmdl; + float spec = 1.0f; + if(*percent>0) spec = *percent/100.0f; + else if(*percent<0) spec = 0.0f; + loadingmodel->setspec(spec); +} + +COMMAND(mdlspec, "i"); + +void mdlambient(int *percent) +{ + checkmdl; + float ambient = 0.3f; + if(*percent>0) ambient = *percent/100.0f; + else if(*percent<0) ambient = 0.0f; + loadingmodel->setambient(ambient); +} + +COMMAND(mdlambient, "i"); + +void mdlalphatest(float *cutoff) +{ + checkmdl; + loadingmodel->setalphatest(max(0.0f, min(1.0f, *cutoff))); +} + +COMMAND(mdlalphatest, "f"); + +void mdlalphablend(int *blend) +{ + checkmdl; + loadingmodel->setalphablend(*blend!=0); +} + +COMMAND(mdlalphablend, "i"); + +void mdlalphadepth(int *depth) +{ + checkmdl; + loadingmodel->alphadepth = *depth!=0; +} + +COMMAND(mdlalphadepth, "i"); + +void mdldepthoffset(int *offset) +{ + checkmdl; + loadingmodel->depthoffset = *offset!=0; +} + +COMMAND(mdldepthoffset, "i"); + +void mdlglow(int *percent, int *delta, float *pulse) +{ + checkmdl; + float glow = 3.0f, glowdelta = *delta/100.0f, glowpulse = *pulse > 0 ? *pulse/1000.0f : 0; + if(*percent>0) glow = *percent/100.0f; + else if(*percent<0) glow = 0.0f; + glowdelta -= glow; + loadingmodel->setglow(glow, glowdelta, glowpulse); +} + +COMMAND(mdlglow, "iif"); + +void mdlglare(float *specglare, float *glowglare) +{ + checkmdl; + loadingmodel->setglare(*specglare, *glowglare); +} + +COMMAND(mdlglare, "ff"); + +void mdlenvmap(float *envmapmax, float *envmapmin, char *envmap) +{ + checkmdl; + loadingmodel->setenvmap(*envmapmin, *envmapmax, envmap[0] ? cubemapload(envmap) : NULL); +} + +COMMAND(mdlenvmap, "ffs"); + +void mdlfullbright(float *fullbright) +{ + checkmdl; + loadingmodel->setfullbright(*fullbright); +} + +COMMAND(mdlfullbright, "f"); + +void mdlshader(char *shader) +{ + checkmdl; + loadingmodel->setshader(lookupshaderbyname(shader)); +} + +COMMAND(mdlshader, "s"); + +void mdlspin(float *yaw, float *pitch) +{ + checkmdl; + loadingmodel->spinyaw = *yaw; + loadingmodel->spinpitch = *pitch; +} + +COMMAND(mdlspin, "ff"); + +void mdlscale(int *percent) +{ + checkmdl; + float scale = 1.0f; + if(*percent>0) scale = *percent/100.0f; + loadingmodel->scale = scale; +} + +COMMAND(mdlscale, "i"); + +void mdltrans(float *x, float *y, float *z) +{ + checkmdl; + loadingmodel->translate = vec(*x, *y, *z); +} + +COMMAND(mdltrans, "fff"); + +void mdlyaw(float *angle) +{ + checkmdl; + loadingmodel->offsetyaw = *angle; +} + +COMMAND(mdlyaw, "f"); + +void mdlpitch(float *angle) +{ + checkmdl; + loadingmodel->offsetpitch = *angle; +} + +COMMAND(mdlpitch, "f"); + +void mdlshadow(int *shadow) +{ + checkmdl; + loadingmodel->shadow = *shadow!=0; +} + +COMMAND(mdlshadow, "i"); + +void mdlbb(float *rad, float *h, float *eyeheight) +{ + checkmdl; + loadingmodel->collidexyradius = *rad; + loadingmodel->collideheight = *h; + loadingmodel->eyeheight = *eyeheight; +} + +COMMAND(mdlbb, "fff"); + +void mdlextendbb(float *x, float *y, float *z) +{ + checkmdl; + loadingmodel->bbextend = vec(*x, *y, *z); +} + +COMMAND(mdlextendbb, "fff"); + +void mdlname() +{ + checkmdl; + result(loadingmodel->name); +} + +COMMAND(mdlname, ""); + +#define checkragdoll \ + checkmdl; \ + if(!loadingmodel->skeletal()) { conoutf(CON_ERROR, "not loading a skeletal model"); return; } \ + skelmodel *m = (skelmodel *)loadingmodel; \ + if(m->parts.empty()) return; \ + skelmodel::skelmeshgroup *meshes = (skelmodel::skelmeshgroup *)m->parts.last()->meshes; \ + if(!meshes) return; \ + skelmodel::skeleton *skel = meshes->skel; \ + if(!skel->ragdoll) skel->ragdoll = new ragdollskel; \ + ragdollskel *ragdoll = skel->ragdoll; \ + if(ragdoll->loaded) return; + + +void rdvert(float *x, float *y, float *z, float *radius) +{ + checkragdoll; + ragdollskel::vert &v = ragdoll->verts.add(); + v.pos = vec(*x, *y, *z); + v.radius = *radius > 0 ? *radius : 1; +} +COMMAND(rdvert, "ffff"); + +void rdeye(int *v) +{ + checkragdoll; + ragdoll->eye = *v; +} +COMMAND(rdeye, "i"); + +void rdtri(int *v1, int *v2, int *v3) +{ + checkragdoll; + ragdollskel::tri &t = ragdoll->tris.add(); + t.vert[0] = *v1; + t.vert[1] = *v2; + t.vert[2] = *v3; +} +COMMAND(rdtri, "iii"); + +void rdjoint(int *n, int *t, int *v1, int *v2, int *v3) +{ + checkragdoll; + if(*n < 0 || *n >= skel->numbones) return; + ragdollskel::joint &j = ragdoll->joints.add(); + j.bone = *n; + j.tri = *t; + j.vert[0] = *v1; + j.vert[1] = *v2; + j.vert[2] = *v3; +} +COMMAND(rdjoint, "iibbb"); + +void rdlimitdist(int *v1, int *v2, float *mindist, float *maxdist) +{ + checkragdoll; + ragdollskel::distlimit &d = ragdoll->distlimits.add(); + d.vert[0] = *v1; + d.vert[1] = *v2; + d.mindist = *mindist; + d.maxdist = max(*maxdist, *mindist); +} +COMMAND(rdlimitdist, "iiff"); + +void rdlimitrot(int *t1, int *t2, float *maxangle, float *qx, float *qy, float *qz, float *qw) +{ + checkragdoll; + ragdollskel::rotlimit &r = ragdoll->rotlimits.add(); + r.tri[0] = *t1; + r.tri[1] = *t2; + r.maxangle = *maxangle * RAD; + r.middle = matrix3(quat(*qx, *qy, *qz, *qw)); +} +COMMAND(rdlimitrot, "iifffff"); + +void rdanimjoints(int *on) +{ + checkragdoll; + ragdoll->animjoints = *on!=0; +} +COMMAND(rdanimjoints, "i"); + +// mapmodels + +vector mapmodels; + +void mmodel(char *name) +{ + mapmodelinfo &mmi = mapmodels.add(); + copystring(mmi.name, name); + mmi.m = NULL; +} + +void mapmodelcompat(int *rad, int *h, int *tex, char *name, char *shadow) +{ + mmodel(name); +} + +void mapmodelreset(int *n) +{ + if(!(identflags&IDF_OVERRIDDEN) && !game::allowedittoggle()) return; + mapmodels.shrink(clamp(*n, 0, mapmodels.length())); +} + +mapmodelinfo *getmminfo(int i) { return mapmodels.inrange(i) ? &mapmodels[i] : 0; } +const char *mapmodelname(int i) { return mapmodels.inrange(i) ? mapmodels[i].name : NULL; } + +COMMAND(mmodel, "s"); +COMMANDN(mapmodel, mapmodelcompat, "iiiss"); +COMMAND(mapmodelreset, "i"); +ICOMMAND(mapmodelname, "i", (int *index), { result(mapmodels.inrange(*index) ? mapmodels[*index].name : ""); }); +ICOMMAND(mapmodelloaded, "i", (int *index), { intret(mapmodels.inrange(*index) && mapmodels[*index].m ? 1 : 0); }); +ICOMMAND(nummapmodels, "", (), { intret(mapmodels.length()); }); +ICOMMAND(mapmodelfind, "s", (char *name), { int found = -1; loopv(mapmodels) if(strstr(mapmodels[i].name, name)) { found = i; break; } intret(found); }); + +// model registry + +hashnameset models; +vector preloadmodels; + +void preloadmodel(const char *name) +{ + if(!name || !name[0] || models.access(name)) return; + preloadmodels.add(newstring(name)); +} + +void flushpreloadedmodels(bool msg) +{ + loopv(preloadmodels) + { + loadprogress = float(i+1)/preloadmodels.length(); + model *m = loadmodel(preloadmodels[i], -1, msg); + if(!m) { if(msg) conoutf(CON_WARN, "could not load model: %s", preloadmodels[i]); } + else + { + m->preloadmeshes(); + } + } + preloadmodels.deletearrays(); + loadprogress = 0; +} + +void preloadusedmapmodels(bool msg, bool bih) +{ + vector &ents = entities::getents(); + vector mapmodels; + loopv(ents) + { + extentity &e = *ents[i]; + if(e.type==ET_MAPMODEL && e.attr2 >= 0 && mapmodels.find(e.attr2) < 0) mapmodels.add(e.attr2); + } + + loopv(mapmodels) + { + loadprogress = float(i+1)/mapmodels.length(); + int mmindex = mapmodels[i]; + mapmodelinfo *mmi = getmminfo(mmindex); + if(!mmi) { if(msg) conoutf(CON_WARN, "could not find map model: %d", mmindex); } + else if(mmi->name[0] && !loadmodel(NULL, mmindex, msg)) { if(msg) conoutf(CON_WARN, "could not load model: %s", mmi->name); } + else if(mmi->m) + { + if(bih) mmi->m->preloadBIH(); + mmi->m->preloadmeshes(); + } + } + loadprogress = 0; +} + +bool modelloaded(const char *name) +{ + return models.find(name, NULL) != NULL; +} + +model *loadmodel(const char *name, int i, bool msg) +{ + if(!name) + { + if(!mapmodels.inrange(i)) return NULL; + mapmodelinfo &mmi = mapmodels[i]; + if(mmi.m) return mmi.m; + name = mmi.name; + } + model **mm = models.access(name); + model *m; + if(mm) m = *mm; + else + { + if(!name[0] || loadingmodel || lightmapping > 1) return NULL; + if(msg) + { + defformatstring(filename, "packages/models/%s", name); + renderprogress(loadprogress, filename); + } + loopi(NUMMODELTYPES) + { + m = modeltypes[i](name); + if(!m) continue; + loadingmodel = m; + if(m->load()) break; + DELETEP(m); + } + loadingmodel = NULL; + if(!m) return NULL; + models.access(m->name, m); + m->preloadshaders(); + } + if(mapmodels.inrange(i) && !mapmodels[i].m) mapmodels[i].m = m; + return m; +} + +void preloadmodelshaders(bool force) +{ + if(initing) return; + enumerate(models, model *, m, m->preloadshaders(force)); +} + +void clear_mdls() +{ + enumerate(models, model *, m, delete m); +} + +void cleanupmodels() +{ + enumerate(models, model *, m, m->cleanup()); +} + +void clearmodel(char *name) +{ + model **m = models.access(name); + if(!m) { conoutf(CON_WARN, "model %s is not loaded", name); return; } + loopv(mapmodels) if(mapmodels[i].m==*m) mapmodels[i].m = NULL; + models.remove(name); + (*m)->cleanup(); + delete *m; + conoutf("cleared model %s", name); +} + +COMMAND(clearmodel, "s"); + +bool modeloccluded(const vec ¢er, float radius) +{ + ivec bbmin(vec(center).sub(radius)), bbmax(ivec(center).add(radius+1)); + return pvsoccluded(bbmin, bbmax) || bboccluded(bbmin, bbmax); +} + +VAR(showboundingbox, 0, 0, 2); + +void render2dbox(vec &o, float x, float y, float z) +{ + gle::begin(GL_LINE_LOOP); + gle::attribf(o.x, o.y, o.z); + gle::attribf(o.x, o.y, o.z+z); + gle::attribf(o.x+x, o.y+y, o.z+z); + gle::attribf(o.x+x, o.y+y, o.z); + xtraverts += gle::end(); +} + +void render3dbox(vec &o, float tofloor, float toceil, float xradius, float yradius) +{ + if(yradius<=0) yradius = xradius; + vec c = o; + c.sub(vec(xradius, yradius, tofloor)); + float xsz = xradius*2, ysz = yradius*2; + float h = tofloor+toceil; + gle::colorf(1, 1, 1); + gle::defvertex(); + render2dbox(c, xsz, 0, h); + render2dbox(c, 0, ysz, h); + c.add(vec(xsz, ysz, 0)); + render2dbox(c, -xsz, 0, h); + render2dbox(c, 0, -ysz, h); +} + +void renderellipse(vec &o, float xradius, float yradius, float yaw) +{ + gle::colorf(0.5f, 0.5f, 0.5f); + gle::defvertex(); + gle::begin(GL_LINE_LOOP); + loopi(15) + { + const vec2 &sc = sincos360[i*(360/15)]; + gle::attrib(vec(xradius*sc.x, yradius*sc.y, 0).rotate_around_z((yaw+90)*RAD).add(o)); + } + xtraverts += gle::end(); +} + +struct batchedmodel +{ + vec pos, color, dir; + int anim; + float yaw, pitch, transparent; + int basetime, basetime2, flags; + dynent *d; + int attached; + occludequery *query; +}; +struct modelbatch +{ + model *m; + int flags; + vector batched; +}; +static vector batches; +static vector modelattached; +static int numbatches = -1; +static occludequery *modelquery = NULL; + +void startmodelbatches() +{ + numbatches = 0; + modelattached.setsize(0); +} + +modelbatch &addbatchedmodel(model *m) +{ + modelbatch *b = NULL; + if(m->batch>=0 && m->batchbatch]->m==m) b = batches[m->batch]; + else + { + if(numbatchesbatched.setsize(0); + } + else b = batches.add(new modelbatch); + b->m = m; + b->flags = 0; + m->batch = numbatches++; + } + return *b; +} + +void renderbatchedmodel(model *m, batchedmodel &b) +{ + modelattach *a = NULL; + if(b.attached>=0) a = &modelattached[b.attached]; + + int anim = b.anim; + if(shadowmapping) + { + anim |= ANIM_NOSKIN; + GLOBALPARAMF(shadowintensity, b.transparent); + } + else + { + if(b.flags&MDL_FULLBRIGHT) anim |= ANIM_FULLBRIGHT; + if(b.flags&MDL_GHOST) anim |= ANIM_GHOST; + } + + m->render(anim, b.basetime, b.basetime2, b.pos, b.yaw, b.pitch, b.d, a, b.color, b.dir, b.transparent); +} + +struct transparentmodel +{ + model *m; + batchedmodel *batched; + float dist; +}; + +static inline bool sorttransparentmodels(const transparentmodel &x, const transparentmodel &y) +{ + return x.dist < y.dist; +} + +void endmodelbatches() +{ + vector transparent; + loopi(numbatches) + { + modelbatch &b = *batches[i]; + if(b.batched.empty()) continue; + if(b.flags&(MDL_SHADOW|MDL_DYNSHADOW)) + { + vec center, bbradius; + b.m->boundbox(center, bbradius); + loopvj(b.batched) + { + batchedmodel &bm = b.batched[j]; + if(bm.flags&(MDL_SHADOW|MDL_DYNSHADOW)) + renderblob(bm.flags&MDL_DYNSHADOW ? BLOB_DYNAMIC : BLOB_STATIC, bm.d && bm.d->ragdoll ? bm.d->ragdoll->center : bm.pos, bm.d ? bm.d->radius : max(bbradius.x, bbradius.y), bm.transparent); + } + flushblobs(); + } + bool rendered = false; + occludequery *query = NULL; + if(b.flags&MDL_GHOST) + { + loopvj(b.batched) + { + batchedmodel &bm = b.batched[j]; + if((bm.flags&(MDL_CULL_VFC|MDL_GHOST))!=MDL_GHOST || bm.query) continue; + if(!rendered) { b.m->startrender(); rendered = true; } + renderbatchedmodel(b.m, bm); + } + if(rendered) + { + b.m->endrender(); + rendered = false; + } + } + loopvj(b.batched) + { + batchedmodel &bm = b.batched[j]; + if(bm.flags&(MDL_CULL_VFC|MDL_GHOST)) continue; + if(bm.query!=query) + { + if(query) endquery(query); + query = bm.query; + if(query) startquery(query); + } + if(bm.transparent < 1 && (!query || query->owner==bm.d) && !shadowmapping) + { + transparentmodel &tm = transparent.add(); + tm.m = b.m; + tm.batched = &bm; + tm.dist = camera1->o.dist(bm.d && bm.d->ragdoll ? bm.d->ragdoll->center : bm.pos); + continue; + } + if(!rendered) { b.m->startrender(); rendered = true; } + renderbatchedmodel(b.m, bm); + } + if(query) endquery(query); + if(rendered) b.m->endrender(); + } + if(transparent.length()) + { + transparent.sort(sorttransparentmodels); + model *lastmodel = NULL; + occludequery *query = NULL; + loopv(transparent) + { + transparentmodel &tm = transparent[i]; + if(lastmodel!=tm.m) + { + if(lastmodel) lastmodel->endrender(); + (lastmodel = tm.m)->startrender(); + } + if(query!=tm.batched->query) + { + if(query) endquery(query); + query = tm.batched->query; + if(query) startquery(query); + } + renderbatchedmodel(tm.m, *tm.batched); + } + if(query) endquery(query); + if(lastmodel) lastmodel->endrender(); + } + numbatches = -1; +} + +void startmodelquery(occludequery *query) +{ + modelquery = query; +} + +void endmodelquery() +{ + int querybatches = 0; + loopi(numbatches) + { + modelbatch &b = *batches[i]; + if(b.batched.empty() || b.batched.last().query!=modelquery) continue; + querybatches++; + } + if(querybatches<=1) + { + if(!querybatches) modelquery->fragments = 0; + modelquery = NULL; + return; + } + int minattached = modelattached.length(); + startquery(modelquery); + loopi(numbatches) + { + modelbatch &b = *batches[i]; + if(b.batched.empty() || b.batched.last().query!=modelquery) continue; + b.m->startrender(); + do + { + batchedmodel &bm = b.batched.pop(); + if(bm.attached>=0) minattached = min(minattached, bm.attached); + renderbatchedmodel(b.m, bm); + } + while(b.batched.length() && b.batched.last().query==modelquery); + b.m->endrender(); + } + endquery(modelquery); + modelquery = NULL; + modelattached.setsize(minattached); +} + +VAR(maxmodelradiusdistance, 10, 200, 1000); + +static inline void enablecullmodelquery() +{ + startbb(); +} + +static inline void rendercullmodelquery(model *m, dynent *d, const vec ¢er, float radius) +{ + if(fabs(camera1->o.x-center.x) < radius+1 && + fabs(camera1->o.y-center.y) < radius+1 && + fabs(camera1->o.z-center.z) < radius+1) + { + d->query = NULL; + return; + } + d->query = newquery(d); + if(!d->query) return; + startquery(d->query); + int br = int(radius*2)+1; + drawbb(ivec(int(center.x-radius), int(center.y-radius), int(center.z-radius)), ivec(br, br, br)); + endquery(d->query); +} + +static inline void disablecullmodelquery() +{ + endbb(); +} + +static inline int cullmodel(model *m, const vec ¢er, float radius, int flags, dynent *d = NULL, bool shadow = false) +{ + if(flags&MDL_CULL_DIST && center.dist(camera1->o)/radius>maxmodelradiusdistance) return MDL_CULL_DIST; + if(flags&MDL_CULL_VFC) + { + if(reflecting || refracting) + { + if(reflecting || refracting>0) + { + if(center.z+radius<=reflectz) return MDL_CULL_VFC; + } + else + { + if(fogging && center.z+radius=reflectz) return MDL_CULL_VFC; + } + if(center.dist(camera1->o)-radius>reflectdist) return MDL_CULL_VFC; + } + if(isfoggedsphere(radius, center)) return MDL_CULL_VFC; + if(shadowmapping && !isshadowmapcaster(center, radius)) return MDL_CULL_VFC; + } + if(shadowmapping) + { + if(d) + { + if(flags&MDL_CULL_OCCLUDED && d->occluded>=OCCLUDE_PARENT) return MDL_CULL_OCCLUDED; + if(flags&MDL_CULL_QUERY && d->occluded+1>=OCCLUDE_BB && d->query && d->query->owner==d && checkquery(d->query)) return MDL_CULL_QUERY; + } + if(!addshadowmapcaster(center, radius, radius)) return MDL_CULL_VFC; + } + else if(flags&MDL_CULL_OCCLUDED && modeloccluded(center, radius)) + { + if(!reflecting && !refracting && d) d->occluded = OCCLUDE_PARENT; + return MDL_CULL_OCCLUDED; + } + else if(flags&MDL_CULL_QUERY && d->query && d->query->owner==d && checkquery(d->query)) + { + if(!reflecting && !refracting && d->occludedoccluded++; + return MDL_CULL_QUERY; + } + return 0; +} + +void rendermodel(entitylight *light, const char *mdl, int anim, const vec &o, float yaw, float pitch, int flags, dynent *d, modelattach *a, int basetime, int basetime2, float trans) +{ + if(shadowmapping && !(flags&(MDL_SHADOW|MDL_DYNSHADOW))) return; + model *m = loadmodel(mdl); + if(!m) return; + vec center(0, 0, 0), bbradius(0, 0, 0); + float radius = 0; + bool shadow = !shadowmap && !glaring && (flags&(MDL_SHADOW|MDL_DYNSHADOW)) && showblobs; + + if(flags&(MDL_CULL_VFC|MDL_CULL_DIST|MDL_CULL_OCCLUDED|MDL_CULL_QUERY|MDL_SHADOW|MDL_DYNSHADOW)) + { + if(flags&MDL_CULL_QUERY) + { + if(!oqfrags || !oqdynent || !d) flags &= ~MDL_CULL_QUERY; + } + + m->boundbox(center, bbradius); + radius = bbradius.magnitude(); + if(d && d->ragdoll) + { + radius = max(radius, d->ragdoll->radius); + center = d->ragdoll->center; + } + else + { + center.rotate_around_z(yaw*RAD); + center.add(o); + } + + int culled = cullmodel(m, center, radius, flags, d, shadow); + if(culled) + { + if(culled&(MDL_CULL_OCCLUDED|MDL_CULL_QUERY) && flags&MDL_CULL_QUERY && !reflecting && !refracting) + { + enablecullmodelquery(); + rendercullmodelquery(m, d, center, radius); + disablecullmodelquery(); + } + return; + } + + if(reflecting || refracting || shadowmapping) flags &= ~MDL_CULL_QUERY; + } + + if(flags&MDL_NORENDER) anim |= ANIM_NORENDER; + else if(showboundingbox && !shadowmapping && !reflecting && !refracting && editmode) + { + notextureshader->set(); + if(d && showboundingbox==1) + { + render3dbox(d->o, d->eyeheight, d->aboveeye, d->radius); + renderellipse(d->o, d->xradius, d->yradius, d->yaw); + } + else + { + vec center, radius; + if(showboundingbox==1) m->collisionbox(center, radius); + else m->boundbox(center, radius); + rotatebb(center, radius, int(yaw)); + center.add(o); + render3dbox(center, radius.z, radius.z, radius.x, radius.y); + } + } + + vec lightcolor(1, 1, 1), lightdir(0, 0, 1); + if(!shadowmapping) + { + vec pos = o; + if(d) + { + if(!reflecting && !refracting) d->occluded = OCCLUDE_NOTHING; + if(!light) light = &d->light; + if(flags&MDL_LIGHT && light->millis!=lastmillis) + { + if(d->ragdoll) + { + pos = d->ragdoll->center; + pos.z += radius/2; + } + else if(d->type < ENT_CAMERA) pos.z += 0.75f*(d->eyeheight + d->aboveeye); + lightreaching(pos, light->color, light->dir, (flags&MDL_LIGHT_FAST)!=0); + dynlightreaching(pos, light->color, light->dir, (flags&MDL_HUD)!=0); + game::lighteffects(d, light->color, light->dir); + light->millis = lastmillis; + } + } + else if(flags&MDL_LIGHT) + { + if(!light) + { + lightreaching(pos, lightcolor, lightdir, (flags&MDL_LIGHT_FAST)!=0); + dynlightreaching(pos, lightcolor, lightdir, (flags&MDL_HUD)!=0); + } + else if(light->millis!=lastmillis) + { + lightreaching(pos, light->color, light->dir, (flags&MDL_LIGHT_FAST)!=0); + dynlightreaching(pos, light->color, light->dir, (flags&MDL_HUD)!=0); + light->millis = lastmillis; + } + } + if(light) { lightcolor = light->color; lightdir = light->dir; } + if(flags&MDL_DYNLIGHT) dynlightreaching(pos, lightcolor, lightdir, (flags&MDL_HUD)!=0); + } + + if(a) for(int i = 0; a[i].tag; i++) + { + if(a[i].name) a[i].m = loadmodel(a[i].name); + //if(a[i].m && a[i].m->type()!=m->type()) a[i].m = NULL; + } + + if(numbatches>=0) + { + modelbatch &mb = addbatchedmodel(m); + batchedmodel &b = mb.batched.add(); + b.query = modelquery; + b.pos = o; + b.color = lightcolor; + b.dir = lightdir; + b.anim = anim; + b.yaw = yaw; + b.pitch = pitch; + b.basetime = basetime; + b.basetime2 = basetime2; + b.transparent = trans; + b.flags = flags & ~(MDL_CULL_VFC | MDL_CULL_DIST | MDL_CULL_OCCLUDED); + if(!shadow || reflecting || refracting>0) + { + b.flags &= ~(MDL_SHADOW|MDL_DYNSHADOW); + if((flags&MDL_CULL_VFC) && refracting<0 && center.z-radius>=reflectz) b.flags |= MDL_CULL_VFC; + } + mb.flags |= b.flags; + b.d = d; + b.attached = a ? modelattached.length() : -1; + if(a) for(int i = 0;; i++) { modelattached.add(a[i]); if(!a[i].tag) break; } + if(flags&MDL_CULL_QUERY) d->query = b.query = newquery(d); + return; + } + + if(shadow && !reflecting && refracting<=0) + { + renderblob(flags&MDL_DYNSHADOW ? BLOB_DYNAMIC : BLOB_STATIC, d && d->ragdoll ? center : o, d ? d->radius : max(bbradius.x, bbradius.y), trans); + flushblobs(); + if((flags&MDL_CULL_VFC) && refracting<0 && center.z-radius>=reflectz) return; + } + + m->startrender(); + + if(shadowmapping) + { + anim |= ANIM_NOSKIN; + GLOBALPARAMF(shadowintensity, trans); + } + else + { + if(flags&MDL_FULLBRIGHT) anim |= ANIM_FULLBRIGHT; + if(flags&MDL_GHOST) anim |= ANIM_GHOST; + } + + if(flags&MDL_CULL_QUERY) + { + d->query = newquery(d); + if(d->query) startquery(d->query); + } + + m->render(anim, basetime, basetime2, o, yaw, pitch, d, a, lightcolor, lightdir, trans); + + if(flags&MDL_CULL_QUERY && d->query) endquery(d->query); + + m->endrender(); +} + +void abovemodel(vec &o, const char *mdl) +{ + model *m = loadmodel(mdl); + if(!m) return; + o.z += m->above(); +} + +bool matchanim(const char *name, const char *pattern) +{ + for(;; pattern++) + { + const char *s = name; + char c; + for(;; pattern++) + { + c = *pattern; + if(!c || c=='|') break; + else if(c=='*') + { + if(!*s || iscubespace(*s)) break; + do s++; while(*s && !iscubespace(*s)); + } + else if(c!=*s) break; + else s++; + } + if(!*s && (!c || c=='|')) return true; + pattern = strchr(pattern, '|'); + if(!pattern) break; + } + return false; +} + +void findanims(const char *pattern, vector &anims) +{ + loopi(sizeof(animnames)/sizeof(animnames[0])) if(matchanim(animnames[i], pattern)) anims.add(i); +} + +ICOMMAND(findanims, "s", (char *name), +{ + vector anims; + findanims(name, anims); + vector buf; + string num; + loopv(anims) + { + formatstring(num, "%d", anims[i]); + if(i > 0) buf.add(' '); + buf.put(num, strlen(num)); + } + buf.add('\0'); + result(buf.getbuf()); +}); + +void loadskin(const char *dir, const char *altdir, Texture *&skin, Texture *&masks) // model skin sharing +{ +#define ifnoload(tex, path) if((tex = textureload(path, 0, true, false))==notexture) +#define tryload(tex, prefix, cmd, name) \ + ifnoload(tex, makerelpath(mdir, name ".jpg", prefix, cmd)) \ + { \ + ifnoload(tex, makerelpath(mdir, name ".png", prefix, cmd)) \ + { \ + ifnoload(tex, makerelpath(maltdir, name ".jpg", prefix, cmd)) \ + { \ + ifnoload(tex, makerelpath(maltdir, name ".png", prefix, cmd)) return; \ + } \ + } \ + } + + defformatstring(mdir, "packages/models/%s", dir); + defformatstring(maltdir, "packages/models/%s", altdir); + masks = notexture; + tryload(skin, NULL, NULL, "skin"); + tryload(masks, NULL, NULL, "masks"); +} + +// convenient function that covers the usual anims for players/monsters/npcs + +VAR(animoverride, -1, 0, NUMANIMS-1); +VAR(testanims, 0, 0, 1); +VAR(testpitch, -90, 0, 90); + +void renderclient(dynent *d, const char *mdlname, modelattach *attachments, int hold, int attack, int attackdelay, int lastaction, int lastpain, float fade, bool ragdoll) +{ + int anim = hold ? hold : ANIM_IDLE|ANIM_LOOP; + float yaw = testanims && d==player ? 0 : d->yaw+90, + pitch = testpitch && d==player ? testpitch : d->pitch; + vec o = d->feetpos(); + int basetime = 0; + if(animoverride) anim = (animoverride<0 ? ANIM_ALL : animoverride)|ANIM_LOOP; + else if(d->state==CS_DEAD) + { + anim = ANIM_DYING|ANIM_NOPITCH; + basetime = lastpain; + if(ragdoll) + { + if(!d->ragdoll || d->ragdoll->millis < basetime) + { + DELETEP(d->ragdoll); + anim |= ANIM_RAGDOLL; + } + } + else if(lastmillis-basetime>1000) anim = ANIM_DEAD|ANIM_LOOP|ANIM_NOPITCH; + } + else if(d->state==CS_EDITING || d->state==CS_SPECTATOR) anim = ANIM_EDIT|ANIM_LOOP; + else if(d->state==CS_LAGGED) anim = ANIM_LAG|ANIM_LOOP; + else + { + if(lastmillis-lastpain < 300) + { + anim = ANIM_PAIN; + basetime = lastpain; + } + else if(lastpain < lastaction && (attack < 0 || (d->type != ENT_AI && lastmillis-lastaction < attackdelay))) + { + anim = attack < 0 ? -attack : attack; + basetime = lastaction; + } + + if(d->inwater && d->physstate<=PHYS_FALL) anim |= (((game::allowmove(d) && (d->move || d->strafe)) || d->vel.z+d->falling.z>0 ? ANIM_SWIM : ANIM_SINK)|ANIM_LOOP)<timeinair>100) anim |= (ANIM_JUMP|ANIM_END)<move || d->strafe)) + { + if(d->move>0) anim |= (ANIM_FORWARD|ANIM_LOOP)<strafe) + { + if(d->move<0) anim |= ((d->strafe>0 ? ANIM_RIGHT : ANIM_LEFT)|ANIM_REVERSE|ANIM_LOOP)<strafe>0 ? ANIM_LEFT : ANIM_RIGHT)|ANIM_LOOP)<move<0) anim |= (ANIM_BACKWARD|ANIM_LOOP)<>ANIM_SECONDARY)&ANIM_INDEX) anim >>= ANIM_SECONDARY; + } + if(d->ragdoll && (!ragdoll || (anim&ANIM_INDEX)!=ANIM_DYING)) DELETEP(d->ragdoll); + if(!((anim>>ANIM_SECONDARY)&ANIM_INDEX)) anim |= (ANIM_IDLE|ANIM_LOOP)<type==ENT_PLAYER) flags |= MDL_FULLBRIGHT; + else flags |= MDL_CULL_DIST; + if(d->state==CS_LAGGED) fade = min(fade, 0.3f); + else flags |= MDL_DYNSHADOW; + if(drawtex == DRAWTEX_MODELPREVIEW) flags &= ~(MDL_LIGHT | MDL_FULLBRIGHT | MDL_CULL_VFC | MDL_CULL_OCCLUDED | MDL_CULL_QUERY | MDL_CULL_DIST | MDL_DYNSHADOW); + rendermodel(NULL, mdlname, anim, o, yaw, pitch, flags, d, attachments, basetime, 0, fade); +} + +void setbbfrommodel(dynent *d, const char *mdl) +{ + model *m = loadmodel(mdl); + if(!m) return; + vec center, radius; + m->collisionbox(center, radius); + if(d->type==ENT_INANIMATE && !m->ellipsecollide) + d->collidetype = COLLIDE_OBB; + d->xradius = radius.x + fabs(center.x); + d->yradius = radius.y + fabs(center.y); + d->radius = d->collidetype==COLLIDE_OBB ? sqrtf(d->xradius*d->xradius + d->yradius*d->yradius) : max(d->xradius, d->yradius); + d->eyeheight = (center.z-radius.z) + radius.z*2*m->eyeheight; + d->aboveeye = radius.z*2*(1.0f-m->eyeheight); + if (d->aboveeye + d->eyeheight <= 0.5f) + { + float zrad = (0.5f - (d->aboveeye + d->eyeheight)) / 2; + d->aboveeye += zrad; + d->eyeheight += zrad; + } +} + diff --git a/src/engine/renderparticles.cpp b/src/engine/renderparticles.cpp new file mode 100644 index 0000000..17350cf --- /dev/null +++ b/src/engine/renderparticles.cpp @@ -0,0 +1,1551 @@ +// renderparticles.cpp + +#include "engine.h" +#include "rendertarget.h" + +Shader *particleshader = NULL, *particlenotextureshader = NULL; + +VARP(particlesize, 20, 100, 500); + +// Check canemitparticles() to limit the rate that paricles can be emitted for models/sparklies +// Automatically stops particles being emitted when paused or in reflective drawing +VARP(emitmillis, 1, 17, 1000); +static int lastemitframe = 0, emitoffset = 0; +static bool canemit = false, regenemitters = false, canstep = false; + +static bool canemitparticles() +{ + if(reflecting || refracting) return false; + return canemit || emitoffset; +} + +VARP(showparticles, 0, 1, 1); +VAR(cullparticles, 0, 1, 1); +VAR(replayparticles, 0, 1, 1); +VARN(seedparticles, seedmillis, 0, 3000, 10000); +VAR(dbgpcull, 0, 0, 1); +VAR(dbgpseed, 0, 0, 1); + +struct particleemitter +{ + extentity *ent; + vec bbmin, bbmax; + vec center; + float radius; + ivec cullmin, cullmax; + int maxfade, lastemit, lastcull; + + particleemitter(extentity *ent) + : ent(ent), bbmin(ent->o), bbmax(ent->o), maxfade(-1), lastemit(0), lastcull(0) + {} + + void finalize() + { + center = vec(bbmin).add(bbmax).mul(0.5f); + radius = bbmin.dist(bbmax)/2; + cullmin = ivec(int(floor(bbmin.x)), int(floor(bbmin.y)), int(floor(bbmin.z))); + cullmax = ivec(int(ceil(bbmax.x)), int(ceil(bbmax.y)), int(ceil(bbmax.z))); + if(dbgpseed) conoutf(CON_DEBUG, "radius: %f, maxfade: %d", radius, maxfade); + } + + void extendbb(const vec &o, float size = 0) + { + bbmin.x = min(bbmin.x, o.x - size); + bbmin.y = min(bbmin.y, o.y - size); + bbmin.z = min(bbmin.z, o.z - size); + bbmax.x = max(bbmax.x, o.x + size); + bbmax.y = max(bbmax.y, o.y + size); + bbmax.z = max(bbmax.z, o.z + size); + } + + void extendbb(float z, float size = 0) + { + bbmin.z = min(bbmin.z, z - size); + bbmax.z = max(bbmax.z, z + size); + } +}; + +static vector emitters; +static particleemitter *seedemitter = NULL; + +void clearparticleemitters() +{ + emitters.setsize(0); + regenemitters = true; +} + +void addparticleemitters() +{ + emitters.setsize(0); + const vector &ents = entities::getents(); + loopv(ents) + { + extentity &e = *ents[i]; + if(e.type != ET_PARTICLES) continue; + emitters.add(particleemitter(&e)); + } + regenemitters = false; +} + +enum +{ + PT_PART = 0, + PT_TAPE, + PT_TRAIL, + PT_TEXT, + PT_TEXTICON, + PT_METER, + PT_METERVS, + PT_FIREBALL, + PT_LIGHTNING, + PT_FLARE, + + PT_MOD = 1<<8, + PT_RND4 = 1<<9, + PT_LERP = 1<<10, // use very sparingly - order of blending issues + PT_TRACK = 1<<11, + PT_GLARE = 1<<12, + PT_SOFT = 1<<13, + PT_HFLIP = 1<<14, + PT_VFLIP = 1<<15, + PT_ROT = 1<<16, + PT_CULL = 1<<17, + PT_FEW = 1<<18, + PT_ICON = 1<<19, + PT_NOTEX = 1<<20, + PT_SHADER = 1<<21, + PT_FLIP = PT_HFLIP | PT_VFLIP | PT_ROT +}; + +const char *partnames[] = { "part", "tape", "trail", "text", "texticon", "meter", "metervs", "fireball", "lightning", "flare" }; + +struct particle +{ + vec o, d; + int gravity, fade, millis; + bvec color; + uchar flags; + float size; + union + { + const char *text; + float val; + physent *owner; + struct + { + uchar color2[3]; + uchar progress; + }; + }; +}; + +struct partvert +{ + vec pos; + bvec4 color; + vec2 tc; +}; + +#define COLLIDERADIUS 8.0f +#define COLLIDEERROR 1.0f + +struct partrenderer +{ + Texture *tex; + const char *texname; + int texclamp; + uint type; + int collide; + string info; + + partrenderer(const char *texname, int texclamp, int type, int collide = 0) + : tex(NULL), texname(texname), texclamp(texclamp), type(type), collide(collide) + { + } + partrenderer(int type, int collide = 0) + : tex(NULL), texname(NULL), texclamp(0), type(type), collide(collide) + { + } + virtual ~partrenderer() + { + } + + virtual void init(int n) { } + virtual void reset() = 0; + virtual void resettracked(physent *owner) { } + virtual particle *addpart(const vec &o, const vec &d, int fade, int color, float size, int gravity = 0) = 0; + virtual int adddepthfx(vec &bbmin, vec &bbmax) { return 0; } + virtual void update() { } + virtual void render() = 0; + virtual bool haswork() = 0; + virtual int count() = 0; //for debug + virtual void cleanup() {} + + virtual void seedemitter(particleemitter &pe, const vec &o, const vec &d, int fade, float size, int gravity) + { + } + + //blend = 0 => remove it + void calc(particle *p, int &blend, int &ts, vec &o, vec &d, bool step = true) + { + o = p->o; + d = p->d; + if(type&PT_TRACK && p->owner) game::particletrack(p->owner, o, d); + if(p->fade <= 5) + { + ts = 1; + blend = 255; + } + else + { + ts = lastmillis-p->millis; + blend = max(255 - (ts<<8)/p->fade, 0); + if(p->gravity) + { + if(ts > p->fade) ts = p->fade; + float t = ts; + o.add(vec(d).mul(t/5000.0f)); + o.z -= t*t/(2.0f * 5000.0f * p->gravity); + } + if(collide && o.z < p->val && step) + { + if(collide >= 0) + { + vec surface; + float floorz = rayfloor(vec(o.x, o.y, p->val), surface, RAY_CLIPMAT, COLLIDERADIUS); + float collidez = floorz<0 ? o.z-COLLIDERADIUS : p->val - floorz; + if(o.z >= collidez+COLLIDEERROR) + p->val = collidez+COLLIDEERROR; + else + { + adddecal(collide, vec(o.x, o.y, collidez), vec(p->o).sub(o).normalize(), 2*p->size, p->color, type&PT_RND4 ? (p->flags>>5)&3 : 0); + blend = 0; + } + } + else blend = 0; + } + } + } + + const char *debuginfo() + { + formatstring(info, "%d\t%s(", count(), partnames[type&0xFF]); + if(type&PT_GLARE) concatstring(info, "g,"); + if(type&PT_SOFT) concatstring(info, "s,"); + if(type&PT_LERP) concatstring(info, "l,"); + if(type&PT_MOD) concatstring(info, "m,"); + if(type&PT_RND4) concatstring(info, "r,"); + if(type&PT_FLIP) concatstring(info, "f,"); + if(collide) concatstring(info, "c,"); + int len = strlen(info); + info[len-1] = info[len-1] == ',' ? ')' : '\0'; + if(texname) + { + const char *title = strrchr(texname, '/'); + if(title) concformatstring(info, ": %s", title+1); + } + return info; + } +}; + +struct listparticle : particle +{ + listparticle *next; +}; + +VARP(outlinemeters, 0, 0, 1); + +struct listrenderer : partrenderer +{ + static listparticle *parempty; + listparticle *list; + + listrenderer(const char *texname, int texclamp, int type, int collide = 0) + : partrenderer(texname, texclamp, type, collide), list(NULL) + { + } + listrenderer(int type, int collide = 0) + : partrenderer(type, collide), list(NULL) + { + } + + virtual ~listrenderer() + { + } + + virtual void killpart(listparticle *p) + { + } + + void reset() + { + if(!list) return; + listparticle *p = list; + for(;;) + { + killpart(p); + if(p->next) p = p->next; + else break; + } + p->next = parempty; + parempty = list; + list = NULL; + } + + void resettracked(physent *owner) + { + if(!(type&PT_TRACK)) return; + for(listparticle **prev = &list, *cur = list; cur; cur = *prev) + { + if(!owner || cur->owner==owner) + { + *prev = cur->next; + cur->next = parempty; + parempty = cur; + } + else prev = &cur->next; + } + } + + particle *addpart(const vec &o, const vec &d, int fade, int color, float size, int gravity) + { + if(!parempty) + { + listparticle *ps = new listparticle[256]; + loopi(255) ps[i].next = &ps[i+1]; + ps[255].next = parempty; + parempty = ps; + } + listparticle *p = parempty; + parempty = p->next; + p->next = list; + list = p; + p->o = o; + p->d = d; + p->gravity = gravity; + p->fade = fade; + p->millis = lastmillis + emitoffset; + p->color = bvec(color>>16, (color>>8)&0xFF, color&0xFF); + p->size = size; + p->owner = NULL; + p->flags = 0; + return p; + } + + int count() + { + int num = 0; + listparticle *lp; + for(lp = list; lp; lp = lp->next) num++; + return num; + } + + bool haswork() + { + return (list != NULL); + } + + virtual void startrender() = 0; + virtual void endrender() = 0; + virtual void renderpart(listparticle *p, const vec &o, const vec &d, int blend, int ts) = 0; + + void render() + { + startrender(); + if(texname) + { + if(!tex) tex = textureload(texname, texclamp); + glBindTexture(GL_TEXTURE_2D, tex->id); + } + + for(listparticle **prev = &list, *p = list; p; p = *prev) + { + vec o, d; + int blend, ts; + calc(p, blend, ts, o, d, canstep); + if(blend > 0) + { + renderpart(p, o, d, blend, ts); + + if(p->fade > 5 || !canstep) + { + prev = &p->next; + continue; + } + } + //remove + *prev = p->next; + p->next = parempty; + killpart(p); + parempty = p; + } + + endrender(); + } +}; + +listparticle *listrenderer::parempty = NULL; + +struct meterrenderer : listrenderer +{ + meterrenderer(int type) + : listrenderer(type|PT_NOTEX|PT_LERP) + {} + + void startrender() + { + glDisable(GL_BLEND); + gle::defvertex(); + } + + void endrender() + { + glEnable(GL_BLEND); + } + + void renderpart(listparticle *p, const vec &o, const vec &d, int blend, int ts) + { + int basetype = type&0xFF; + float scale = FONTH*p->size/80.0f, right = 8, left = p->progress/100.0f*right; + matrix4x3 m(camright, vec(camup).neg(), vec(camdir).neg(), o); + m.scale(scale); + m.translate(-right/2.0f, 0, 0); + + if(outlinemeters) + { + gle::colorf(0, 0.8f, 0); + gle::begin(GL_TRIANGLE_STRIP); + loopk(10) + { + const vec2 &sc = sincos360[k*(180/(10-1))]; + float c = (0.5f + 0.1f)*sc.y, s = 0.5f - (0.5f + 0.1f)*sc.x; + gle::attrib(m.transform(vec2(-c, s))); + gle::attrib(m.transform(vec2(right + c, s))); + } + gle::end(); + } + + if(basetype==PT_METERVS) gle::colorub(p->color2[0], p->color2[1], p->color2[2]); + else gle::colorf(0, 0, 0); + gle::begin(GL_TRIANGLE_STRIP); + loopk(10) + { + const vec2 &sc = sincos360[k*(180/(10-1))]; + float c = 0.5f*sc.y, s = 0.5f - 0.5f*sc.x; + gle::attrib(m.transform(vec2(left + c, s))); + gle::attrib(m.transform(vec2(right + c, s))); + } + gle::end(); + + if(outlinemeters) + { + gle::colorf(0, 0.8f, 0); + gle::begin(GL_TRIANGLE_FAN); + loopk(10) + { + const vec2 &sc = sincos360[k*(180/(10-1))]; + float c = (0.5f + 0.1f)*sc.y, s = 0.5f - (0.5f + 0.1f)*sc.x; + gle::attrib(m.transform(vec2(left + c, s))); + } + gle::end(); + } + + gle::color(p->color); + gle::begin(GL_TRIANGLE_STRIP); + loopk(10) + { + const vec2 &sc = sincos360[k*(180/(10-1))]; + float c = 0.5f*sc.y, s = 0.5f - 0.5f*sc.x; + gle::attrib(m.transform(vec2(-c, s))); + gle::attrib(m.transform(vec2(left + c, s))); + } + gle::end(); + } +}; +static meterrenderer meters(PT_METER), metervs(PT_METERVS); + +struct textrenderer : listrenderer +{ + textrenderer(int type) + : listrenderer(type) + {} + + void startrender() + { + } + + void endrender() + { + } + + void killpart(listparticle *p) + { + if(p->text && p->flags&1) delete[] p->text; + } + + void renderpart(listparticle *p, const vec &o, const vec &d, int blend, int ts) + { + float scale = p->size/80.0f, xoff = -(text_width(p->text) + ((p->flags>>1)&7)*FONTH)/2, yoff = 0; + + matrix4x3 m(camright, vec(camup).neg(), vec(camdir).neg(), o); + m.scale(scale); + m.translate(xoff, yoff, 50); + + textmatrix = &m; + draw_text(p->text, 0, 0, p->color.r, p->color.g, p->color.b, blend); + textmatrix = NULL; + } +}; +static textrenderer texts(PT_TEXT|PT_LERP); + +struct texticonrenderer : listrenderer +{ + texticonrenderer(const char *texname, int type) + : listrenderer(texname, 3, type) + {} + + void startrender() + { + gle::defvertex(); + gle::deftexcoord0(); + gle::defcolor(4, GL_UNSIGNED_BYTE); + gle::begin(GL_QUADS); + } + + void endrender() + { + gle::end(); + } + + void renderpart(listparticle *p, const vec &o, const vec &d, int blend, int ts) + { + float scale = p->size/80.0f, xoff = p->val, yoff = 0; + + matrix4x3 m(camright, vec(camup).neg(), vec(camdir).neg(), o); + m.scale(scale); + m.translate(xoff, yoff, 50); + + float tx = 0.25f*(p->flags&3), ty = 0.25f*((p->flags>>2)&3); + + gle::attrib(m.transform(vec2(0, 0))); + gle::attrib(tx, ty); + gle::attrib(p->color, blend); + gle::attrib(m.transform(vec2(FONTH, 0))); + gle::attrib(tx + 0.25f, ty); + gle::attrib(p->color, blend); + gle::attrib(m.transform(vec2(FONTH, FONTH))); + gle::attrib(tx + 0.25f, ty + 0.25f); + gle::attrib(p->color, blend); + gle::attrib(m.transform(vec2(0, FONTH))); + gle::attrib(tx, ty + 0.25f); + gle::attrib(p->color, blend); + } +}; +static texticonrenderer texticons("packages/hud/items.png", PT_TEXTICON|PT_LERP); + +template +static inline void modifyblend(const vec &o, int &blend) +{ + blend = min(blend<<2, 255); +} + +template<> +inline void modifyblend(const vec &o, int &blend) +{ +} + +template +static inline void genpos(const vec &o, const vec &d, float size, int grav, int ts, partvert *vs) +{ + vec udir = vec(camup).sub(camright).mul(size); + vec vdir = vec(camup).add(camright).mul(size); + vs[0].pos = vec(o.x + udir.x, o.y + udir.y, o.z + udir.z); + vs[1].pos = vec(o.x + vdir.x, o.y + vdir.y, o.z + vdir.z); + vs[2].pos = vec(o.x - udir.x, o.y - udir.y, o.z - udir.z); + vs[3].pos = vec(o.x - vdir.x, o.y - vdir.y, o.z - vdir.z); +} + +template<> +inline void genpos(const vec &o, const vec &d, float size, int ts, int grav, partvert *vs) +{ + vec dir1 = d, dir2 = d, c; + dir1.sub(o); + dir2.sub(camera1->o); + c.cross(dir2, dir1).normalize().mul(size); + vs[0].pos = vec(d.x-c.x, d.y-c.y, d.z-c.z); + vs[1].pos = vec(o.x-c.x, o.y-c.y, o.z-c.z); + vs[2].pos = vec(o.x+c.x, o.y+c.y, o.z+c.z); + vs[3].pos = vec(d.x+c.x, d.y+c.y, d.z+c.z); +} + +template<> +inline void genpos(const vec &o, const vec &d, float size, int ts, int grav, partvert *vs) +{ + vec e = d; + if(grav) e.z -= float(ts)/grav; + e.div(-75.0f).add(o); + genpos(o, e, size, ts, grav, vs); +} + +template +static inline void genrotpos(const vec &o, const vec &d, float size, int grav, int ts, partvert *vs, int rot) +{ + genpos(o, d, size, grav, ts, vs); +} + +#define ROTCOEFFS(n) { \ + vec(-1, 1, 0).rotate_around_z(n*2*M_PI/32.0f), \ + vec( 1, 1, 0).rotate_around_z(n*2*M_PI/32.0f), \ + vec( 1, -1, 0).rotate_around_z(n*2*M_PI/32.0f), \ + vec(-1, -1, 0).rotate_around_z(n*2*M_PI/32.0f) \ +} +static const vec rotcoeffs[32][4] = +{ + ROTCOEFFS(0), ROTCOEFFS(1), ROTCOEFFS(2), ROTCOEFFS(3), ROTCOEFFS(4), ROTCOEFFS(5), ROTCOEFFS(6), ROTCOEFFS(7), + ROTCOEFFS(8), ROTCOEFFS(9), ROTCOEFFS(10), ROTCOEFFS(11), ROTCOEFFS(12), ROTCOEFFS(13), ROTCOEFFS(14), ROTCOEFFS(15), + ROTCOEFFS(16), ROTCOEFFS(17), ROTCOEFFS(18), ROTCOEFFS(19), ROTCOEFFS(20), ROTCOEFFS(21), ROTCOEFFS(22), ROTCOEFFS(7), + ROTCOEFFS(24), ROTCOEFFS(25), ROTCOEFFS(26), ROTCOEFFS(27), ROTCOEFFS(28), ROTCOEFFS(29), ROTCOEFFS(30), ROTCOEFFS(31), +}; + +template<> +inline void genrotpos(const vec &o, const vec &d, float size, int grav, int ts, partvert *vs, int rot) +{ + const vec *coeffs = rotcoeffs[rot]; + (vs[0].pos = o).add(vec(camright).mul(coeffs[0].x*size)).add(vec(camup).mul(coeffs[0].y*size)); + (vs[1].pos = o).add(vec(camright).mul(coeffs[1].x*size)).add(vec(camup).mul(coeffs[1].y*size)); + (vs[2].pos = o).add(vec(camright).mul(coeffs[2].x*size)).add(vec(camup).mul(coeffs[2].y*size)); + (vs[3].pos = o).add(vec(camright).mul(coeffs[3].x*size)).add(vec(camup).mul(coeffs[3].y*size)); +} + +template +static inline void seedpos(particleemitter &pe, const vec &o, const vec &d, int fade, float size, int grav) +{ + if(grav) + { + vec end(o); + float t = fade; + end.add(vec(d).mul(t/5000.0f)); + end.z -= t*t/(2.0f * 5000.0f * grav); + pe.extendbb(end, size); + + float tpeak = d.z*grav; + if(tpeak > 0 && tpeak < fade) pe.extendbb(o.z + 1.5f*d.z*tpeak/5000.0f, size); + } +} + +template<> +inline void seedpos(particleemitter &pe, const vec &o, const vec &d, int fade, float size, int grav) +{ + pe.extendbb(d, size); +} + +template<> +inline void seedpos(particleemitter &pe, const vec &o, const vec &d, int fade, float size, int grav) +{ + vec e = d; + if(grav) e.z -= float(fade)/grav; + e.div(-75.0f).add(o); + pe.extendbb(e, size); +} + +template +struct varenderer : partrenderer +{ + partvert *verts; + particle *parts; + int maxparts, numparts, lastupdate, rndmask; + GLuint vbo; + + varenderer(const char *texname, int type, int collide = 0) + : partrenderer(texname, 3, type, collide), + verts(NULL), parts(NULL), maxparts(0), numparts(0), lastupdate(-1), rndmask(0), vbo(0) + { + if(type & PT_HFLIP) rndmask |= 0x01; + if(type & PT_VFLIP) rndmask |= 0x02; + if(type & PT_ROT) rndmask |= 0x1F<<2; + if(type & PT_RND4) rndmask |= 0x03<<5; + } + + void cleanup() + { + if(vbo) { glDeleteBuffers_(1, &vbo); vbo = 0; } + } + + void init(int n) + { + DELETEA(parts); + DELETEA(verts); + parts = new particle[n]; + verts = new partvert[n*4]; + maxparts = n; + numparts = 0; + lastupdate = -1; + } + + void reset() + { + numparts = 0; + lastupdate = -1; + } + + void resettracked(physent *owner) + { + if(!(type&PT_TRACK)) return; + loopi(numparts) + { + particle *p = parts+i; + if(!owner || (p->owner == owner)) p->fade = -1; + } + lastupdate = -1; + } + + int count() + { + return numparts; + } + + bool haswork() + { + return (numparts > 0); + } + + particle *addpart(const vec &o, const vec &d, int fade, int color, float size, int gravity) + { + particle *p = parts + (numparts < maxparts ? numparts++ : rnd(maxparts)); //next free slot, or kill a random kitten + p->o = o; + p->d = d; + p->gravity = gravity; + p->fade = fade; + p->millis = lastmillis + emitoffset; + p->color = bvec(color>>16, (color>>8)&0xFF, color&0xFF); + p->size = size; + p->owner = NULL; + p->flags = 0x80 | (rndmask ? rnd(0x80) & rndmask : 0); + lastupdate = -1; + return p; + } + + void seedemitter(particleemitter &pe, const vec &o, const vec &d, int fade, float size, int gravity) + { + pe.maxfade = max(pe.maxfade, fade); + size *= SQRT2; + pe.extendbb(o, size); + + seedpos(pe, o, d, fade, size, gravity); + if(!gravity) return; + + vec end(o); + float t = fade; + end.add(vec(d).mul(t/5000.0f)); + end.z -= t*t/(2.0f * 5000.0f * gravity); + pe.extendbb(end, size); + + float tpeak = d.z*gravity; + if(tpeak > 0 && tpeak < fade) pe.extendbb(o.z + 1.5f*d.z*tpeak/5000.0f, size); + } + + void genverts(particle *p, partvert *vs, bool regen) + { + vec o, d; + int blend, ts; + + calc(p, blend, ts, o, d); + if(blend <= 1 || p->fade <= 5) p->fade = -1; //mark to remove on next pass (i.e. after render) + + modifyblend(o, blend); + + if(regen) + { + p->flags &= ~0x80; + + #define SETTEXCOORDS(u1c, u2c, v1c, v2c, body) \ + { \ + float u1 = u1c, u2 = u2c, v1 = v1c, v2 = v2c; \ + body; \ + vs[0].tc = vec2(u1, v1); \ + vs[1].tc = vec2(u2, v1); \ + vs[2].tc = vec2(u2, v2); \ + vs[3].tc = vec2(u1, v2); \ + } + if(type&PT_RND4) + { + float tx = 0.5f*((p->flags>>5)&1), ty = 0.5f*((p->flags>>6)&1); + SETTEXCOORDS(tx, tx + 0.5f, ty, ty + 0.5f, + { + if(p->flags&0x01) swap(u1, u2); + if(p->flags&0x02) swap(v1, v2); + }); + } + else if(type&PT_ICON) + { + float tx = 0.25f*(p->flags&3), ty = 0.25f*((p->flags>>2)&3); + SETTEXCOORDS(tx, tx + 0.25f, ty, ty + 0.25f, {}); + } + else SETTEXCOORDS(0, 1, 0, 1, {}); + + #define SETCOLOR(r, g, b, a) \ + do { \ + bvec4 col(r, g, b, a); \ + loopi(4) vs[i].color = col; \ + } while(0) + #define SETMODCOLOR SETCOLOR((p->color.r*blend)>>8, (p->color.g*blend)>>8, (p->color.b*blend)>>8, 255) + if(type&PT_MOD) SETMODCOLOR; + else SETCOLOR(p->color.r, p->color.g, p->color.b, blend); + } + else if(type&PT_MOD) SETMODCOLOR; + else loopi(4) vs[i].color.a = blend; + + if(type&PT_ROT) genrotpos(o, d, p->size, ts, p->gravity, vs, (p->flags>>2)&0x1F); + else genpos(o, d, p->size, ts, p->gravity, vs); + } + + void genverts() + { + loopi(numparts) + { + particle *p = &parts[i]; + partvert *vs = &verts[i*4]; + if(p->fade < 0) + { + do + { + --numparts; + if(numparts <= i) return; + } + while(parts[numparts].fade < 0); + *p = parts[numparts]; + genverts(p, vs, true); + } + else genverts(p, vs, (p->flags&0x80)!=0); + } + } + + void update() + { + if(lastmillis == lastupdate && vbo) return; + lastupdate = lastmillis; + + genverts(); + + if(!vbo) glGenBuffers_(1, &vbo); + gle::bindvbo(vbo); + glBufferData_(GL_ARRAY_BUFFER, maxparts*4*sizeof(partvert), NULL, GL_STREAM_DRAW); + glBufferSubData_(GL_ARRAY_BUFFER, 0, numparts*4*sizeof(partvert), verts); + gle::clearvbo(); + } + + void render() + { + if(!tex) tex = textureload(texname, texclamp); + glBindTexture(GL_TEXTURE_2D, tex->id); + + gle::bindvbo(vbo); + const partvert *ptr = 0; + gle::vertexpointer(sizeof(partvert), ptr->pos.v); + gle::texcoord0pointer(sizeof(partvert), ptr->tc.v); + gle::colorpointer(sizeof(partvert), ptr->color.v); + gle::enablevertex(); + gle::enabletexcoord0(); + gle::enablecolor(); + gle::enablequads(); + + gle::drawquads(0, numparts); + + gle::disablequads(); + gle::disablevertex(); + gle::disabletexcoord0(); + gle::disablecolor(); + gle::clearvbo(); + } +}; +typedef varenderer quadrenderer; +typedef varenderer taperenderer; +typedef varenderer trailrenderer; + +#include "depthfx.h" +#include "explosion.h" +#include "lensflare.h" +#include "lightning.h" + +struct softquadrenderer : quadrenderer +{ + softquadrenderer(const char *texname, int type, int collide = 0) + : quadrenderer(texname, type|PT_SOFT, collide) + { + } + + int adddepthfx(vec &bbmin, vec &bbmax) + { + if(!depthfxtex.highprecision() && !depthfxtex.emulatehighprecision()) return 0; + int numsoft = 0; + loopi(numparts) + { + particle &p = parts[i]; + float radius = p.size*SQRT2; + vec o, d; + int blend, ts; + calc(&p, blend, ts, o, d, false); + if(!isfoggedsphere(radius, p.o) && (depthfxscissor!=2 || depthfxtex.addscissorbox(p.o, radius))) + { + numsoft++; + loopk(3) + { + bbmin[k] = min(bbmin[k], o[k] - radius); + bbmax[k] = max(bbmax[k], o[k] + radius); + } + } + } + return numsoft; + } +}; + +static partrenderer *parts[] = +{ + new quadrenderer("packages/particles/blood.png", PT_PART|PT_FLIP|PT_MOD|PT_RND4, DECAL_BLOOD), // blood spats (note: rgb is inverted) + new trailrenderer("packages/particles/base.png", PT_TRAIL|PT_LERP), // water, entity + new quadrenderer("packages/particles/smoke.png", PT_PART|PT_FLIP|PT_LERP), // smoke + new quadrenderer("packages/particles/steam.png", PT_PART|PT_FLIP), // steam + new quadrenderer("packages/particles/flames.png", PT_PART|PT_HFLIP|PT_RND4|PT_GLARE), // flame on - no flipping please, they have orientation + new quadrenderer("packages/particles/ball1.png", PT_PART|PT_FEW|PT_GLARE), // fireball1 + new quadrenderer("packages/particles/ball2.png", PT_PART|PT_FEW|PT_GLARE), // fireball2 + new quadrenderer("packages/particles/ball3.png", PT_PART|PT_FEW|PT_GLARE), // fireball3 + new taperenderer("packages/particles/flare.jpg", PT_TAPE|PT_GLARE), // streak + &lightnings, // lightning + &fireballs, // explosion fireball + &bluefireballs, // bluish explosion fireball + new quadrenderer("packages/particles/spark.png", PT_PART|PT_FLIP|PT_GLARE), // sparks + new quadrenderer("packages/particles/base.png", PT_PART|PT_FLIP|PT_GLARE), // edit mode entities + new quadrenderer("packages/particles/snow.png", PT_PART|PT_FLIP|PT_RND4, -1), // colliding snow + new quadrenderer("packages/particles/muzzleflash1.jpg", PT_PART|PT_FEW|PT_FLIP|PT_GLARE|PT_TRACK), // muzzle flash + new quadrenderer("packages/particles/muzzleflash2.jpg", PT_PART|PT_FEW|PT_FLIP|PT_GLARE|PT_TRACK), // muzzle flash + new quadrenderer("packages/particles/muzzleflash3.jpg", PT_PART|PT_FEW|PT_FLIP|PT_GLARE|PT_TRACK), // muzzle flash + new quadrenderer("packages/hud/items.png", PT_PART|PT_FEW|PT_ICON), // hud icon + new quadrenderer("packages/hud/items.png", PT_PART|PT_FEW|PT_ICON), // grey hud icon + &texts, // text + &texticons, // text icons + &meters, // meter + &metervs, // meter vs. + &flares // lens flares - must be done last +}; + +void finddepthfxranges() +{ + depthfxmin = vec(1e16f, 1e16f, 1e16f); + depthfxmax = vec(0, 0, 0); + numdepthfxranges = fireballs.finddepthfxranges(depthfxowners, depthfxranges, 0, MAXDFXRANGES, depthfxmin, depthfxmax); + numdepthfxranges = bluefireballs.finddepthfxranges(depthfxowners, depthfxranges, numdepthfxranges, MAXDFXRANGES, depthfxmin, depthfxmax); + loopk(3) + { + depthfxmin[k] -= depthfxmargin; + depthfxmax[k] += depthfxmargin; + } + if(depthfxparts) + { + loopi(sizeof(parts)/sizeof(parts[0])) + { + partrenderer *p = parts[i]; + if(p->type&PT_SOFT && p->adddepthfx(depthfxmin, depthfxmax)) + { + if(!numdepthfxranges) + { + numdepthfxranges = 1; + depthfxowners[0] = NULL; + depthfxranges[0] = 0; + } + } + } + } + if(depthfxscissor<2 && numdepthfxranges>0) depthfxtex.addscissorbox(depthfxmin, depthfxmax); +} + +VARFP(maxparticles, 10, 4000, 40000, initparticles()); +VARFP(fewparticles, 10, 100, 40000, initparticles()); + +void initparticles() +{ + if(!particleshader) particleshader = lookupshaderbyname("particle"); + if(!particlenotextureshader) particlenotextureshader = lookupshaderbyname("particlenotexture"); + loopi(sizeof(parts)/sizeof(parts[0])) parts[i]->init(parts[i]->type&PT_FEW ? min(fewparticles, maxparticles) : maxparticles); +} + +void clearparticles() +{ + loopi(sizeof(parts)/sizeof(parts[0])) parts[i]->reset(); + clearparticleemitters(); +} + +void cleanupparticles() +{ + loopi(sizeof(parts)/sizeof(parts[0])) parts[i]->cleanup(); +} + +void removetrackedparticles(physent *owner) +{ + loopi(sizeof(parts)/sizeof(parts[0])) parts[i]->resettracked(owner); +} + +VARP(particleglare, 0, 2, 100); + +VARN(debugparticles, dbgparts, 0, 0, 1); + +void debugparticles() +{ + if(!dbgparts) return; + int n = sizeof(parts)/sizeof(parts[0]); + pushhudmatrix(); + hudmatrix.ortho(0, FONTH*n*2*screenw/float(screenh), FONTH*n*2, 0, -1, 1); //squeeze into top-left corner + flushhudmatrix(); + hudshader->set(); + loopi(n) draw_text(parts[i]->info, FONTH, (i+n/2)*FONTH); + pophudmatrix(); +} + +void renderparticles(bool mainpass) +{ + canstep = mainpass; + //want to debug BEFORE the lastpass render (that would delete particles) + if(dbgparts && mainpass) loopi(sizeof(parts)/sizeof(parts[0])) parts[i]->debuginfo(); + + if(glaring && !particleglare) return; + + loopi(sizeof(parts)/sizeof(parts[0])) + { + if(glaring && !(parts[i]->type&PT_GLARE)) continue; + parts[i]->update(); + } + + bool rendered = false; + uint lastflags = PT_LERP|PT_SHADER, + flagmask = PT_LERP|PT_MOD|PT_SHADER|PT_NOTEX; + + if(binddepthfxtex()) flagmask |= PT_SOFT; + + loopi(sizeof(parts)/sizeof(parts[0])) + { + partrenderer *p = parts[i]; + if(glaring && !(p->type&PT_GLARE)) continue; + if(!p->haswork()) continue; + + if(!rendered) + { + rendered = true; + glDepthMask(GL_FALSE); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + if(glaring) GLOBALPARAMF(colorscale, particleglare, particleglare, particleglare, 1); + else GLOBALPARAMF(colorscale, 1, 1, 1, 1); + } + + uint flags = p->type & flagmask, changedbits = (flags ^ lastflags); + if(changedbits) + { + if(changedbits&PT_LERP) + { + if(flags&PT_LERP) resetfogcolor(); + else zerofogcolor(); + } + if(changedbits&(PT_LERP|PT_MOD)) + { + if(flags&PT_LERP) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + else if(flags&PT_MOD) glBlendFunc(GL_ZERO, GL_ONE_MINUS_SRC_COLOR); + else glBlendFunc(GL_SRC_ALPHA, GL_ONE); + } + if(!(flags&PT_SHADER)) + { + if(changedbits&(PT_SOFT|PT_SHADER|PT_NOTEX|PT_LERP)) + { + if(flags&PT_SOFT) + { + if(!depthfxtex.highprecision()) SETSHADER(particlesoft8); + else SETSHADER(particlesoft); + + binddepthfxparams(depthfxpartblend); + } + else if(flags&PT_NOTEX) particlenotextureshader->set(); + else particleshader->set(); + } + } + lastflags = flags; + } + p->render(); + } + + if(rendered) + { + if(lastflags&(PT_LERP|PT_MOD)) glBlendFunc(GL_SRC_ALPHA, GL_ONE); + if(!(lastflags&PT_LERP)) resetfogcolor(); + glDisable(GL_BLEND); + glDepthMask(GL_TRUE); + } +} + +static int addedparticles = 0; + +static inline particle *newparticle(const vec &o, const vec &d, int fade, int type, int color, float size, int gravity = 0) +{ + static particle dummy; + if(seedemitter) + { + parts[type]->seedemitter(*seedemitter, o, d, fade, size, gravity); + return &dummy; + } + if(fade + emitoffset < 0) return &dummy; + addedparticles++; + return parts[type]->addpart(o, d, fade, color, size, gravity); +} + +VARP(maxparticledistance, 256, 1024, 4096); + +static void splash(int type, int color, int radius, int num, int fade, const vec &p, float size, int gravity) +{ + if(camera1->o.dist(p) > maxparticledistance && !seedemitter) return; + float collidez = parts[type]->collide ? p.z - raycube(p, vec(0, 0, -1), COLLIDERADIUS, RAY_CLIPMAT) + (parts[type]->collide >= 0 ? COLLIDEERROR : 0) : -1; + int fmin = 1; + int fmax = fade*3; + loopi(num) + { + int x, y, z; + do + { + x = rnd(radius*2)-radius; + y = rnd(radius*2)-radius; + z = rnd(radius*2)-radius; + } + while(x*x+y*y+z*z>radius*radius); + vec tmp = vec((float)x, (float)y, (float)z); + int f = (num < 10) ? (fmin + rnd(fmax)) : (fmax - (i*(fmax-fmin))/(num-1)); //help deallocater by using fade distribution rather than random + newparticle(p, tmp, f, type, color, size, gravity)->val = collidez; + } +} + +static void regularsplash(int type, int color, int radius, int num, int fade, const vec &p, float size, int gravity, int delay = 0) +{ + if(!canemitparticles() || (delay > 0 && rnd(delay) != 0)) return; + splash(type, color, radius, num, fade, p, size, gravity); +} + +bool canaddparticles() +{ + return !renderedgame && !shadowmapping && !minimized; +} + +void regular_particle_splash(int type, int num, int fade, const vec &p, int color, float size, int radius, int gravity, int delay) +{ + if(!canaddparticles()) return; + regularsplash(type, color, radius, num, fade, p, size, gravity, delay); +} + +void particle_splash(int type, int num, int fade, const vec &p, int color, float size, int radius, int gravity) +{ + if(!canaddparticles()) return; + splash(type, color, radius, num, fade, p, size, gravity); +} + +VARP(maxtrail, 1, 500, 10000); + +void particle_trail(int type, int fade, const vec &s, const vec &e, int color, float size, int gravity) +{ + if(!canaddparticles()) return; + vec v; + float d = e.dist(s, v); + int steps = clamp(int(d*2), 1, maxtrail); + v.div(steps); + vec p = s; + loopi(steps) + { + p.add(v); + vec tmp = vec(float(rnd(11)-5), float(rnd(11)-5), float(rnd(11)-5)); + newparticle(p, tmp, rnd(fade)+fade, type, color, size, gravity); + } +} + +VARP(particletext, 0, 1, 1); +VARP(maxparticletextdistance, 0, 128, 10000); + +void particle_text(const vec &s, const char *t, int type, int fade, int color, float size, int gravity, int icons) +{ + if(!canaddparticles()) return; + if(!particletext || camera1->o.dist(s) > maxparticletextdistance) return; + particle *p = newparticle(s, vec(0, 0, 1), fade, type, color, size, gravity); + p->text = t; + p->flags = icons<<1; +} + +void particle_textcopy(const vec &s, const char *t, int type, int fade, int color, float size, int gravity) +{ + if(!canaddparticles()) return; + if(!particletext || camera1->o.dist(s) > maxparticletextdistance) return; + particle *p = newparticle(s, vec(0, 0, 1), fade, type, color, size, gravity); + p->text = newstring(t); + p->flags = 1; +} + +void particle_texticon(const vec &s, int ix, int iy, float offset, int type, int fade, int color, float size, int gravity) +{ + if(!canaddparticles()) return; + if(!particletext || camera1->o.dist(s) > maxparticletextdistance) return; + particle *p = newparticle(s, vec(0, 0, 1), fade, type, color, size, gravity); + p->flags |= ix | (iy<<2); + p->val = offset; +} + +void particle_icon(const vec &s, int ix, int iy, int type, int fade, int color, float size, int gravity) +{ + if(!canaddparticles()) return; + particle *p = newparticle(s, vec(0, 0, 1), fade, type, color, size, gravity); + p->flags |= ix | (iy<<2); +} + +void particle_meter(const vec &s, float val, int type, int fade, int color, int color2, float size) +{ + if(!canaddparticles()) return; + particle *p = newparticle(s, vec(0, 0, 1), fade, type, color, size); + p->color2[0] = color2>>16; + p->color2[1] = (color2>>8)&0xFF; + p->color2[2] = color2&0xFF; + p->progress = clamp(int(val*100), 0, 100); +} + +void particle_flare(const vec &p, const vec &dest, int fade, int type, int color, float size, physent *owner) +{ + if(!canaddparticles()) return; + newparticle(p, dest, fade, type, color, size)->owner = owner; +} + +void particle_fireball(const vec &dest, float maxsize, int type, int fade, int color, float size) +{ + if(!canaddparticles()) return; + float growth = maxsize - size; + if(fade < 0) fade = int(growth*20); + newparticle(dest, vec(0, 0, 1), fade, type, color, size)->val = growth; +} + +//dir = 0..6 where 0=up +static inline vec offsetvec(vec o, int dir, int dist) +{ + vec v = vec(o); + v[(2+dir)%3] += (dir>2)?(-dist):dist; + return v; +} + +//converts a 16bit color to 24bit +static inline int colorfromattr(int attr) +{ + return (((attr&0xF)<<4) | ((attr&0xF0)<<8) | ((attr&0xF00)<<12)) + 0x0F0F0F; +} + +/* Experiments in shapes... + * dir: (where dir%3 is similar to offsetvec with 0=up) + * 0..2 circle + * 3.. 5 cylinder shell + * 6..11 cone shell + * 12..14 plane volume + * 15..20 line volume, i.e. wall + * 21 sphere + * 24..26 flat plane + * +32 to inverse direction + */ +void regularshape(int type, int radius, int color, int dir, int num, int fade, const vec &p, float size, int gravity, int vel = 200) +{ + if(!canemitparticles()) return; + + int basetype = parts[type]->type&0xFF; + bool flare = (basetype == PT_TAPE) || (basetype == PT_LIGHTNING), + inv = (dir&0x20)!=0, taper = (dir&0x40)!=0 && !seedemitter; + dir &= 0x1F; + loopi(num) + { + vec to, from; + if(dir < 12) + { + const vec2 &sc = sincos360[rnd(360)]; + to[dir%3] = sc.y*radius; + to[(dir+1)%3] = sc.x*radius; + to[(dir+2)%3] = 0.0; + to.add(p); + if(dir < 3) //circle + from = p; + else if(dir < 6) //cylinder + { + from = to; + to[(dir+2)%3] += radius; + from[(dir+2)%3] -= radius; + } + else //cone + { + from = p; + to[(dir+2)%3] += (dir < 9)?radius:(-radius); + } + } + else if(dir < 15) //plane + { + to[dir%3] = float(rnd(radius<<4)-(radius<<3))/8.0; + to[(dir+1)%3] = float(rnd(radius<<4)-(radius<<3))/8.0; + to[(dir+2)%3] = radius; + to.add(p); + from = to; + from[(dir+2)%3] -= 2*radius; + } + else if(dir < 21) //line + { + if(dir < 18) + { + to[dir%3] = float(rnd(radius<<4)-(radius<<3))/8.0; + to[(dir+1)%3] = 0.0; + } + else + { + to[dir%3] = 0.0; + to[(dir+1)%3] = float(rnd(radius<<4)-(radius<<3))/8.0; + } + to[(dir+2)%3] = 0.0; + to.add(p); + from = to; + to[(dir+2)%3] += radius; + } + else if(dir < 24) //sphere + { + to = vec(2*M_PI*float(rnd(1000))/1000.0, M_PI*float(rnd(1000)-500)/1000.0).mul(radius); + to.add(p); + from = p; + } + else if(dir < 27) // flat plane + { + to[dir%3] = float(rndscale(2*radius)-radius); + to[(dir+1)%3] = float(rndscale(2*radius)-radius); + to[(dir+2)%3] = 0.0; + to.add(p); + from = to; + } + else from = to = p; + + if(inv) swap(from, to); + + if(taper) + { + float dist = clamp(from.dist2(camera1->o)/maxparticledistance, 0.0f, 1.0f); + if(dist > 0.2f) + { + dist = 1 - (dist - 0.2f)/0.8f; + if(rnd(0x10000) > dist*dist*0xFFFF) continue; + } + } + + if(flare) + newparticle(from, to, rnd(fade*3)+1, type, color, size, gravity); + else + { + vec d = vec(to).sub(from).rescale(vel); //velocity + particle *n = newparticle(from, d, rnd(fade*3)+1, type, color, size, gravity); + if(parts[type]->collide) + n->val = from.z - raycube(from, vec(0, 0, -1), parts[type]->collide >= 0 ? COLLIDERADIUS : max(from.z, 0.0f), RAY_CLIPMAT) + (parts[type]->collide >= 0 ? COLLIDEERROR : 0); + } + } +} + +static void regularflame(int type, const vec &p, float radius, float height, int color, int density = 3, float scale = 2.0f, float speed = 200.0f, float fade = 600.0f, int gravity = -15) +{ + if(!canemitparticles()) return; + + float size = scale * min(radius, height); + vec v(0, 0, min(1.0f, height)*speed); + loopi(density) + { + vec s = p; + s.x += rndscale(radius*2.0f)-radius; + s.y += rndscale(radius*2.0f)-radius; + newparticle(s, v, rnd(max(int(fade*height), 1))+1, type, color, size, gravity); + } +} + +void regular_particle_flame(int type, const vec &p, float radius, float height, int color, int density, float scale, float speed, float fade, int gravity) +{ + if(!canaddparticles()) return; + regularflame(type, p, radius, height, color, density, scale, speed, fade, gravity); +} + +static void makeparticles(entity &e) +{ + switch(e.attr1) + { + case 0: //fire and smoke - - 0 values default to compat for old maps + { + //regularsplash(PART_FIREBALL1, 0xFFC8C8, 150, 1, 40, e.o, 4.8f); + //regularsplash(PART_SMOKE, 0x897661, 50, 1, 200, vec(e.o.x, e.o.y, e.o.z+3.0f), 2.4f, -20, 3); + float radius = e.attr2 ? float(e.attr2)/100.0f : 1.5f, + height = e.attr3 ? float(e.attr3)/100.0f : radius/3; + regularflame(PART_FLAME, e.o, radius, height, e.attr4 ? colorfromattr(e.attr4) : 0x903020, 3, 2.0f); + regularflame(PART_SMOKE, vec(e.o.x, e.o.y, e.o.z + 4.0f*min(radius, height)), radius, height, 0x303020, 1, 4.0f, 100.0f, 2000.0f, -20); + break; + } + case 1: //steam vent - + regularsplash(PART_STEAM, 0x897661, 50, 1, 200, offsetvec(e.o, e.attr2, rnd(10)), 2.4f, -20); + break; + case 2: //water fountain - + { + int color; + if(e.attr3 > 0) color = colorfromattr(e.attr3); + else + { + int mat = MAT_WATER + clamp(-e.attr3, 0, 3); + const bvec &wfcol = getwaterfallcolor(mat); + color = (int(wfcol[0])<<16) | (int(wfcol[1])<<8) | int(wfcol[2]); + if(!color) + { + const bvec &wcol = getwatercolor(mat); + color = (int(wcol[0])<<16) | (int(wcol[1])<<8) | int(wcol[2]); + } + } + regularsplash(PART_WATER, color, 150, 4, 200, offsetvec(e.o, e.attr2, rnd(10)), 0.6f, 2); + break; + } + case 3: //fire ball - + newparticle(e.o, vec(0, 0, 1), 1, PART_EXPLOSION, colorfromattr(e.attr3), 4.0f)->val = 1+e.attr2; + break; + case 4: //tape - + case 7: //lightning + case 9: //steam + case 10: //water + case 13: //snow + { + static const int typemap[] = { PART_STREAK, -1, -1, PART_LIGHTNING, -1, PART_STEAM, PART_WATER, -1, -1, PART_SNOW }; + static const float sizemap[] = { 0.28f, 0.0f, 0.0f, 1.0f, 0.0f, 2.4f, 0.60f, 0.0f, 0.0f, 0.5f }; + static const int gravmap[] = { 0, 0, 0, 0, 0, -20, 2, 0, 0, 20 }; + int type = typemap[e.attr1-4]; + float size = sizemap[e.attr1-4]; + int gravity = gravmap[e.attr1-4]; + if(e.attr2 >= 256) regularshape(type, max(1+e.attr3, 1), colorfromattr(e.attr4), e.attr2-256, 5, e.attr5 > 0 ? min(int(e.attr5), 10000) : 200, e.o, size, gravity); + else newparticle(e.o, offsetvec(e.o, e.attr2, max(1+e.attr3, 0)), 1, type, colorfromattr(e.attr4), size, gravity); + break; + } + case 5: //meter, metervs - + case 6: + { + particle *p = newparticle(e.o, vec(0, 0, 1), 1, e.attr1==5 ? PART_METER : PART_METER_VS, colorfromattr(e.attr3), 2.0f); + int color2 = colorfromattr(e.attr4); + p->color2[0] = color2>>16; + p->color2[1] = (color2>>8)&0xFF; + p->color2[2] = color2&0xFF; + p->progress = clamp(int(e.attr2), 0, 100); + break; + } + case 11: // flame - radius=100, height=100 is the classic size + regularflame(PART_FLAME, e.o, float(e.attr2)/100.0f, float(e.attr3)/100.0f, colorfromattr(e.attr4), 3, 2.0f); + break; + case 12: // smoke plume + regularflame(PART_SMOKE, e.o, float(e.attr2)/100.0f, float(e.attr3)/100.0f, colorfromattr(e.attr4), 1, 4.0f, 100.0f, 2000.0f, -20); + break; + case 32: //lens flares - plain/sparkle/sun/sparklesun + case 33: + case 34: + case 35: + flares.addflare(e.o, e.attr2, e.attr3, e.attr4, (e.attr1&0x02)!=0, (e.attr1&0x01)!=0); + break; + default: + if(!editmode) + { + defformatstring(ds, "particles %d?", e.attr1); + particle_textcopy(e.o, ds, PART_TEXT, 1, 0x6496FF, 2.0f); + } + break; + } +} + +bool printparticles(extentity &e, char *buf, int len) +{ + switch(e.attr1) + { + case 0: case 4: case 7: case 8: case 9: case 10: case 11: case 12: case 13: + nformatstring(buf, len, "%s %d %d %d 0x%.3hX %d", entities::entname(e.type), e.attr1, e.attr2, e.attr3, e.attr4, e.attr5); + return true; + case 3: + nformatstring(buf, len, "%s %d %d 0x%.3hX %d %d", entities::entname(e.type), e.attr1, e.attr2, e.attr3, e.attr4, e.attr5); + return true; + case 5: case 6: + nformatstring(buf, len, "%s %d %d 0x%.3hX 0x%.3hX %d", entities::entname(e.type), e.attr1, e.attr2, e.attr3, e.attr4, e.attr5); + return true; + } + return false; +} + +void seedparticles() +{ + renderprogress(0, "seeding particles"); + addparticleemitters(); + canemit = true; + loopv(emitters) + { + particleemitter &pe = emitters[i]; + extentity &e = *pe.ent; + seedemitter = &pe; + for(int millis = 0; millis < seedmillis; millis += min(emitmillis, seedmillis/10)) + makeparticles(e); + seedemitter = NULL; + pe.lastemit = -seedmillis; + pe.finalize(); + } +} + +void updateparticles() +{ + if(regenemitters) addparticleemitters(); + + if(minimized) { canemit = false; return; } + + if(lastmillis - lastemitframe >= emitmillis) + { + canemit = true; + lastemitframe = lastmillis - (lastmillis%emitmillis); + } + else canemit = false; + + flares.makelightflares(); + + if(!editmode || showparticles) + { + int emitted = 0, replayed = 0; + addedparticles = 0; + loopv(emitters) + { + particleemitter &pe = emitters[i]; + extentity &e = *pe.ent; + if(e.o.dist(camera1->o) > maxparticledistance) { pe.lastemit = lastmillis; continue; } + if(cullparticles && pe.maxfade >= 0) + { + if(isfoggedsphere(pe.radius, pe.center)) { pe.lastcull = lastmillis; continue; } + if(pvsoccluded(pe.cullmin, pe.cullmax)) { pe.lastcull = lastmillis; continue; } + } + makeparticles(e); + emitted++; + if(replayparticles && pe.maxfade > 5 && pe.lastcull > pe.lastemit) + { + for(emitoffset = max(pe.lastemit + emitmillis - lastmillis, -pe.maxfade); emitoffset < 0; emitoffset += emitmillis) + { + makeparticles(e); + replayed++; + } + emitoffset = 0; + } + pe.lastemit = lastmillis; + } + if(dbgpcull && (canemit || replayed) && addedparticles) conoutf(CON_DEBUG, "%d emitters, %d particles", emitted, addedparticles); + } + if(editmode) // show sparkly thingies for map entities in edit mode + { + const vector &ents = entities::getents(); + // note: order matters in this case as particles of the same type are drawn in the reverse order that they are added + loopv(entgroup) + { + entity &e = *ents[entgroup[i]]; + particle_textcopy(e.o, entname(e), PART_TEXT, 1, 0xFF4B19, 2.0f); + } + loopv(ents) + { + entity &e = *ents[i]; + if(e.type==ET_EMPTY) continue; + particle_textcopy(e.o, entname(e), PART_TEXT, 1, 0x1EC850, 2.0f); + regular_particle_splash(PART_EDIT, 2, 40, e.o, 0x3232FF, 0.32f*particlesize/100.0f); + } + } +} diff --git a/src/engine/rendersky.cpp b/src/engine/rendersky.cpp new file mode 100644 index 0000000..32ca947 --- /dev/null +++ b/src/engine/rendersky.cpp @@ -0,0 +1,774 @@ +#include "engine.h" + +Texture *sky[6] = { 0, 0, 0, 0, 0, 0 }, *clouds[6] = { 0, 0, 0, 0, 0, 0 }; + +void loadsky(const char *basename, Texture *texs[6]) +{ + const char *wildcard = strchr(basename, '*'); + loopi(6) + { + const char *side = cubemapsides[i].name; + string name; + copystring(name, makerelpath("packages", basename)); + if(wildcard) + { + char *chop = strchr(name, '*'); + if(chop) { *chop = '\0'; concatstring(name, side); concatstring(name, wildcard+1); } + texs[i] = textureload(name, 3, true, false); + } + else + { + defformatstring(ext, "_%s.jpg", side); + concatstring(name, ext); + if((texs[i] = textureload(name, 3, true, false))==notexture) + { + strcpy(name+strlen(name)-3, "png"); + texs[i] = textureload(name, 3, true, false); + } + } + if(texs[i]==notexture) conoutf(CON_ERROR, "could not load side %s of sky texture %s", side, basename); + } +} + +Texture *cloudoverlay = NULL; + +Texture *loadskyoverlay(const char *basename) +{ + const char *ext = strrchr(basename, '.'); + string name; + copystring(name, makerelpath("packages", basename)); + Texture *t = notexture; + if(ext) t = textureload(name, 0, true, false); + else + { + concatstring(name, ".jpg"); + if((t = textureload(name, 0, true, false)) == notexture) + { + strcpy(name+strlen(name)-3, "png"); + t = textureload(name, 0, true, false); + } + } + if(t==notexture) conoutf(CON_ERROR, "could not load sky overlay texture %s", basename); + return t; +} + +SVARFR(skybox, "", { if(skybox[0]) loadsky(skybox, sky); }); +HVARR(skyboxcolour, 0, 0xFFFFFF, 0xFFFFFF); +FVARR(spinsky, -720, 0, 720); +VARR(yawsky, 0, 0, 360); +SVARFR(cloudbox, "", { if(cloudbox[0]) loadsky(cloudbox, clouds); }); +HVARR(cloudboxcolour, 0, 0xFFFFFF, 0xFFFFFF); +FVARR(cloudboxalpha, 0, 1, 1); +FVARR(spinclouds, -720, 0, 720); +VARR(yawclouds, 0, 0, 360); +FVARR(cloudclip, 0, 0.5f, 1); +SVARFR(cloudlayer, "", { if(cloudlayer[0]) cloudoverlay = loadskyoverlay(cloudlayer); }); +FVARR(cloudoffsetx, 0, 0, 1); +FVARR(cloudoffsety, 0, 0, 1); +FVARR(cloudscrollx, -16, 0, 16); +FVARR(cloudscrolly, -16, 0, 16); +FVARR(cloudscale, 0.001, 1, 64); +FVARR(spincloudlayer, -720, 0, 720); +VARR(yawcloudlayer, 0, 0, 360); +FVARR(cloudheight, -1, 0.2f, 1); +FVARR(cloudfade, 0, 0.2f, 1); +FVARR(cloudalpha, 0, 1, 1); +VARR(cloudsubdiv, 4, 16, 64); +HVARR(cloudcolour, 0, 0xFFFFFF, 0xFFFFFF); + +void drawenvboxface(float s0, float t0, int x0, int y0, int z0, + float s1, float t1, int x1, int y1, int z1, + float s2, float t2, int x2, int y2, int z2, + float s3, float t3, int x3, int y3, int z3, + Texture *tex) +{ + glBindTexture(GL_TEXTURE_2D, (tex ? tex : notexture)->id); + gle::begin(GL_TRIANGLE_STRIP); + gle::attribf(x3, y3, z3); gle::attribf(s3, t3); + gle::attribf(x2, y2, z2); gle::attribf(s2, t2); + gle::attribf(x0, y0, z0); gle::attribf(s0, t0); + gle::attribf(x1, y1, z1); gle::attribf(s1, t1); + xtraverts += gle::end(); +} + +void drawenvbox(int w, float z1clip = 0.0f, float z2clip = 1.0f, int faces = 0x3F, Texture **sky = NULL) +{ + if(z1clip >= z2clip) return; + + float v1 = 1-z1clip, v2 = 1-z2clip; + int z1 = int(ceil(2*w*(z1clip-0.5f))), z2 = int(ceil(2*w*(z2clip-0.5f))); + + gle::defvertex(); + gle::deftexcoord0(); + + if(faces&0x01) + drawenvboxface(0.0f, v2, -w, -w, z2, + 1.0f, v2, -w, w, z2, + 1.0f, v1, -w, w, z1, + 0.0f, v1, -w, -w, z1, sky[0]); + + if(faces&0x02) + drawenvboxface(1.0f, v1, w, -w, z1, + 0.0f, v1, w, w, z1, + 0.0f, v2, w, w, z2, + 1.0f, v2, w, -w, z2, sky[1]); + + if(faces&0x04) + drawenvboxface(1.0f, v1, -w, -w, z1, + 0.0f, v1, w, -w, z1, + 0.0f, v2, w, -w, z2, + 1.0f, v2, -w, -w, z2, sky[2]); + + if(faces&0x08) + drawenvboxface(1.0f, v1, w, w, z1, + 0.0f, v1, -w, w, z1, + 0.0f, v2, -w, w, z2, + 1.0f, v2, w, w, z2, sky[3]); + + if(z1clip <= 0 && faces&0x10) + drawenvboxface(0.0f, 1.0f, -w, w, -w, + 0.0f, 0.0f, w, w, -w, + 1.0f, 0.0f, w, -w, -w, + 1.0f, 1.0f, -w, -w, -w, sky[4]); + + if(z2clip >= 1 && faces&0x20) + drawenvboxface(0.0f, 1.0f, w, w, w, + 0.0f, 0.0f, -w, w, w, + 1.0f, 0.0f, -w, -w, w, + 1.0f, 1.0f, w, -w, w, sky[5]); +} + +void drawenvoverlay(int w, Texture *overlay = NULL, float tx = 0, float ty = 0) +{ + float z = w*cloudheight, tsz = 0.5f*(1-cloudfade)/cloudscale, psz = w*(1-cloudfade); + glBindTexture(GL_TEXTURE_2D, overlay ? overlay->id : notexture->id); + vec color = vec::hexcolor(cloudcolour); + gle::color(color, cloudalpha); + gle::defvertex(); + gle::deftexcoord0(); + gle::begin(GL_TRIANGLE_FAN); + loopi(cloudsubdiv+1) + { + vec p(1, 1, 0); + p.rotate_around_z((-2.0f*M_PI*i)/cloudsubdiv); + gle::attribf(p.x*psz, p.y*psz, z); + gle::attribf(tx + p.x*tsz, ty + p.y*tsz); + } + xtraverts += gle::end(); + float tsz2 = 0.5f/cloudscale; + gle::defvertex(); + gle::deftexcoord0(); + gle::defcolor(4); + gle::begin(GL_TRIANGLE_STRIP); + loopi(cloudsubdiv+1) + { + vec p(1, 1, 0); + p.rotate_around_z((-2.0f*M_PI*i)/cloudsubdiv); + gle::attribf(p.x*psz, p.y*psz, z); + gle::attribf(tx + p.x*tsz, ty + p.y*tsz); + gle::attrib(color, cloudalpha); + gle::attribf(p.x*w, p.y*w, z); + gle::attribf(tx + p.x*tsz2, ty + p.y*tsz2); + gle::attrib(color, 0.0f); + } + xtraverts += gle::end(); +} + +FVARR(fogdomeheight, -1, -0.5f, 1); +FVARR(fogdomemin, 0, 0, 1); +FVARR(fogdomemax, 0, 0, 1); +VARR(fogdomecap, 0, 1, 1); +FVARR(fogdomeclip, 0, 1, 1); +bvec fogdomecolor(0, 0, 0); +HVARFR(fogdomecolour, 0, 0, 0xFFFFFF, +{ + fogdomecolor = bvec((fogdomecolour>>16)&0xFF, (fogdomecolour>>8)&0xFF, fogdomecolour&0xFF); +}); +VARR(fogdomeclouds, 0, 1, 1); + +namespace fogdome +{ + struct vert + { + vec pos; + bvec4 color; + + vert() {} + vert(const vec &pos, const bvec &fcolor, float alpha) : pos(pos), color(fcolor, uchar(alpha*255)) + { + } + vert(const vert &v0, const vert &v1) : pos(vec(v0.pos).add(v1.pos).normalize()), color(v0.color) + { + if(v0.pos.z != v1.pos.z) color.a += uchar((v1.color.a - v0.color.a) * (pos.z - v0.pos.z) / (v1.pos.z - v0.pos.z)); + } + } *verts = NULL; + GLushort *indices = NULL; + int numverts = 0, numindices = 0, capindices = 0; + GLuint vbuf = 0, ebuf = 0; + bvec lastcolor(0, 0, 0); + float lastminalpha = 0, lastmaxalpha = 0, lastcapsize = -1, lastclipz = 1; + + void subdivide(int depth, int face); + + void genface(int depth, int i1, int i2, int i3) + { + int face = numindices; numindices += 3; + indices[face] = i3; + indices[face+1] = i2; + indices[face+2] = i1; + subdivide(depth, face); + } + + void subdivide(int depth, int face) + { + if(depth-- <= 0) return; + int idx[6]; + loopi(3) idx[i] = indices[face+2-i]; + loopi(3) + { + int curvert = numverts++; + verts[curvert] = vert(verts[idx[i]], verts[idx[(i+1)%3]]); //push on to unit sphere + idx[3+i] = curvert; + indices[face+2-i] = curvert; + } + subdivide(depth, face); + loopi(3) genface(depth, idx[i], idx[3+i], idx[3+(i+2)%3]); + } + + int sortcap(GLushort x, GLushort y) + { + const vec &xv = verts[x].pos, &yv = verts[y].pos; + return xv.y < 0 ? yv.y >= 0 || xv.x < yv.x : yv.y >= 0 && xv.x > yv.x; + } + + void init(const bvec &color, float minalpha = 0.0f, float maxalpha = 1.0f, float capsize = -1, float clipz = 1, int hres = 16, int depth = 2) + { + const int tris = hres << (2*depth); + numverts = numindices = capindices = 0; + verts = new vert[tris+1 + (capsize >= 0 ? 1 : 0)]; + indices = new GLushort[(tris + (capsize >= 0 ? hres<= 1) + { + verts[numverts++] = vert(vec(0.0f, 0.0f, 1.0f), color, minalpha); //build initial 'hres' sided pyramid + loopi(hres) verts[numverts++] = vert(vec(sincos360[(360*i)/hres], 0.0f), color, maxalpha); + loopi(hres) genface(depth, 0, i+1, 1+(i+1)%hres); + } + else if(clipz <= 0) + { + loopi(hres<= 0) + { + GLushort *cap = &indices[numindices]; + int capverts = 0; + loopi(numverts) if(!verts[i].pos.z) cap[capverts++] = i; + verts[numverts++] = vert(vec(0.0f, 0.0f, -capsize), color, maxalpha); + quicksort(cap, capverts, sortcap); + loopi(capverts) + { + int n = capverts-1-i; + cap[n*3] = cap[n]; + cap[n*3+1] = cap[(n+1)%capverts]; + cap[n*3+2] = numverts-1; + capindices += 3; + } + } + + if(!vbuf) glGenBuffers_(1, &vbuf); + gle::bindvbo(vbuf); + glBufferData_(GL_ARRAY_BUFFER, numverts*sizeof(vert), verts, GL_STATIC_DRAW); + DELETEA(verts); + + if(!ebuf) glGenBuffers_(1, &ebuf); + gle::bindebo(ebuf); + glBufferData_(GL_ELEMENT_ARRAY_BUFFER, (numindices + capindices)*sizeof(GLushort), indices, GL_STATIC_DRAW); + DELETEA(indices); + } + + void cleanup() + { + numverts = numindices = 0; + if(vbuf) { glDeleteBuffers_(1, &vbuf); vbuf = 0; } + if(ebuf) { glDeleteBuffers_(1, &ebuf); ebuf = 0; } + } + + void draw() + { + float capsize = fogdomecap && fogdomeheight < 1 ? (1 + fogdomeheight) / (1 - fogdomeheight) : -1; + bvec color = fogdomecolour ? fogdomecolor : fogcolor; + if(!numverts || lastcolor != color || lastminalpha != fogdomemin || lastmaxalpha != fogdomemax || lastcapsize != capsize || lastclipz != fogdomeclip) + { + init(color, min(fogdomemin, fogdomemax), fogdomemax, capsize, fogdomeclip); + lastcolor = color; + lastminalpha = fogdomemin; + lastmaxalpha = fogdomemax; + lastcapsize = capsize; + lastclipz = fogdomeclip; + } + + gle::bindvbo(vbuf); + gle::bindebo(ebuf); + + gle::vertexpointer(sizeof(vert), &verts->pos); + gle::colorpointer(sizeof(vert), &verts->color); + gle::enablevertex(); + gle::enablecolor(); + + glDrawRangeElements_(GL_TRIANGLES, 0, numverts-1, numindices + fogdomecap*capindices, GL_UNSIGNED_SHORT, indices); + xtraverts += numverts; + glde++; + + gle::disablevertex(); + gle::disablecolor(); + + gle::clearvbo(); + gle::clearebo(); + } +} + +static void drawfogdome(int farplane) +{ + SETSHADER(skyfog); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + matrix4 skymatrix = cammatrix, skyprojmatrix; + skymatrix.settranslation(vec(cammatrix.c).mul(farplane*fogdomeheight*0.5f)); + skymatrix.scale(farplane/2, farplane/2, farplane*(0.5f - fogdomeheight*0.5f)); + skyprojmatrix.mul(projmatrix, skymatrix); + LOCALPARAM(skymatrix, skyprojmatrix); + + fogdome::draw(); + + glDisable(GL_BLEND); +} + +void cleanupsky() +{ + fogdome::cleanup(); +} + +extern int atmo; + +void preloadatmoshaders(bool force = false) +{ + static bool needatmo = false; + if(force) needatmo = true; + if(!atmo || !needatmo) return; + + useshaderbyname("atmosphere"); + useshaderbyname("atmosphereglare"); +} + +void setupsky() +{ + preloadatmoshaders(true); +} + +VARFR(atmo, 0, 0, 1, preloadatmoshaders()); +FVARR(atmoplanetsize, 1e-3f, 1, 1e3f); +FVARR(atmoheight, 1e-3f, 1, 1e3f); +FVARR(atmobright, 0, 1, 16); +bvec atmosunlightcolor(0, 0, 0); +HVARFR(atmosunlight, 0, 0, 0xFFFFFF, +{ + if(atmosunlight <= 255) atmosunlight |= (atmosunlight<<8) | (atmosunlight<<16); + atmosunlightcolor = bvec((atmosunlight>>16)&0xFF, (atmosunlight>>8)&0xFF, atmosunlight&0xFF); +}); +FVARR(atmosunlightscale, 0, 1, 16); +bvec atmosundiskcolor(0, 0, 0); +HVARFR(atmosundisk, 0, 0, 0xFFFFFF, +{ + if(atmosundisk <= 255) atmosundisk |= (atmosundisk<<8) | (atmosundisk<<16); + atmosundiskcolor = bvec((atmosundisk>>16)&0xFF, (atmosundisk>>8)&0xFF, atmosundisk&0xFF); +}); +FVARR(atmosundisksize, 0, 12, 90); +FVARR(atmosundiskcorona, 0, 0.4f, 1); +FVARR(atmosundiskbright, 0, 1, 16); +FVARR(atmohaze, 0, 0.1f, 16); +FVARR(atmodensity, 0, 1, 16); +FVARR(atmoozone, 0, 1, 16); +FVARR(atmoalpha, 0, 1, 1); + +static void drawatmosphere(int w, float z1clip = 0.0f, float z2clip = 1.0f, int faces = 0x3F) +{ + if(z1clip >= z2clip) return; + + if(glaring) SETSHADER(atmosphereglare); + else SETSHADER(atmosphere); + + matrix4 skymatrix = cammatrix, skyprojmatrix; + skymatrix.settranslation(0, 0, 0); + skyprojmatrix.mul(projmatrix, skymatrix); + LOCALPARAM(skymatrix, skyprojmatrix); + + // optical depth scales for 3 different shells of atmosphere - air, haze, ozone + const float earthradius = 6371e3f, earthairheight = 8.4e3f, earthhazeheight = 1.25e3f, earthozoneheight = 50e3f; + float planetradius = earthradius*atmoplanetsize; + vec atmoshells = vec(earthairheight, earthhazeheight, earthozoneheight).mul(atmoheight).add(planetradius).square().sub(planetradius*planetradius); + LOCALPARAM(opticaldepthparams, vec4(atmoshells, planetradius)); + + // Henyey-Greenstein approximation, 1/(4pi) * (1 - g^2)/(1 + g^2 - 2gcos)]^1.5 + // Hoffman-Preetham variation uses (1-g)^2 instead of 1-g^2 which avoids excessive glare + // clamp values near 0 angle to avoid spotlight artifact inside sundisk + float gm = max(0.95f - 0.2f*atmohaze, 0.65f), miescale = pow((1-gm)*(1-gm)/(4*M_PI), -2.0f/3.0f); + LOCALPARAMF(mieparams, miescale*(1 + gm*gm), miescale*-2*gm, 1 - (1 - cosf(0.5f*atmosundisksize*(1 - atmosundiskcorona)*RAD))); + + static const vec lambda(680e-9f, 550e-9f, 450e-9f), + k(0.686f, 0.678f, 0.666f), + ozone(3.426f, 8.298f, 0.356f); + vec betar = vec(lambda).square().square().recip().mul(1.241e-30f/M_LN2 * atmodensity), + betam = vec(lambda).recip().square().mul(k).mul(9.072e-17f/M_LN2 * atmohaze), + betao = vec(ozone).mul(1.5e-7f/M_LN2 * atmoozone); + LOCALPARAM(betarayleigh, betar); + LOCALPARAM(betamie, betam); + LOCALPARAM(betaozone, betao); + + // extinction in direction of sun + float sunoffset = sunlightdir.z*planetradius; + vec sundepth = vec(atmoshells).add(sunoffset*sunoffset).sqrt().sub(sunoffset); + vec sunweight = vec(betar).mul(sundepth.x).madd(betam, sundepth.y).madd(betao, sundepth.z - sundepth.x); + vec sunextinction = vec(sunweight).neg().exp2(); + vec suncolor = atmosunlight ? atmosunlightcolor.tocolor().mul(atmosunlightscale) : sunlightcolor.tocolor().mul(sunlightscale); + // assume sunlight color is gamma encoded, so decode to linear light, then apply extinction + vec sunscale = vec(suncolor).square().mul(atmobright * 16).mul(sunextinction); + float maxsunweight = max(max(sunweight.x, sunweight.y), sunweight.z); + if(maxsunweight > 127) sunweight.mul(127/maxsunweight); + sunweight.add(1e-4f); + LOCALPARAM(sunweight, sunweight); + LOCALPARAM(sunlight, vec4(sunscale, atmoalpha)); + LOCALPARAM(sundir, sunlightdir); + + // invert extinction at zenith to get an approximation of how bright the sun disk should be + vec zenithdepth = vec(atmoshells).add(planetradius*planetradius).sqrt().sub(planetradius); + vec zenithweight = vec(betar).mul(zenithdepth.x).madd(betam, zenithdepth.y).madd(betao, zenithdepth.z - zenithdepth.x); + vec zenithextinction = vec(zenithweight).sub(sunweight).exp2(); + vec diskcolor = (atmosundisk ? atmosundiskcolor.tocolor() : suncolor).square().mul(zenithextinction).mul(atmosundiskbright * (glaring ? 1 : 1.5f)).min(1); + LOCALPARAM(sundiskcolor, diskcolor); + + // convert from view cosine into mu^2 for limb darkening, where mu = sqrt(1 - sin^2) and sin^2 = 1 - cos^2, thus mu^2 = 1 - (1 - cos^2*scale) + // convert corona offset into scale for mu^2, where sin = (1-corona) and thus mu^2 = 1 - (1-corona^2) + float sundiskscale = sinf(0.5f*atmosundisksize*RAD); + float coronamu = 1 - (1-atmosundiskcorona)*(1-atmosundiskcorona); + if(sundiskscale > 0) LOCALPARAMF(sundiskparams, 1.0f/(sundiskscale*sundiskscale), 1.0f/max(coronamu, 1e-3f)); + else LOCALPARAMF(sundiskparams, 0, 0); + + float z1 = 2*w*(z1clip-0.5f), z2 = ceil(2*w*(z2clip-0.5f)); + + gle::defvertex(); + + if(glaring) + { + if(sundiskscale > 0 && sunlightdir.z*w + sundiskscale > z1 && sunlightdir.z*w - sundiskscale < z2) + { + gle::begin(GL_TRIANGLE_FAN); + vec spoke; + spoke.orthogonal(sunlightdir); + spoke.rescale(2*sundiskscale); + loopi(4) gle::attrib(vec(spoke).rotate(-2*M_PI*i/4.0f, sunlightdir).add(sunlightdir).mul(w)); + xtraverts += gle::end(); + } + return; + } + + gle::begin(GL_QUADS); + + if(faces&0x01) + { + gle::attribf(-w, -w, z1); + gle::attribf(-w, w, z1); + gle::attribf(-w, w, z2); + gle::attribf(-w, -w, z2); + } + + if(faces&0x02) + { + gle::attribf(w, -w, z2); + gle::attribf(w, w, z2); + gle::attribf(w, w, z1); + gle::attribf(w, -w, z1); + } + + if(faces&0x04) + { + gle::attribf(-w, -w, z2); + gle::attribf( w, -w, z2); + gle::attribf( w, -w, z1); + gle::attribf(-w, -w, z1); + } + + if(faces&0x08) + { + gle::attribf( w, w, z2); + gle::attribf(-w, w, z2); + gle::attribf(-w, w, z1); + gle::attribf( w, w, z1); + } + + if(z1clip <= 0 && faces&0x10) + { + gle::attribf(-w, -w, -w); + gle::attribf( w, -w, -w); + gle::attribf( w, w, -w); + gle::attribf(-w, w, -w); + } + + if(z2clip >= 1 && faces&0x20) + { + gle::attribf( w, -w, w); + gle::attribf(-w, -w, w); + gle::attribf(-w, w, w); + gle::attribf( w, w, w); + } + + xtraverts += gle::end(); +} + +VARP(sparklyfix, 0, 0, 1); +VAR(showsky, 0, 1, 1); +VAR(clipsky, 0, 1, 1); + +bool drawskylimits(bool explicitonly) +{ + nocolorshader->set(); + + glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + bool rendered = rendersky(explicitonly); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + + return rendered; +} + +void drawskyoutline() +{ + notextureshader->set(); + + glDepthMask(GL_FALSE); + extern int wireframe; + if(!wireframe) + { + enablepolygonoffset(GL_POLYGON_OFFSET_LINE); + glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + } + gle::colorf(0.5f, 0.0f, 0.5f); + rendersky(true); + if(!wireframe) + { + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + disablepolygonoffset(GL_POLYGON_OFFSET_LINE); + } + glDepthMask(GL_TRUE); +} + +VAR(clampsky, 0, 1, 1); + +static int yawskyfaces(int faces, int yaw, float spin = 0) +{ + if(spin || yaw%90) return faces&0x0F ? faces | 0x0F : faces; + static const int faceidxs[3][4] = + { + { 3, 2, 0, 1 }, + { 1, 0, 3, 2 }, + { 2, 3, 1, 0 } + }; + yaw /= 90; + if(yaw < 1 || yaw > 3) return faces; + const int *idxs = faceidxs[yaw - 1]; + return (faces & ~0x0F) | (((faces>>idxs[0])&1)<<0) | (((faces>>idxs[1])&1)<<1) | (((faces>>idxs[2])&1)<<2) | (((faces>>idxs[3])&1)<<3); +} + +void drawskybox(int farplane, bool limited, bool force) +{ + extern int renderedskyfaces, renderedskyclip; // , renderedsky, renderedexplicitsky; + bool alwaysrender = editmode || !insideworld(camera1->o) || reflecting || force, + explicitonly = false; + if(limited) + { + explicitonly = alwaysrender || !sparklyfix || refracting; + if(!drawskylimits(explicitonly) && !alwaysrender) return; + extern int ati_skybox_bug; + if(!alwaysrender && !renderedskyfaces && !ati_skybox_bug) explicitonly = false; + } + else if(!alwaysrender) + { + renderedskyfaces = 0; + renderedskyclip = INT_MAX; + for(vtxarray *va = visibleva; va; va = va->next) + { + if(va->occluded >= OCCLUDE_BB && va->skyfaces&0x80) continue; + renderedskyfaces |= va->skyfaces&0x3F; + if(!(va->skyfaces&0x1F) || camera1->o.z < va->skyclip) renderedskyclip = min(renderedskyclip, va->skyclip); + else renderedskyclip = 0; + } + if(!renderedskyfaces) return; + } + + if(alwaysrender) + { + renderedskyfaces = 0x3F; + renderedskyclip = 0; + } + + float skyclip = clipsky ? max(renderedskyclip-1, 0) : 0, topclip = 1; + if(reflectzo.z)/float(worldsize); + else if(reflectz>skyclip) skyclip = reflectz; + } + if(skyclip) skyclip = 0.5f + 0.5f*(skyclip-camera1->o.z)/float(worldsize); + + if(limited) + { + if(explicitonly) glDisable(GL_DEPTH_TEST); + else glDepthFunc(GL_GEQUAL); + } + else glDepthFunc(GL_LEQUAL); + + glDepthMask(GL_FALSE); + + if(clampsky) glDepthRange(1, 1); + + if(!atmo || (skybox[0] && atmoalpha < 1)) + { + if(glaring) SETSHADER(skyboxglare); + else SETSHADER(skybox); + + gle::color(vec::hexcolor(skyboxcolour)); + + matrix4 skymatrix = cammatrix, skyprojmatrix; + skymatrix.settranslation(0, 0, 0); + skymatrix.rotate_around_z((spinsky*lastmillis/1000.0f+yawsky)*-RAD); + skyprojmatrix.mul(projmatrix, skymatrix); + LOCALPARAM(skymatrix, skyprojmatrix); + + drawenvbox(farplane/2, skyclip, topclip, yawskyfaces(renderedskyfaces, yawsky, spinsky), sky); + } + + if(atmo) + { + if(atmoalpha < 1) + { + if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + } + + drawatmosphere(farplane/2, skyclip, topclip, renderedskyfaces); + + if(atmoalpha < 1) glDisable(GL_BLEND); + } + + if(!glaring) + { + if(fogdomemax && !fogdomeclouds) + { + if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE); + drawfogdome(farplane); + } + + if(cloudbox[0]) + { + SETSHADER(skybox); + + if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + gle::color(vec::hexcolor(cloudboxcolour), cloudboxalpha); + + matrix4 skymatrix = cammatrix, skyprojmatrix; + skymatrix.settranslation(0, 0, 0); + skymatrix.rotate_around_z((spinclouds*lastmillis/1000.0f+yawclouds)*-RAD); + skyprojmatrix.mul(projmatrix, skymatrix); + LOCALPARAM(skymatrix, skyprojmatrix); + + drawenvbox(farplane/2, skyclip ? skyclip : cloudclip, topclip, yawskyfaces(renderedskyfaces, yawclouds, spinclouds), clouds); + + glDisable(GL_BLEND); + } + + if(cloudlayer[0] && cloudheight && renderedskyfaces&(cloudheight < 0 ? 0x1F : 0x2F)) + { + SETSHADER(skybox); + + if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE); + + glDisable(GL_CULL_FACE); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + matrix4 skymatrix = cammatrix, skyprojmatrix; + skymatrix.settranslation(0, 0, 0); + skymatrix.rotate_around_z((spincloudlayer*lastmillis/1000.0f+yawcloudlayer)*-RAD); + skyprojmatrix.mul(projmatrix, skymatrix); + LOCALPARAM(skymatrix, skyprojmatrix); + + drawenvoverlay(farplane/2, cloudoverlay, cloudoffsetx + cloudscrollx * lastmillis/1000.0f, cloudoffsety + cloudscrolly * lastmillis/1000.0f); + + glDisable(GL_BLEND); + + glEnable(GL_CULL_FACE); + } + + if(fogdomemax && fogdomeclouds) + { + if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE); + drawfogdome(farplane); + } + } + + if(clampsky) glDepthRange(0, 1); + + glDepthMask(GL_TRUE); + + if(limited) + { + if(explicitonly) glEnable(GL_DEPTH_TEST); + else glDepthFunc(GL_LESS); + if(!reflecting && !refracting && !drawtex && editmode && showsky) drawskyoutline(); + } + else glDepthFunc(GL_LESS); +} + +VARNR(skytexture, useskytexture, 0, 1, 1); + +int explicitsky = 0; +double skyarea = 0; + +bool limitsky() +{ + return (explicitsky && (useskytexture || editmode)) || (sparklyfix && skyarea / (double(worldsize)*double(worldsize)*6) < 0.9); +} + +bool shouldrenderskyenvmap() +{ + return atmo != 0; +} + +bool shouldclearskyboxglare() +{ + return atmo && (!skybox[0] || atmoalpha >= 1); +} + diff --git a/src/engine/rendertarget.h b/src/engine/rendertarget.h new file mode 100644 index 0000000..3c93f18 --- /dev/null +++ b/src/engine/rendertarget.h @@ -0,0 +1,464 @@ +extern int rtsharefb, rtscissor, blurtile; + +struct rendertarget +{ + int texw, texh, vieww, viewh; + GLenum colorfmt, depthfmt; + GLuint rendertex, renderfb, renderdb, blurtex, blurfb, blurdb; + int blursize, blurysize; + float blursigma; + float blurweights[MAXBLURRADIUS+1], bluroffsets[MAXBLURRADIUS+1]; + float bluryweights[MAXBLURRADIUS+1], bluryoffsets[MAXBLURRADIUS+1]; + + float scissorx1, scissory1, scissorx2, scissory2; +#define BLURTILES 32 +#define BLURTILEMASK (0xFFFFFFFFU>>(32-BLURTILES)) + uint blurtiles[BLURTILES+1]; + + bool initialized; + + rendertarget() : texw(0), texh(0), vieww(0), viewh(0), colorfmt(GL_FALSE), depthfmt(GL_FALSE), rendertex(0), renderfb(0), renderdb(0), blurtex(0), blurfb(0), blurdb(0), blursize(0), blurysize(0), blursigma(0), initialized(false) + { + } + + virtual ~rendertarget() {} + + virtual GLenum attachment() const + { + return GL_COLOR_ATTACHMENT0; + } + + virtual const GLenum *colorformats() const + { + static const GLenum colorfmts[] = { GL_RGB, GL_RGB8, GL_FALSE }; + return colorfmts; + } + + virtual const GLenum *depthformats() const + { + static const GLenum depthfmts[] = { GL_DEPTH_COMPONENT24, GL_DEPTH_COMPONENT, GL_DEPTH_COMPONENT16, GL_DEPTH_COMPONENT32, GL_FALSE }; + return depthfmts; + } + + virtual bool depthtest() const { return true; } + + void cleanup(bool fullclean = false) + { + if(renderfb) { glDeleteFramebuffers_(1, &renderfb); renderfb = 0; } + if(renderdb) { glDeleteRenderbuffers_(1, &renderdb); renderdb = 0; } + if(rendertex) { glDeleteTextures(1, &rendertex); rendertex = 0; } + texw = texh = 0; + cleanupblur(); + + if(fullclean) colorfmt = depthfmt = GL_FALSE; + } + + void cleanupblur() + { + if(blurfb) { glDeleteFramebuffers_(1, &blurfb); blurfb = 0; } + if(blurtex) { glDeleteTextures(1, &blurtex); blurtex = 0; } + if(blurdb) { glDeleteRenderbuffers_(1, &blurdb); blurdb = 0; } + blursize = blurysize = 0; + blursigma = 0.0f; + } + + void setupblur() + { + if(!blurtex) glGenTextures(1, &blurtex); + createtexture(blurtex, texw, texh, NULL, 3, 1, colorfmt); + + if(!swaptexs() || rtsharefb) return; + if(!blurfb) glGenFramebuffers_(1, &blurfb); + glBindFramebuffer_(GL_FRAMEBUFFER, blurfb); + glFramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, blurtex, 0); + if(depthtest()) + { + if(!blurdb) glGenRenderbuffers_(1, &blurdb); + glGenRenderbuffers_(1, &blurdb); + glBindRenderbuffer_(GL_RENDERBUFFER, blurdb); + glRenderbufferStorage_(GL_RENDERBUFFER, depthfmt, texw, texh); + glFramebufferRenderbuffer_(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, blurdb); + } + glBindFramebuffer_(GL_FRAMEBUFFER, 0); + } + + void setup(int w, int h) + { + if(!renderfb) glGenFramebuffers_(1, &renderfb); + glBindFramebuffer_(GL_FRAMEBUFFER, renderfb); + if(!rendertex) glGenTextures(1, &rendertex); + + GLenum attach = attachment(); + if(attach == GL_DEPTH_ATTACHMENT) + { + glDrawBuffer(GL_NONE); + glReadBuffer(GL_NONE); + } + + const GLenum *colorfmts = colorformats(); + int find = 0; + do + { + createtexture(rendertex, w, h, NULL, 3, filter() ? 1 : 0, colorfmt ? colorfmt : colorfmts[find]); + glFramebufferTexture2D_(GL_FRAMEBUFFER, attach, GL_TEXTURE_2D, rendertex, 0); + if(glCheckFramebufferStatus_(GL_FRAMEBUFFER)==GL_FRAMEBUFFER_COMPLETE) break; + } + while(!colorfmt && colorfmts[++find]); + if(!colorfmt) colorfmt = colorfmts[find]; + + if(attach != GL_DEPTH_ATTACHMENT && depthtest()) + { + if(!renderdb) { glGenRenderbuffers_(1, &renderdb); depthfmt = GL_FALSE; } + if(!depthfmt) glBindRenderbuffer_(GL_RENDERBUFFER, renderdb); + const GLenum *depthfmts = depthformats(); + find = 0; + do + { + if(!depthfmt) glRenderbufferStorage_(GL_RENDERBUFFER, depthfmts[find], w, h); + glFramebufferRenderbuffer_(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, renderdb); + if(glCheckFramebufferStatus_(GL_FRAMEBUFFER)==GL_FRAMEBUFFER_COMPLETE) break; + } + while(!depthfmt && depthfmts[++find]); + if(!depthfmt) depthfmt = depthfmts[find]; + } + + glBindFramebuffer_(GL_FRAMEBUFFER, 0); + + texw = w; + texh = h; + initialized = false; + } + + bool addblurtiles(float x1, float y1, float x2, float y2, float blurmargin = 0) + { + if(x1 >= 1 || y1 >= 1 || x2 <= -1 || y2 <= -1) return false; + + scissorx1 = min(scissorx1, max(x1, -1.0f)); + scissory1 = min(scissory1, max(y1, -1.0f)); + scissorx2 = max(scissorx2, min(x2, 1.0f)); + scissory2 = max(scissory2, min(y2, 1.0f)); + + float blurerror = 2.0f*float(2*blursize + blurmargin); + int tx1 = max(0, min(BLURTILES - 1, int((x1-blurerror/vieww + 1)/2 * BLURTILES))), + ty1 = max(0, min(BLURTILES - 1, int((y1-blurerror/viewh + 1)/2 * BLURTILES))), + tx2 = max(0, min(BLURTILES - 1, int((x2+blurerror/vieww + 1)/2 * BLURTILES))), + ty2 = max(0, min(BLURTILES - 1, int((y2+blurerror/viewh + 1)/2 * BLURTILES))); + + uint mask = (BLURTILEMASK>>(BLURTILES - (tx2+1))) & (BLURTILEMASK< scissorx2 || y1-blurerror/viewh > scissory2) + return false; + + if(!blurtile) return true; + + int tx1 = max(0, min(BLURTILES - 1, int((x1 + 1)/2 * BLURTILES))), + ty1 = max(0, min(BLURTILES - 1, int((y1 + 1)/2 * BLURTILES))), + tx2 = max(0, min(BLURTILES - 1, int((x2 + 1)/2 * BLURTILES))), + ty2 = max(0, min(BLURTILES - 1, int((y2 + 1)/2 * BLURTILES))); + + uint mask = (BLURTILEMASK>>(BLURTILES - (tx2+1))) & (BLURTILEMASK<>= 8; x += 8; } + while(!(mask&1)) { mask >>= 1; x++; } + int xstart = x; + do { mask >>= 1; x++; } while(mask&1); + uint strip = (BLURTILEMASK>>(BLURTILES - x)) & (BLURTILEMASK< 0 && sh > 0; + if(!scissoring) { sx = sy = 0; sw = vieww; sh = viewh; } + blur(blursize, blursigma, blurysize, sx, sy, sw, sh, scissoring); + } + + virtual bool scissorrender(int &x, int &y, int &w, int &h) + { + if(scissorx1 >= scissorx2 || scissory1 >= scissory2) + { + if(vieww < texw || viewh < texh) + { + x = y = 0; + w = vieww; + h = viewh; + return true; + } + return false; + } + x = max(int(floor((scissorx1+1)/2*vieww)) - 2*blursize, 0); + y = max(int(floor((scissory1+1)/2*viewh)) - 2*blursize, 0); + w = min(int(ceil((scissorx2+1)/2*vieww)) + 2*blursize, vieww) - x; + h = min(int(ceil((scissory2+1)/2*viewh)) + 2*blursize, viewh) - y; + return true; + } + + virtual bool scissorblur(int &x, int &y, int &w, int &h) + { + if(scissorx1 >= scissorx2 || scissory1 >= scissory2) + { + if(vieww < texw || viewh < texh) + { + x = y = 0; + w = vieww; + h = viewh; + return true; + } + return false; + } + x = max(int(floor((scissorx1+1)/2*vieww)), 0); + y = max(int(floor((scissory1+1)/2*viewh)), 0); + w = min(int(ceil((scissorx2+1)/2*vieww)), vieww) - x; + h = min(int(ceil((scissory2+1)/2*viewh)), viewh) - y; + return true; + } + + virtual void doclear() {} + + virtual bool screenrect() const { return false; } + virtual bool filter() const { return true; } + + void render(int w, int h, int blursize = 0, float blursigma = 0, int blurysize = 0) + { + w = min(w, hwtexsize); + h = min(h, hwtexsize); + if(screenrect()) + { + if(w > screenw) w = screenw; + if(h > screenh) h = screenh; + } + vieww = w; + viewh = h; + if(w!=texw || h!=texh || (swaptexs() && !rtsharefb ? !blurfb : blurfb)) cleanup(); + + if(!filter()) + { + if(blurtex) cleanupblur(); + blursize = blurysize = 0; + } + + if(!rendertex) setup(w, h); + + scissorx2 = scissory2 = -1; + scissorx1 = scissory1 = 1; + memset(blurtiles, 0, sizeof(blurtiles)); + + if(!shouldrender()) return; + + if(blursize && !blurtex) setupblur(); + if(swaptexs() && blursize) + { + swap(rendertex, blurtex); + if(!rtsharefb) + { + swap(renderfb, blurfb); + swap(renderdb, blurdb); + } + } + glBindFramebuffer_(GL_FRAMEBUFFER, renderfb); + if(swaptexs() && blursize && rtsharefb) + glFramebufferTexture2D_(GL_FRAMEBUFFER, attachment(), GL_TEXTURE_2D, rendertex, 0); + glViewport(0, 0, vieww, viewh); + + doclear(); + + int sx, sy, sw, sh; + bool scissoring = rtscissor && scissorrender(sx, sy, sw, sh) && sw > 0 && sh > 0; + if(scissoring) + { + glScissor(sx, sy, sw, sh); + glEnable(GL_SCISSOR_TEST); + } + else + { + sx = sy = 0; + sw = vieww; + sh = viewh; + } + + if(!depthtest()) glDisable(GL_DEPTH_TEST); + + bool succeeded = dorender(); + + if(!depthtest()) glEnable(GL_DEPTH_TEST); + + if(scissoring) glDisable(GL_SCISSOR_TEST); + + if(succeeded) + { + initialized = true; + + if(blursize) doblur(blursize, blursigma, blurysize ? blurysize : blursize); + } + + glBindFramebuffer_(GL_FRAMEBUFFER, 0); + glViewport(0, 0, screenw, screenh); + } + + virtual void dodebug(int w, int h) {} + virtual bool flipdebug() const { return true; } + + void debugscissor(int w, int h, bool lines = false) + { + if(!rtscissor || scissorx1 >= scissorx2 || scissory1 >= scissory2) return; + int sx = int(0.5f*(scissorx1 + 1)*w), + sy = int(0.5f*(scissory1 + 1)*h), + sw = int(0.5f*(scissorx2 - scissorx1)*w), + sh = int(0.5f*(scissory2 - scissory1)*h); + if(flipdebug()) { sy = h - sy; sh = -sh; } + gle::defvertex(2); + gle::begin(lines ? GL_LINE_LOOP : GL_TRIANGLE_STRIP); + gle::attribf(sx, sy); + gle::attribf(sx + sw, sy); + if(lines) gle::attribf(sx + sw, sy + sh); + gle::attribf(sx, sy + sh); + if(!lines) gle::attribf(sx + sw, sy + sh); + gle::end(); + } + + void debugblurtiles(int w, int h, bool lines = false) + { + if(!blurtile) return; + float vxsz = float(w)/BLURTILES, vysz = float(h)/BLURTILES; + gle::defvertex(2); + loop(y, BLURTILES+1) + { + uint mask = blurtiles[y]; + int x = 0; + while(mask) + { + while(!(mask&0xFF)) { mask >>= 8; x += 8; } + while(!(mask&1)) { mask >>= 1; x++; } + int xstart = x; + do { mask >>= 1; x++; } while(mask&1); + uint strip = (BLURTILEMASK>>(BLURTILES - x)) & (BLURTILEMASK<set(); + gle::colorf(1, 1, 1); + glBindTexture(GL_TEXTURE_2D, rendertex); + float tx1 = 0, tx2 = 1, ty1 = 0, ty2 = 1; + if(flipdebug()) swap(ty1, ty2); + hudquad(0, 0, w, h, tx1, ty1, tx2-tx1, ty2-ty1); + hudnotextureshader->set(); + dodebug(w, h); + } +}; + diff --git a/src/engine/rendertext.cpp b/src/engine/rendertext.cpp new file mode 100644 index 0000000..44ad136 --- /dev/null +++ b/src/engine/rendertext.cpp @@ -0,0 +1,392 @@ +#include "engine.h" + +static hashnameset fonts; +static font *fontdef = NULL; +static int fontdeftex = 0; + +font *curfont = NULL; +int curfonttex = 0; + +void newfont(char *name, char *tex, int *defaultw, int *defaulth) +{ + font *f = &fonts[name]; + if(!f->name) f->name = newstring(name); + f->texs.shrink(0); + f->texs.add(textureload(tex)); + f->chars.shrink(0); + f->charoffset = '!'; + f->defaultw = *defaultw; + f->defaulth = *defaulth; + f->scale = f->defaulth; + + fontdef = f; + fontdeftex = 0; +} + +void fontoffset(char *c) +{ + if(!fontdef) return; + + fontdef->charoffset = c[0]; +} + +void fontscale(int *scale) +{ + if(!fontdef) return; + + fontdef->scale = *scale > 0 ? *scale : fontdef->defaulth; +} + +void fonttex(char *s) +{ + if(!fontdef) return; + + Texture *t = textureload(s); + loopv(fontdef->texs) if(fontdef->texs[i] == t) { fontdeftex = i; return; } + fontdeftex = fontdef->texs.length(); + fontdef->texs.add(t); +} + +void fontchar(int *x, int *y, int *w, int *h, int *offsetx, int *offsety, int *advance) +{ + if(!fontdef) return; + + font::charinfo &c = fontdef->chars.add(); + c.x = *x; + c.y = *y; + c.w = *w ? *w : fontdef->defaultw; + c.h = *h ? *h : fontdef->defaulth; + c.offsetx = *offsetx; + c.offsety = *offsety; + c.advance = *advance ? *advance : c.offsetx + c.w; + c.tex = fontdeftex; +} + +void fontskip(int *n) +{ + if(!fontdef) return; + loopi(max(*n, 1)) + { + font::charinfo &c = fontdef->chars.add(); + c.x = c.y = c.w = c.h = c.offsetx = c.offsety = c.advance = c.tex = 0; + } +} + +COMMANDN(font, newfont, "ssii"); +COMMAND(fontoffset, "s"); +COMMAND(fontscale, "i"); +COMMAND(fonttex, "s"); +COMMAND(fontchar, "iiiiiii"); +COMMAND(fontskip, "i"); + +void fontalias(const char *dst, const char *src) +{ + font *s = fonts.access(src); + if(!s) return; + font *d = &fonts[dst]; + if(!d->name) d->name = newstring(dst); + d->texs = s->texs; + d->chars = s->chars; + d->charoffset = s->charoffset; + d->defaultw = s->defaultw; + d->defaulth = s->defaulth; + d->scale = s->scale; + + fontdef = d; + fontdeftex = d->texs.length()-1; +} + +COMMAND(fontalias, "ss"); + +bool setfont(const char *name) +{ + font *f = fonts.access(name); + if(!f) return false; + curfont = f; + return true; +} + +static vector fontstack; + +void pushfont() +{ + fontstack.add(curfont); +} + +bool popfont() +{ + if(fontstack.empty()) return false; + curfont = fontstack.pop(); + return true; +} + +void gettextres(int &w, int &h) +{ + if(w < MINRESW || h < MINRESH) + { + if(MINRESW > w*MINRESH/h) + { + h = h*MINRESW/w; + w = MINRESW; + } + else + { + w = w*MINRESH/h; + h = MINRESH; + } + } +} + +float text_widthf(const char *str) +{ + float width, height; + text_boundsf(str, width, height); + return width; +} + +#define FONTTAB (4*FONTW) +#define TEXTTAB(x) ((int((x)/FONTTAB)+1.0f)*FONTTAB) + +void tabify(const char *str, int *numtabs) +{ + int tw = max(*numtabs, 0)*FONTTAB-1, tabs = 0; + for(float w = text_widthf(str); w <= tw; w = TEXTTAB(w)) ++tabs; + int len = strlen(str); + char *tstr = newstring(len + tabs); + memcpy(tstr, str, len); + memset(&tstr[len], '\t', tabs); + tstr[len+tabs] = '\0'; + stringret(tstr); +} + +COMMAND(tabify, "si"); + +void draw_textf(const char *fstr, int left, int top, ...) +{ + defvformatstring(str, top, fstr); + draw_text(str, left, top); +} + +const matrix4x3 *textmatrix = NULL; + +static float draw_char(Texture *&tex, int c, float x, float y, float scale) +{ + font::charinfo &info = curfont->chars[c-curfont->charoffset]; + if(tex != curfont->texs[info.tex]) + { + xtraverts += gle::end(); + tex = curfont->texs[info.tex]; + glBindTexture(GL_TEXTURE_2D, tex->id); + } + + float x1 = x + scale*info.offsetx, + y1 = y + scale*info.offsety, + x2 = x + scale*(info.offsetx + info.w), + y2 = y + scale*(info.offsety + info.h), + tx1 = info.x / float(tex->xs), + ty1 = info.y / float(tex->ys), + tx2 = (info.x + info.w) / float(tex->xs), + ty2 = (info.y + info.h) / float(tex->ys); + + if(textmatrix) + { + gle::attrib(textmatrix->transform(vec2(x1, y1))); gle::attribf(tx1, ty1); + gle::attrib(textmatrix->transform(vec2(x2, y1))); gle::attribf(tx2, ty1); + gle::attrib(textmatrix->transform(vec2(x2, y2))); gle::attribf(tx2, ty2); + gle::attrib(textmatrix->transform(vec2(x1, y2))); gle::attribf(tx1, ty2); + } + else + { + gle::attribf(x1, y1); gle::attribf(tx1, ty1); + gle::attribf(x2, y1); gle::attribf(tx2, ty1); + gle::attribf(x2, y2); gle::attribf(tx2, ty2); + gle::attribf(x1, y2); gle::attribf(tx1, ty2); + } + + return scale*info.advance; +} + +//stack[sp] is current color index +static void text_color(char c, char *stack, int size, int &sp, bvec color, int a) +{ + if(c=='s') // save color + { + c = stack[sp]; + if(sp 0) --sp; c = stack[sp]; } // restore color + else stack[sp] = c; + switch(c) + { + case '0': color = bvec( 64, 255, 128); break; // green: player talk + case '1': color = bvec( 96, 160, 255); break; // blue: "echo" command + case '2': color = bvec(255, 192, 64); break; // yellow: gameplay messages + case '3': color = bvec(255, 64, 64); break; // red: important errors + case '4': color = bvec(128, 128, 128); break; // gray + case '5': color = bvec(192, 64, 192); break; // magenta + case '6': color = bvec(255, 128, 0); break; // orange + case '7': color = bvec(255, 255, 255); break; // white + case '8': color = bvec( 96, 240, 255); break; // cyan + // provided color: everything else + } + gle::color(color, a); + } +} + +#define TEXTSKELETON \ + float y = 0, x = 0, scale = curfont->scale/float(curfont->defaulth);\ + int i;\ + for(i = 0; str[i]; i++)\ + {\ + TEXTINDEX(i)\ + int c = uchar(str[i]);\ + if(c=='\t') { x = TEXTTAB(x); TEXTWHITE(i) }\ + else if(c==' ') { x += scale*curfont->defaultw; TEXTWHITE(i) }\ + else if(c=='\n') { TEXTLINE(i) x = 0; y += FONTH; }\ + else if(c=='\f') { if(str[i+1]) { i++; TEXTCOLOR(i) }}\ + else if(curfont->chars.inrange(c-curfont->charoffset))\ + {\ + float cw = scale*curfont->chars[c-curfont->charoffset].advance;\ + if(cw <= 0) continue;\ + if(maxwidth != -1)\ + {\ + int j = i;\ + float w = cw;\ + for(; str[i+1]; i++)\ + {\ + int c = uchar(str[i+1]);\ + if(c=='\f') { if(str[i+2]) i++; continue; }\ + if(i-j > 16) break;\ + if(!curfont->chars.inrange(c-curfont->charoffset)) break;\ + float cw = scale*curfont->chars[c-curfont->charoffset].advance;\ + if(cw <= 0 || w + cw > maxwidth) break;\ + w += cw;\ + }\ + if(x + w > maxwidth && j!=0) { TEXTLINE(j-1) x = 0; y += FONTH; }\ + TEXTWORD\ + }\ + else\ + { TEXTCHAR(i) }\ + }\ + } + +//all the chars are guaranteed to be either drawable or color commands +#define TEXTWORDSKELETON \ + for(; j <= i; j++)\ + {\ + TEXTINDEX(j)\ + int c = uchar(str[j]);\ + if(c=='\f') { if(str[j+1]) { j++; TEXTCOLOR(j) }}\ + else { float cw = scale*curfont->chars[c-curfont->charoffset].advance; TEXTCHAR(j) }\ + } + +#define TEXTEND(cursor) if(cursor >= i) { do { TEXTINDEX(cursor); } while(0); } + +int text_visible(const char *str, float hitx, float hity, int maxwidth) +{ + #define TEXTINDEX(idx) + #define TEXTWHITE(idx) if(y+FONTH > hity && x >= hitx) return idx; + #define TEXTLINE(idx) if(y+FONTH > hity) return idx; + #define TEXTCOLOR(idx) + #define TEXTCHAR(idx) x += cw; TEXTWHITE(idx) + #define TEXTWORD TEXTWORDSKELETON + TEXTSKELETON + #undef TEXTINDEX + #undef TEXTWHITE + #undef TEXTLINE + #undef TEXTCOLOR + #undef TEXTCHAR + #undef TEXTWORD + return i; +} + +//inverse of text_visible +void text_posf(const char *str, int cursor, float &cx, float &cy, int maxwidth) +{ + #define TEXTINDEX(idx) if(idx == cursor) { cx = x; cy = y; break; } + #define TEXTWHITE(idx) + #define TEXTLINE(idx) + #define TEXTCOLOR(idx) + #define TEXTCHAR(idx) x += cw; + #define TEXTWORD TEXTWORDSKELETON if(i >= cursor) break; + cx = cy = 0; + TEXTSKELETON + TEXTEND(cursor) + #undef TEXTINDEX + #undef TEXTWHITE + #undef TEXTLINE + #undef TEXTCOLOR + #undef TEXTCHAR + #undef TEXTWORD +} + +void text_boundsf(const char *str, float &width, float &height, int maxwidth) +{ + #define TEXTINDEX(idx) + #define TEXTWHITE(idx) + #define TEXTLINE(idx) if(x > width) width = x; + #define TEXTCOLOR(idx) + #define TEXTCHAR(idx) x += cw; + #define TEXTWORD x += w; + width = 0; + TEXTSKELETON + height = y + FONTH; + TEXTLINE(_) + #undef TEXTINDEX + #undef TEXTWHITE + #undef TEXTLINE + #undef TEXTCOLOR + #undef TEXTCHAR + #undef TEXTWORD +} + +void draw_text(const char *str, int left, int top, int r, int g, int b, int a, int cursor, int maxwidth) +{ + #define TEXTINDEX(idx) if(idx == cursor) { cx = x; cy = y; } + #define TEXTWHITE(idx) + #define TEXTLINE(idx) + #define TEXTCOLOR(idx) if(usecolor) text_color(str[idx], colorstack, sizeof(colorstack), colorpos, color, a); + #define TEXTCHAR(idx) draw_char(tex, c, left+x, top+y, scale); x += cw; + #define TEXTWORD TEXTWORDSKELETON + char colorstack[10]; + colorstack[0] = 'c'; //indicate user color + bvec color(r, g, b); + int colorpos = 0; + float cx = -FONTW, cy = 0; + bool usecolor = true; + if(a < 0) { usecolor = false; a = -a; } + Texture *tex = curfont->texs[0]; + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glBindTexture(GL_TEXTURE_2D, tex->id); + gle::color(color, a); + gle::defvertex(textmatrix ? 3 : 2); + gle::deftexcoord0(); + gle::begin(GL_QUADS); + TEXTSKELETON + TEXTEND(cursor) + xtraverts += gle::end(); + if(cursor >= 0 && (totalmillis/250)&1) + { + gle::color(color, a); + if(maxwidth != -1 && cx >= maxwidth) { cx = 0; cy += FONTH; } + draw_char(tex, '_', left+cx, top+cy, scale); + xtraverts += gle::end(); + } + #undef TEXTINDEX + #undef TEXTWHITE + #undef TEXTLINE + #undef TEXTCOLOR + #undef TEXTCHAR + #undef TEXTWORD +} + +void reloadfonts() +{ + enumerate(fonts, font, f, + loopv(f.texs) if(!reloadtexture(*f.texs[i])) fatal("failed to reload font texture"); + ); +} + diff --git a/src/engine/renderva.cpp b/src/engine/renderva.cpp new file mode 100644 index 0000000..cf9d376 --- /dev/null +++ b/src/engine/renderva.cpp @@ -0,0 +1,1896 @@ +// renderva.cpp: handles the occlusion and rendering of vertex arrays + +#include "engine.h" + +static inline void drawtris(GLsizei numindices, const GLvoid *indices, ushort minvert, ushort maxvert) +{ + glDrawRangeElements_(GL_TRIANGLES, minvert, maxvert, numindices, GL_UNSIGNED_SHORT, indices); + glde++; +} + +static inline void drawvatris(vtxarray *va, GLsizei numindices, const GLvoid *indices) +{ + drawtris(numindices, indices, va->minvert, va->maxvert); +} + +///////// view frustrum culling /////////////////////// + +plane vfcP[5]; // perpindictular vectors to view frustrum bounding planes +float vfcDfog; // far plane culling distance (fog limit). +float vfcDnear[5], vfcDfar[5]; + +vtxarray *visibleva; + +bool isfoggedsphere(float rad, const vec &cv) +{ + loopi(4) if(vfcP[i].dist(cv) < -rad) return true; + float dist = vfcP[4].dist(cv); + return dist < -rad || dist > vfcDfog + rad; +} + +int isvisiblesphere(float rad, const vec &cv) +{ + int v = VFC_FULL_VISIBLE; + float dist; + + loopi(5) + { + dist = vfcP[i].dist(cv); + if(dist < -rad) return VFC_NOT_VISIBLE; + if(dist < rad) v = VFC_PART_VISIBLE; + } + + dist -= vfcDfog; + if(dist > rad) return VFC_FOGGED; //VFC_NOT_VISIBLE; // culling when fog is closer than size of world results in HOM + if(dist > -rad) v = VFC_PART_VISIBLE; + + return v; +} + +static inline int ishiddencube(const ivec &o, int size) +{ + loopi(5) if(o.dist(vfcP[i]) < -vfcDfar[i]*size) return true; + return false; +} + +static inline int isfoggedcube(const ivec &o, int size) +{ + loopi(4) if(o.dist(vfcP[i]) < -vfcDfar[i]*size) return true; + float dist = o.dist(vfcP[4]); + return dist < -vfcDfar[4]*size || dist > vfcDfog - vfcDnear[4]*size; +} + +int isvisiblecube(const ivec &o, int size) +{ + int v = VFC_FULL_VISIBLE; + float dist; + + loopi(5) + { + dist = o.dist(vfcP[i]); + if(dist < -vfcDfar[i]*size) return VFC_NOT_VISIBLE; + if(dist < -vfcDnear[i]*size) v = VFC_PART_VISIBLE; + } + + dist -= vfcDfog; + if(dist > -vfcDnear[4]*size) return VFC_FOGGED; + if(dist > -vfcDfar[4]*size) v = VFC_PART_VISIBLE; + + return v; +} + +float vadist(vtxarray *va, const vec &p) +{ + return p.dist_to_bb(va->bbmin, va->bbmax); +} + +#define VASORTSIZE 64 + +static vtxarray *vasort[VASORTSIZE]; + +void addvisibleva(vtxarray *va) +{ + float dist = vadist(va, camera1->o); + va->distance = int(dist); /*cv.dist(camera1->o) - va->size*SQRT3/2*/ + + int hash = clamp(int(dist*VASORTSIZE/worldsize), 0, VASORTSIZE-1); + vtxarray **prev = &vasort[hash], *cur = vasort[hash]; + + while(cur && va->distance >= cur->distance) + { + prev = &cur->next; + cur = cur->next; + } + + va->next = *prev; + *prev = va; +} + +void sortvisiblevas() +{ + visibleva = NULL; + vtxarray **last = &visibleva; + loopi(VASORTSIZE) if(vasort[i]) + { + vtxarray *va = vasort[i]; + *last = va; + while(va->next) va = va->next; + last = &va->next; + } +} + +void findvisiblevas(vector &vas, bool resetocclude = false) +{ + loopv(vas) + { + vtxarray &v = *vas[i]; + int prevvfc = resetocclude ? VFC_NOT_VISIBLE : v.curvfc; + v.curvfc = isvisiblecube(v.o, v.size); + if(v.curvfc!=VFC_NOT_VISIBLE) + { + if(pvsoccluded(v.o, v.size)) + { + v.curvfc += PVS_FULL_VISIBLE - VFC_FULL_VISIBLE; + continue; + } + addvisibleva(&v); + if(v.children.length()) findvisiblevas(v.children, prevvfc>=VFC_NOT_VISIBLE); + if(prevvfc>=VFC_NOT_VISIBLE) + { + v.occluded = !v.texs ? OCCLUDE_GEOM : OCCLUDE_NOTHING; + v.query = NULL; + } + } + } +} + +void calcvfcD() +{ + loopi(5) + { + plane &p = vfcP[i]; + vfcDnear[i] = vfcDfar[i] = 0; + loopk(3) if(p[k] > 0) vfcDfar[i] += p[k]; + else vfcDnear[i] += p[k]; + } +} + +void setvfcP(float z, const vec &bbmin, const vec &bbmax) +{ + vec4 px = camprojmatrix.rowx(), py = camprojmatrix.rowy(), pz = camprojmatrix.rowz(), pw = camprojmatrix.roww(); + vfcP[0] = plane(vec4(pw).mul(-bbmin.x).add(px)).normalize(); // left plane + vfcP[1] = plane(vec4(pw).mul(bbmax.x).sub(px)).normalize(); // right plane + vfcP[2] = plane(vec4(pw).mul(-bbmin.y).add(py)).normalize(); // bottom plane + vfcP[3] = plane(vec4(pw).mul(bbmax.y).sub(py)).normalize(); // top plane + vfcP[4] = plane(vec4(pw).add(pz)).normalize(); // near/far planes + if(z >= 0) loopi(5) vfcP[i].reflectz(z); + + vfcDfog = fog; + calcvfcD(); +} + +plane oldvfcP[5]; + +void savevfcP() +{ + memcpy(oldvfcP, vfcP, sizeof(vfcP)); +} + +void restorevfcP() +{ + memcpy(vfcP, oldvfcP, sizeof(vfcP)); + calcvfcD(); +} + +void visiblecubes(bool cull) +{ + memclear(vasort); + + if(cull) + { + setvfcP(); + findvisiblevas(varoot); + sortvisiblevas(); + } + else + { + memclear(vfcP); + vfcDfog = 1000000; + memclear(vfcDnear); + memclear(vfcDfar); + visibleva = NULL; + loopv(valist) + { + vtxarray *va = valist[i]; + va->distance = 0; + va->curvfc = VFC_FULL_VISIBLE; + va->occluded = !va->texs ? OCCLUDE_GEOM : OCCLUDE_NOTHING; + va->query = NULL; + va->next = visibleva; + visibleva = va; + } + } +} + +static inline bool insideva(const vtxarray *va, const vec &v, int margin = 2) +{ + int size = va->size + margin; + return v.x>=va->o.x-margin && v.y>=va->o.y-margin && v.z>=va->o.z-margin && + v.x<=va->o.x+size && v.y<=va->o.y+size && v.z<=va->o.z+size; +} + +///////// occlusion queries ///////////// + +#define MAXQUERY 2048 +#define MAXQUERYFRAMES 2 + +struct queryframe +{ + int cur, max; + occludequery queries[MAXQUERY]; + + queryframe() : cur(0), max(0) {} + + void flip() { loopi(cur) queries[i].owner = NULL; cur = 0; } + + occludequery *newquery(void *owner) + { + if(cur >= max) + { + if(max >= MAXQUERY) return NULL; + glGenQueries_(1, &queries[max++].id); + } + occludequery *query = &queries[cur++]; + query->owner = owner; + query->fragments = -1; + return query; + } + + void reset() { loopi(max) queries[i].owner = NULL; } + + void cleanup() + { + loopi(max) + { + glDeleteQueries_(1, &queries[i].id); + queries[i].owner = NULL; + } + cur = max = 0; + } +}; + +static queryframe queryframes[MAXQUERYFRAMES]; +static uint flipquery = 0; + +int getnumqueries() +{ + return queryframes[flipquery].cur; +} + +void flipqueries() +{ + flipquery = (flipquery + 1) % MAXQUERYFRAMES; + queryframes[flipquery].flip(); +} + +occludequery *newquery(void *owner) +{ + return queryframes[flipquery].newquery(owner); +} + +void resetqueries() +{ + loopi(MAXQUERYFRAMES) queryframes[i].reset(); +} + +void clearqueries() +{ + loopi(MAXQUERYFRAMES) queryframes[i].cleanup(); +} + +VAR(oqfrags, 0, 8, 64); +VAR(oqwait, 0, 1, 1); + +void startquery(occludequery *query) +{ + glBeginQuery_(GL_SAMPLES_PASSED, query->id); +} + +void endquery(occludequery *query) +{ + glEndQuery_(GL_SAMPLES_PASSED); +} + +bool checkquery(occludequery *query, bool nowait) +{ + GLuint fragments; + if(query->fragments >= 0) fragments = query->fragments; + else + { + if(nowait || !oqwait) + { + GLint avail; + glGetQueryObjectiv_(query->id, GL_QUERY_RESULT_AVAILABLE, &avail); + if(!avail) return false; + } + glGetQueryObjectuiv_(query->id, GL_QUERY_RESULT, &fragments); + query->fragments = fragments; + } + return fragments < uint(oqfrags); +} + +static GLuint bbvbo = 0, bbebo = 0; + +static void setupbb() +{ + if(!bbvbo) + { + glGenBuffers_(1, &bbvbo); + gle::bindvbo(bbvbo); + vec verts[8]; + loopi(8) verts[i] = vec(i&1, (i>>1)&1, (i>>2)&1); + glBufferData_(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW); + gle::clearvbo(); + } + if(!bbebo) + { + glGenBuffers_(1, &bbebo); + gle::bindebo(bbebo); + GLushort tris[3*2*6]; + #define GENFACEORIENT(orient, v0, v1, v2, v3) do { \ + int offset = orient*3*2; \ + tris[offset + 0] = v0; \ + tris[offset + 1] = v1; \ + tris[offset + 2] = v2; \ + tris[offset + 3] = v0; \ + tris[offset + 4] = v2; \ + tris[offset + 5] = v3; \ + } while(0); + #define GENFACEVERT(orient, vert, ox,oy,oz, rx,ry,rz) (ox | oy | oz) + GENFACEVERTS(0, 1, 0, 2, 0, 4, , , , , , ) + #undef GENFACEORIENT + #undef GENFACEVERT + glBufferData_(GL_ELEMENT_ARRAY_BUFFER, sizeof(tris), tris, GL_STATIC_DRAW); + gle::clearebo(); + } +} + +static void cleanupbb() +{ + if(bbvbo) { glDeleteBuffers_(1, &bbvbo); bbvbo = 0; } + if(bbebo) { glDeleteBuffers_(1, &bbebo); bbebo = 0; } +} + +void startbb(bool mask) +{ + setupbb(); + gle::bindvbo(bbvbo); + gle::bindebo(bbebo); + gle::vertexpointer(sizeof(vec), (const vec *)0); + gle::enablevertex(); + SETSHADER(bbquery); + if(mask) + { + glDepthMask(GL_FALSE); + glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + } +} + +void endbb(bool mask) +{ + gle::disablevertex(); + gle::clearvbo(); + gle::clearebo(); + if(mask) + { + glDepthMask(GL_TRUE); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + } +} + +void drawbb(const ivec &bo, const ivec &br) +{ + LOCALPARAMF(bborigin, bo.x, bo.y, bo.z); + LOCALPARAMF(bbsize, br.x, br.y, br.z); + glDrawRangeElements_(GL_TRIANGLES, 0, 8-1, 3*2*6, GL_UNSIGNED_SHORT, (ushort *)0); + xtraverts += 8; +} + +extern int octaentsize; + +static octaentities *visiblemms, **lastvisiblemms; + +static inline bool insideoe(const octaentities *oe, const vec &v, int margin = 1) +{ + return v.x>=oe->bbmin.x-margin && v.y>=oe->bbmin.y-margin && v.z>=oe->bbmin.z-margin && + v.x<=oe->bbmax.x+margin && v.y<=oe->bbmax.y+margin && v.z<=oe->bbmax.z+margin; +} + +void findvisiblemms(const vector &ents, bool doquery) +{ + visiblemms = NULL; + lastvisiblemms = &visiblemms; + for(vtxarray *va = visibleva; va; va = va->next) + { + if(va->mapmodels.empty() || va->curvfc >= VFC_FOGGED || va->occluded >= OCCLUDE_BB) continue; + loopv(va->mapmodels) + { + octaentities *oe = va->mapmodels[i]; + if(isfoggedcube(oe->o, oe->size) || pvsoccluded(oe->bbmin, oe->bbmax)) continue; + + bool occluded = doquery && oe->query && oe->query->owner == oe && checkquery(oe->query); + if(occluded) + { + oe->distance = -1; + + oe->next = NULL; + *lastvisiblemms = oe; + lastvisiblemms = &oe->next; + } + else + { + int visible = 0; + loopv(oe->mapmodels) + { + extentity &e = *ents[oe->mapmodels[i]]; + if(e.flags&EF_NOVIS) continue; + e.flags |= EF_RENDER; + ++visible; + } + if(!visible) continue; + + oe->distance = int(camera1->o.dist_to_bb(oe->o, oe->size)); + + octaentities **prev = &visiblemms, *cur = visiblemms; + while(cur && cur->distance >= 0 && oe->distance > cur->distance) + { + prev = &cur->next; + cur = cur->next; + } + + if(*prev == NULL) lastvisiblemms = &oe->next; + oe->next = *prev; + *prev = oe; + } + } + } +} + +VAR(oqmm, 0, 4, 8); + +void rendermapmodel(extentity &e) +{ + int anim = ANIM_MAPMODEL|ANIM_LOOP, basetime = 0; + if(e.flags&EF_ANIM) entities::animatemapmodel(e, anim, basetime); + mapmodelinfo *mmi = getmminfo(e.attr2); + if(mmi) rendermodel(&e.light, mmi->name, anim, e.o, e.attr1, 0, MDL_CULL_VFC | MDL_CULL_DIST | MDL_DYNLIGHT, NULL, NULL, basetime); +} + +vtxarray *reflectedva; + +void renderreflectedmapmodels() +{ + const vector &ents = entities::getents(); + + octaentities *mms = visiblemms; + if(reflecting) + { + octaentities **lastmms = &mms; + for(vtxarray *va = reflectedva; va; va = va->rnext) + { + if(va->mapmodels.empty() || va->distance > reflectdist) continue; + loopv(va->mapmodels) + { + octaentities *oe = va->mapmodels[i]; + *lastmms = oe; + lastmms = &oe->rnext; + } + } + *lastmms = NULL; + } + for(octaentities *oe = mms; oe; oe = reflecting ? oe->rnext : oe->next) if(reflecting || oe->distance >= 0) + { + if(reflecting || refracting>0 ? oe->bbmax.z <= reflectz : oe->bbmin.z >= reflectz) continue; + if(isfoggedcube(oe->o, oe->size)) continue; + loopv(oe->mapmodels) + { + extentity &e = *ents[oe->mapmodels[i]]; + if(e.flags&(EF_NOVIS | EF_RENDER)) continue; + e.flags |= EF_RENDER; + } + } + if(mms) + { + startmodelbatches(); + for(octaentities *oe = mms; oe; oe = reflecting ? oe->rnext : oe->next) + { + loopv(oe->mapmodels) + { + extentity &e = *ents[oe->mapmodels[i]]; + if(!(e.flags&EF_RENDER)) continue; + rendermapmodel(e); + e.flags &= ~EF_RENDER; + } + } + endmodelbatches(); + } +} + +void rendermapmodels() +{ + static int skipoq = 0; + bool doquery = !drawtex && oqfrags && oqmm; + const vector &ents = entities::getents(); + findvisiblemms(ents, doquery); + + startmodelbatches(); + for(octaentities *oe = visiblemms; oe; oe = oe->next) if(oe->distance>=0) + { + bool rendered = false; + loopv(oe->mapmodels) + { + extentity &e = *ents[oe->mapmodels[i]]; + if(!(e.flags&EF_RENDER)) continue; + if(!rendered) + { + rendered = true; + oe->query = doquery && oe->distance>0 && !(++skipoq%oqmm) ? newquery(oe) : NULL; + if(oe->query) startmodelquery(oe->query); + } + rendermapmodel(e); + e.flags &= ~EF_RENDER; + } + if(rendered && oe->query) endmodelquery(); + } + endmodelbatches(); + + bool queried = true; + for(octaentities *oe = visiblemms; oe; oe = oe->next) if(oe->distance<0) + { + oe->query = doquery && !insideoe(oe, camera1->o) ? newquery(oe) : NULL; + if(!oe->query) continue; + if(queried) + { + startbb(); + queried = false; + } + startquery(oe->query); + drawbb(oe->bbmin, ivec(oe->bbmax).sub(oe->bbmin)); + endquery(oe->query); + } + if(!queried) + { + endbb(); + } +} + +static inline bool bbinsideva(const ivec &bo, const ivec &br, vtxarray *va) +{ + return bo.x >= va->bbmin.x && bo.y >= va->bbmin.y && bo.z >= va->bbmin.z && + br.x <= va->bbmax.x && br.y <= va->bbmax.y && br.z <= va->bbmax.z; +} + +static inline bool bboccluded(const ivec &bo, const ivec &br, cube *c, const ivec &o, int size) +{ + loopoctabox(o, size, bo, br) + { + ivec co(i, o, size); + if(c[i].ext && c[i].ext->va) + { + vtxarray *va = c[i].ext->va; + if(va->curvfc >= VFC_FOGGED || (va->occluded >= OCCLUDE_BB && bbinsideva(bo, br, va))) continue; + } + if(c[i].children && bboccluded(bo, br, c[i].children, co, size>>1)) continue; + return false; + } + return true; +} + +bool bboccluded(const ivec &bo, const ivec &br) +{ + int diff = (bo.x^br.x) | (bo.y^br.y) | (bo.z^br.z); + if(diff&~((1<ext && c->ext->va) + { + vtxarray *va = c->ext->va; + if(va->curvfc >= VFC_FOGGED || (va->occluded >= OCCLUDE_BB && bbinsideva(bo, br, va))) return true; + } + scale--; + while(c->children && !(diff&(1<children[octastep(bo.x, bo.y, bo.z, scale)]; + if(c->ext && c->ext->va) + { + vtxarray *va = c->ext->va; + if(va->curvfc >= VFC_FOGGED || (va->occluded >= OCCLUDE_BB && bbinsideva(bo, br, va))) return true; + } + scale--; + } + if(c->children) return bboccluded(bo, br, c->children, ivec(bo).mask(~((2<set(); + + gle::enablevertex(); + + glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + gle::color(vec::hexcolor(outlinecolour)); + + enablepolygonoffset(GL_POLYGON_OFFSET_LINE); + + if(!dtoutline) glDisable(GL_DEPTH_TEST); + + vtxarray *prev = NULL; + for(vtxarray *va = visibleva; va; va = va->next) + { + if(va->occluded >= OCCLUDE_BB) continue; + if(!va->alphaback && !va->alphafront && (!va->texs || va->occluded >= OCCLUDE_GEOM)) continue; + + if(!prev || va->vbuf != prev->vbuf) + { + gle::bindvbo(va->vbuf); + gle::bindebo(va->ebuf); + const vertex *ptr = 0; + gle::vertexpointer(sizeof(vertex), ptr->pos.v); + } + + if(va->texs && va->occluded < OCCLUDE_GEOM) + { + drawvatris(va, 3*va->tris, va->edata); + xtravertsva += va->verts; + } + if(va->alphatris) + { + drawvatris(va, 3*va->alphatris, &va->edata[3*(va->tris + va->blendtris)]); + xtravertsva += 3*va->alphatris; + } + + prev = va; + } + + if(!dtoutline) glEnable(GL_DEPTH_TEST); + + disablepolygonoffset(GL_POLYGON_OFFSET_LINE); + + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + + gle::clearvbo(); + gle::clearebo(); + gle::disablevertex(); +} + +HVAR(blendbrushcolor, 0, 0x0000C0, 0xFFFFFF); + +void renderblendbrush(GLuint tex, float x, float y, float w, float h) +{ + SETSHADER(blendbrush); + + gle::enablevertex(); + + glDepthFunc(GL_LEQUAL); + + glEnable(GL_BLEND); + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + + glBindTexture(GL_TEXTURE_2D, tex); + gle::color(vec::hexcolor(blendbrushcolor), 0.25f); + + LOCALPARAMF(texgenS, 1.0f/w, 0, 0, -x/w); + LOCALPARAMF(texgenT, 0, 1.0f/h, 0, -y/h); + + vtxarray *prev = NULL; + for(vtxarray *va = visibleva; va; va = va->next) + { + if(!va->texs || va->occluded >= OCCLUDE_GEOM) continue; + if(va->o.x + va->size <= x || va->o.y + va->size <= y || va->o.x >= x + w || va->o.y >= y + h) continue; + + if(!prev || va->vbuf != prev->vbuf) + { + gle::bindvbo(va->vbuf); + gle::bindebo(va->ebuf); + const vertex *ptr = 0; + gle::vertexpointer(sizeof(vertex), ptr->pos.v); + } + + drawvatris(va, 3*va->tris, va->edata); + xtravertsva += va->verts; + + prev = va; + } + + glDisable(GL_BLEND); + + glDepthFunc(GL_LESS); + + gle::clearvbo(); + gle::clearebo(); + gle::disablevertex(); +} + +void rendershadowmapreceivers() +{ + SETSHADER(shadowmapreceiver); + + gle::enablevertex(); + + glCullFace(GL_FRONT); + glDepthMask(GL_FALSE); + glDepthFunc(GL_GREATER); + + extern int ati_minmax_bug; + if(!ati_minmax_bug) glColorMask(GL_FALSE, GL_FALSE, GL_TRUE, GL_FALSE); + + glEnable(GL_BLEND); + glBlendEquation_(GL_MAX); + glBlendFunc(GL_ONE, GL_ONE); + + vtxarray *prev = NULL; + for(vtxarray *va = visibleva; va; va = va->next) + { + if(!va->texs || va->curvfc >= VFC_FOGGED || !isshadowmapreceiver(va)) continue; + + if(!prev || va->vbuf != prev->vbuf) + { + gle::bindvbo(va->vbuf); + gle::bindebo(va->ebuf); + const vertex *ptr = 0; + gle::vertexpointer(sizeof(vertex), ptr->pos.v); + } + + drawvatris(va, 3*va->tris, va->edata); + xtravertsva += va->verts; + + prev = va; + } + + glDisable(GL_BLEND); + glBlendEquation_(GL_FUNC_ADD); + + glCullFace(GL_BACK); + glDepthMask(GL_TRUE); + glDepthFunc(GL_LESS); + + if(!ati_minmax_bug) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + + gle::clearvbo(); + gle::clearebo(); + gle::disablevertex(); +} + +void renderdepthobstacles(const vec &bbmin, const vec &bbmax, float scale, float *ranges, int numranges) +{ + float scales[4] = { 0, 0, 0, 0 }, offsets[4] = { 0, 0, 0, 0 }; + if(numranges < 0) + { + SETSHADER(depthfxsplitworld); + + loopi(-numranges) + { + if(!i) scales[i] = 1.0f/scale; + else scales[i] = scales[i-1]*256; + } + } + else + { + SETSHADER(depthfxworld); + + if(!numranges) loopi(4) scales[i] = 1.0f/scale; + else loopi(numranges) + { + scales[i] = 1.0f/scale; + offsets[i] = -ranges[i]/scale; + } + } + LOCALPARAMF(depthscale, scales[0], scales[1], scales[2], scales[3]); + LOCALPARAMF(depthoffsets, offsets[0], offsets[1], offsets[2], offsets[3]); + + gle::enablevertex(); + + vtxarray *prev = NULL; + for(vtxarray *va = visibleva; va; va = va->next) + { + if(!va->texs || va->occluded >= OCCLUDE_GEOM || + va->o.x > bbmax.x || va->o.y > bbmax.y || va->o.z > bbmax.z || + va->o.x + va->size < bbmin.x || va->o.y + va->size < bbmin.y || va->o.z + va->size < bbmin.z) + continue; + + if(!prev || va->vbuf != prev->vbuf) + { + gle::bindvbo(va->vbuf); + gle::bindebo(va->ebuf); + const vertex *ptr = 0; + gle::vertexpointer(sizeof(vertex), ptr->pos.v); + } + + drawvatris(va, 3*va->tris, va->edata); + xtravertsva += va->verts; + if(va->alphatris > 0) + { + drawvatris(va, 3*va->alphatris, va->edata + 3*(va->tris + va->blendtris)); + xtravertsva += 3*va->alphatris; + } + + prev = va; + } + + gle::clearvbo(); + gle::clearebo(); + gle::disablevertex(); +} + +VAR(oqdist, 0, 256, 1024); +VAR(zpass, 0, 1, 1); +VAR(envpass, 0, 1, 1); + +struct renderstate +{ + bool colormask, depthmask, blending; + int alphaing; + GLuint vbuf; + bool vattribs, vquery; + vec colorscale, lightcolor; + float alphascale; + GLuint textures[8]; + Slot *slot, *texgenslot; + VSlot *vslot, *texgenvslot; + vec2 texgenscroll; + int texgendim; + int visibledynlights; + uint dynlightmask; + + renderstate() : colormask(true), depthmask(true), blending(false), alphaing(0), vbuf(0), vattribs(false), vquery(false), colorscale(1, 1, 1), alphascale(0), slot(NULL), texgenslot(NULL), vslot(NULL), texgenvslot(NULL), texgenscroll(0, 0), texgendim(-1), visibledynlights(0), dynlightmask(0) + { + loopk(8) textures[k] = 0; + } +}; + +static inline void disablevbuf(renderstate &cur) +{ + gle::clearvbo(); + gle::clearebo(); + cur.vbuf = 0; +} + +static inline void enablevquery(renderstate &cur) +{ + if(cur.colormask) { cur.colormask = false; glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); } + if(cur.depthmask) { cur.depthmask = false; glDepthMask(GL_FALSE); } + startbb(false); + cur.vquery = true; +} + +static inline void disablevquery(renderstate &cur) +{ + endbb(false); + cur.vquery = false; +} + +static void renderquery(renderstate &cur, occludequery *query, vtxarray *va, bool full = true) +{ + if(!cur.vquery) enablevquery(cur); + + startquery(query); + + if(full) drawbb(ivec(va->bbmin).sub(1), ivec(va->bbmax).sub(va->bbmin).add(2)); + else drawbb(va->geommin, ivec(va->geommax).sub(va->geommin)); + + endquery(query); +} + +enum +{ + RENDERPASS_LIGHTMAP = 0, + RENDERPASS_Z, + RENDERPASS_CAUSTICS, + RENDERPASS_FOG, + RENDERPASS_LIGHTMAP_BLEND +}; + +struct geombatch +{ + const elementset &es; + VSlot &vslot; + ushort *edata; + vtxarray *va; + int next, batch; + + geombatch(const elementset &es, ushort *edata, vtxarray *va) + : es(es), vslot(lookupvslot(es.texture)), edata(edata), va(va), + next(-1), batch(-1) + {} + + int compare(const geombatch &b) const + { + if(va->vbuf < b.va->vbuf) return -1; + if(va->vbuf > b.va->vbuf) return 1; + if(va->dynlightmask < b.va->dynlightmask) return -1; + if(va->dynlightmask > b.va->dynlightmask) return 1; + if(vslot.slot->shader < b.vslot.slot->shader) return -1; + if(vslot.slot->shader > b.vslot.slot->shader) return 1; + if(vslot.slot->params.length() < b.vslot.slot->params.length()) return -1; + if(vslot.slot->params.length() > b.vslot.slot->params.length()) return 1; + if(es.texture < b.es.texture) return -1; + if(es.texture > b.es.texture) return 1; + if(es.lmid < b.es.lmid) return -1; + if(es.lmid > b.es.lmid) return 1; + if(es.envmap < b.es.envmap) return -1; + if(es.envmap > b.es.envmap) return 1; + if(es.dim < b.es.dim) return -1; + if(es.dim > b.es.dim) return 1; + return 0; + } +}; + +static vector geombatches; +static int firstbatch = -1, numbatches = 0; + +static void mergetexs(renderstate &cur, vtxarray *va, elementset *texs = NULL, int numtexs = 0, ushort *edata = NULL) +{ + if(!texs) + { + texs = va->eslist; + numtexs = va->texs; + edata = va->edata; + if(cur.alphaing) + { + texs += va->texs + va->blends; + edata += 3*(va->tris + va->blendtris); + numtexs = va->alphaback; + if(cur.alphaing > 1) numtexs += va->alphafront; + } + } + + if(firstbatch < 0) + { + firstbatch = geombatches.length(); + numbatches = numtexs; + loopi(numtexs-1) + { + geombatches.add(geombatch(texs[i], edata, va)).next = i+1; + edata += texs[i].length[1]; + } + geombatches.add(geombatch(texs[numtexs-1], edata, va)); + return; + } + + int prevbatch = -1, curbatch = firstbatch, curtex = 0; + do + { + geombatch &b = geombatches.add(geombatch(texs[curtex], edata, va)); + edata += texs[curtex].length[1]; + int dir = -1; + while(curbatch >= 0) + { + dir = b.compare(geombatches[curbatch]); + if(dir <= 0) break; + prevbatch = curbatch; + curbatch = geombatches[curbatch].next; + } + if(!dir) + { + int last = curbatch, next; + for(;;) + { + next = geombatches[last].batch; + if(next < 0) break; + last = next; + } + if(last==curbatch) + { + b.batch = curbatch; + b.next = geombatches[curbatch].next; + if(prevbatch < 0) firstbatch = geombatches.length()-1; + else geombatches[prevbatch].next = geombatches.length()-1; + curbatch = geombatches.length()-1; + } + else + { + b.batch = next; + geombatches[last].batch = geombatches.length()-1; + } + } + else + { + numbatches++; + b.next = curbatch; + if(prevbatch < 0) firstbatch = geombatches.length()-1; + else geombatches[prevbatch].next = geombatches.length()-1; + prevbatch = geombatches.length()-1; + } + } + while(++curtex < numtexs); +} + +static inline void enablevattribs(renderstate &cur, bool all = true) +{ + gle::enablevertex(); + if(all) + { + gle::enabletexcoord0(); + gle::enabletexcoord1(); + gle::enablenormal(); + gle::enabletangent(); + } + cur.vattribs = true; +} + +static inline void disablevattribs(renderstate &cur, bool all = true) +{ + gle::disablevertex(); + if(all) + { + gle::disabletexcoord0(); + gle::disabletexcoord1(); + gle::disablenormal(); + gle::disabletangent(); + } + cur.vattribs = false; +} + +static void changevbuf(renderstate &cur, int pass, vtxarray *va) +{ + gle::bindvbo(va->vbuf); + gle::bindebo(va->ebuf); + cur.vbuf = va->vbuf; + + vertex *vdata = (vertex *)0; + gle::vertexpointer(sizeof(vertex), vdata->pos.v); + + if(pass==RENDERPASS_LIGHTMAP) + { + gle::normalpointer(sizeof(vertex), vdata->norm.v, GL_BYTE); + gle::texcoord0pointer(sizeof(vertex), vdata->tc.v); + gle::texcoord1pointer(sizeof(vertex), vdata->lm.v, GL_SHORT); + gle::tangentpointer(sizeof(vertex), vdata->tangent.v, GL_BYTE); + } +} + +static void changebatchtmus(renderstate &cur, int pass, geombatch &b) +{ + bool changed = false; + extern bool brightengeom; + extern int fullbright; + int lmid = brightengeom && (b.es.lmid < LMID_RESERVED || (fullbright && editmode)) ? LMID_BRIGHT : b.es.lmid; + if(cur.textures[1]!=lightmaptexs[lmid].id) + { + glActiveTexture_(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, cur.textures[1] = lightmaptexs[lmid].id); + changed = true; + } + int tmu = 2; + if(b.vslot.slot->shader->type&SHADER_NORMALSLMS) + { + if(cur.textures[tmu]!=lightmaptexs[lmid+1].id) + { + glActiveTexture_(GL_TEXTURE0+tmu); + glBindTexture(GL_TEXTURE_2D, cur.textures[tmu] = lightmaptexs[lmid+1].id); + changed = true; + } + tmu++; + } + if(b.vslot.slot->shader->type&SHADER_ENVMAP && b.es.envmap!=EMID_CUSTOM) + { + GLuint emtex = lookupenvmap(b.es.envmap); + if(cur.textures[tmu]!=emtex) + { + glActiveTexture_(GL_TEXTURE0+tmu); + glBindTexture(GL_TEXTURE_CUBE_MAP, cur.textures[tmu] = emtex); + changed = true; + } + } + if(changed) glActiveTexture_(GL_TEXTURE0); + + if(cur.dynlightmask != b.va->dynlightmask) + { + cur.visibledynlights = setdynlights(b.va); + cur.dynlightmask = b.va->dynlightmask; + } +} + +static void changeslottmus(renderstate &cur, int pass, Slot &slot, VSlot &vslot) +{ + if(pass==RENDERPASS_LIGHTMAP) + { + GLuint diffusetex = slot.sts.empty() ? notexture->id : slot.sts[0].t->id; + if(cur.textures[0]!=diffusetex) + glBindTexture(GL_TEXTURE_2D, cur.textures[0] = diffusetex); + } + + if(cur.alphaing) + { + float alpha = cur.alphaing > 1 ? vslot.alphafront : vslot.alphaback; + if(cur.colorscale != vslot.colorscale || cur.alphascale != alpha) + { + cur.colorscale = vslot.colorscale; + cur.alphascale = alpha; + GLOBALPARAMF(colorparams, 2*alpha*vslot.colorscale.x, 2*alpha*vslot.colorscale.y, 2*alpha*vslot.colorscale.z, alpha); + setfogcolor(vec(curfogcolor).mul(alpha)); + } + } + else if(cur.colorscale != vslot.colorscale) + { + cur.colorscale = vslot.colorscale; + GLOBALPARAMF(colorparams, 2*vslot.colorscale.x, 2*vslot.colorscale.y, 2*vslot.colorscale.z, 1); + } + int tmu = 2, envmaptmu = -1; + if(slot.shader->type&SHADER_NORMALSLMS) tmu++; + if(slot.shader->type&SHADER_ENVMAP) envmaptmu = tmu++; + loopvj(slot.sts) + { + Slot::Tex &t = slot.sts[j]; + if(t.type==TEX_DIFFUSE || t.combined>=0) continue; + if(t.type==TEX_ENVMAP) + { + if(envmaptmu>=0 && t.t && cur.textures[envmaptmu]!=t.t->id) + { + glActiveTexture_(GL_TEXTURE0+envmaptmu); + glBindTexture(GL_TEXTURE_CUBE_MAP, cur.textures[envmaptmu] = t.t->id); + } + } + else + { + if(cur.textures[tmu]!=t.t->id) + { + glActiveTexture_(GL_TEXTURE0+tmu); + glBindTexture(GL_TEXTURE_2D, cur.textures[tmu] = t.t->id); + } + if(++tmu >= 8) break; + } + } + glActiveTexture_(GL_TEXTURE0); + + cur.slot = &slot; + cur.vslot = &vslot; +} + +static void changeshader(renderstate &cur, Shader *s, Slot &slot, VSlot &vslot, bool shadowed) +{ + if(glaring) + { + static Shader *noglareshader = NULL, *noglareblendshader = NULL, *noglarealphashader = NULL; + Shader *fallback; + if(cur.blending) { if(!noglareblendshader) noglareblendshader = lookupshaderbyname("noglareblendworld"); fallback = noglareblendshader; } + else if(cur.alphaing) { if(!noglarealphashader) noglarealphashader = lookupshaderbyname("noglarealphaworld"); fallback = noglarealphashader; } + else { if(!noglareshader) noglareshader = lookupshaderbyname("noglareworld"); fallback = noglareshader; } + if(s->hasoption(4)) s->setvariant(cur.visibledynlights, 4, slot, vslot, fallback); + else s->setvariant(cur.blending ? 1 : 0, 4, slot, vslot, fallback); + } + else if(fading && !cur.blending && !cur.alphaing) + { + if(shadowed) s->setvariant(cur.visibledynlights, 3, slot, vslot); + else s->setvariant(cur.visibledynlights, 2, slot, vslot); + } + else if(shadowed) s->setvariant(cur.visibledynlights, 1, slot, vslot); + else if(!cur.visibledynlights) s->set(slot, vslot); + else s->setvariant(cur.visibledynlights-1, 0, slot, vslot); +} + +static void changetexgen(renderstate &cur, int dim, Slot &slot, VSlot &vslot) +{ + if(cur.texgenslot != &slot || cur.texgenvslot != &vslot) + { + Texture *curtex = !cur.texgenslot || cur.texgenslot->sts.empty() ? notexture : cur.texgenslot->sts[0].t, + *tex = slot.sts.empty() ? notexture : slot.sts[0].t; + if(!cur.texgenvslot || slot.sts.empty() || + (curtex->xs != tex->xs || curtex->ys != tex->ys || + cur.texgenvslot->rotation != vslot.rotation || cur.texgenvslot->scale != vslot.scale || + cur.texgenvslot->offset != vslot.offset || cur.texgenvslot->scroll != vslot.scroll)) + { + const texrotation &r = texrotations[vslot.rotation]; + float xs = r.flipx ? -tex->xs : tex->xs, + ys = r.flipy ? -tex->ys : tex->ys; + vec2 scroll(vslot.scroll); + if(r.swapxy) swap(scroll.x, scroll.y); + scroll.x *= lastmillis*tex->xs/xs; + scroll.y *= lastmillis*tex->ys/ys; + if(cur.texgenscroll != scroll) + { + cur.texgenscroll = scroll; + cur.texgendim = -1; + } + } + cur.texgenslot = &slot; + cur.texgenvslot = &vslot; + } + + if(cur.texgendim == dim) return; + GLOBALPARAM(texgenscroll, cur.texgenscroll); + cur.texgendim = dim; +} + +static void renderbatch(renderstate &cur, int pass, geombatch &b) +{ + geombatch *shadowed = NULL; + int rendered = -1; + for(geombatch *curbatch = &b;; curbatch = &geombatches[curbatch->batch]) + { + ushort len = curbatch->es.length[curbatch->va->shadowed ? 0 : 1]; + if(len) + { + if(rendered < 0) + { + changeshader(cur, b.vslot.slot->shader, *b.vslot.slot, b.vslot, false); + rendered = 0; + gbatches++; + } + ushort minvert = curbatch->es.minvert[0], maxvert = curbatch->es.maxvert[0]; + if(!curbatch->va->shadowed) { minvert = min(minvert, curbatch->es.minvert[1]); maxvert = max(maxvert, curbatch->es.maxvert[1]); } + drawtris(len, curbatch->edata, minvert, maxvert); + vtris += len/3; + } + if(curbatch->es.length[1] > len && !shadowed) shadowed = curbatch; + if(curbatch->batch < 0) break; + } + if(shadowed) for(geombatch *curbatch = shadowed;; curbatch = &geombatches[curbatch->batch]) + { + if(curbatch->va->shadowed && curbatch->es.length[1] > curbatch->es.length[0]) + { + if(rendered < 1) + { + changeshader(cur, b.vslot.slot->shader, *b.vslot.slot, b.vslot, true); + rendered = 1; + gbatches++; + } + ushort len = curbatch->es.length[1] - curbatch->es.length[0]; + drawtris(len, curbatch->edata + curbatch->es.length[0], curbatch->es.minvert[1], curbatch->es.maxvert[1]); + vtris += len/3; + } + if(curbatch->batch < 0) break; + } +} + +static void resetbatches() +{ + geombatches.setsize(0); + firstbatch = -1; + numbatches = 0; +} + +static void renderbatches(renderstate &cur, int pass) +{ + cur.slot = NULL; + cur.vslot = NULL; + int curbatch = firstbatch; + if(curbatch >= 0) + { + if(cur.alphaing) + { + if(cur.depthmask) { cur.depthmask = false; glDepthMask(GL_FALSE); } + } + else if(!cur.depthmask) { cur.depthmask = true; glDepthMask(GL_TRUE); } + if(!cur.colormask) { cur.colormask = true; glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, cur.alphaing ? GL_FALSE : GL_TRUE); } + if(!cur.vattribs) + { + if(cur.vquery) disablevquery(cur); + enablevattribs(cur); + } + } + while(curbatch >= 0) + { + geombatch &b = geombatches[curbatch]; + curbatch = b.next; + + if(cur.vbuf != b.va->vbuf) changevbuf(cur, pass, b.va); + if(cur.vslot != &b.vslot) + { + changeslottmus(cur, pass, *b.vslot.slot, b.vslot); + if(cur.texgendim != b.es.dim || (cur.texgendim <= 2 && cur.texgenvslot != &b.vslot)) changetexgen(cur, b.es.dim, *b.vslot.slot, b.vslot); + } + else if(cur.texgendim != b.es.dim) changetexgen(cur, b.es.dim, *b.vslot.slot, b.vslot); + if(pass == RENDERPASS_LIGHTMAP) changebatchtmus(cur, pass, b); + + renderbatch(cur, pass, b); + } + + resetbatches(); +} + +void renderzpass(renderstate &cur, vtxarray *va) +{ + if(!cur.vattribs) + { + if(cur.vquery) disablevquery(cur); + enablevattribs(cur, false); + } + if(cur.vbuf!=va->vbuf) changevbuf(cur, RENDERPASS_Z, va); + if(!cur.depthmask) { cur.depthmask = true; glDepthMask(GL_TRUE); } + if(cur.colormask) { cur.colormask = false; glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); } + int firsttex = 0, numtris = va->tris; + ushort *edata = va->edata; + if(cur.alphaing) + { + firsttex += va->texs + va->blends; + edata += 3*(va->tris + va->blendtris); + numtris = va->alphatris; + xtravertsva += 3*numtris; + } + else xtravertsva += va->verts; + nocolorshader->set(); + drawvatris(va, 3*numtris, edata); +} + +vector foggedvas; + +#define startvaquery(va, flush) \ + do { \ + if(va->query) \ + { \ + flush; \ + startquery(va->query); \ + } \ + } while(0) + + +#define endvaquery(va, flush) \ + do { \ + if(va->query) \ + { \ + flush; \ + endquery(va->query); \ + } \ + } while(0) + +void renderfoggedvas(renderstate &cur, bool doquery = false) +{ + static Shader *fogshader = NULL; + if(!fogshader) fogshader = lookupshaderbyname("fogworld"); + if(fading) fogshader->setvariant(0, 2); + else fogshader->set(); + + if(!cur.vattribs) enablevattribs(cur, false); + + loopv(foggedvas) + { + vtxarray *va = foggedvas[i]; + if(cur.vbuf!=va->vbuf) changevbuf(cur, RENDERPASS_FOG, va); + + if(doquery) startvaquery(va, ); + drawvatris(va, 3*va->tris, va->edata); + vtris += va->tris; + if(doquery) endvaquery(va, ); + } + + foggedvas.setsize(0); +} + +VAR(batchgeom, 0, 1, 1); + +void renderva(renderstate &cur, vtxarray *va, int pass = RENDERPASS_LIGHTMAP, bool fogpass = false, bool doquery = false) +{ + switch(pass) + { + case RENDERPASS_LIGHTMAP: + if(!cur.alphaing) vverts += va->verts; + va->shadowed = false; + va->dynlightmask = 0; + if(fogpass ? va->geommax.z<=reflectz-refractfog || !refractfog : va->curvfc==VFC_FOGGED) + { + if(!cur.alphaing && !cur.blending) foggedvas.add(va); + break; + } + if(!drawtex && !glaring && !cur.alphaing) + { + va->shadowed = isshadowmapreceiver(va); + calcdynlightmask(va); + } + if(doquery) startvaquery(va, { if(geombatches.length()) renderbatches(cur, pass); }); + mergetexs(cur, va); + if(doquery) endvaquery(va, { if(geombatches.length()) renderbatches(cur, pass); }); + else if(!batchgeom && geombatches.length()) renderbatches(cur, pass); + break; + + case RENDERPASS_LIGHTMAP_BLEND: + { + if(doquery) startvaquery(va, { if(geombatches.length()) renderbatches(cur, RENDERPASS_LIGHTMAP); }); + mergetexs(cur, va, &va->eslist[va->texs], va->blends, va->edata + 3*va->tris); + if(doquery) endvaquery(va, { if(geombatches.length()) renderbatches(cur, RENDERPASS_LIGHTMAP); }); + else if(!batchgeom && geombatches.length()) renderbatches(cur, RENDERPASS_LIGHTMAP); + break; + } + + case RENDERPASS_FOG: + if(cur.vbuf!=va->vbuf) changevbuf(cur, pass, va); + drawvatris(va, 3*va->tris, va->edata); + xtravertsva += va->verts; + break; + + case RENDERPASS_CAUSTICS: + if(cur.vbuf!=va->vbuf) changevbuf(cur, pass, va); + drawvatris(va, 3*va->tris, va->edata); + xtravertsva += va->verts; + break; + + case RENDERPASS_Z: + if(doquery) startvaquery(va, ); + renderzpass(cur, va); + if(doquery) endvaquery(va, ); + break; + } +} + +#define NUMCAUSTICS 32 + +static Texture *caustictex[NUMCAUSTICS] = { NULL }; + +void loadcaustics(bool force) +{ + static bool needcaustics = false; + if(force) needcaustics = true; + if(!caustics || !needcaustics) return; + useshaderbyname("caustic"); + if(caustictex[0]) return; + loopi(NUMCAUSTICS) + { + defformatstring(name, "packages/caustics/caust%.2d.png", i); + caustictex[i] = textureload(name); + } +} + +void cleanupva() +{ + clearvas(worldroot); + clearqueries(); + cleanupbb(); + cleanupgrass(); + loopi(NUMCAUSTICS) caustictex[i] = NULL; +} + +VARR(causticscale, 0, 50, 10000); +VARR(causticmillis, 0, 75, 1000); +FVARR(causticcontrast, 0, 0.6f, 1); +VARFP(caustics, 0, 1, 1, loadcaustics()); + +void setupcaustics(float blend) +{ + if(!caustictex[0]) loadcaustics(true); + + vec s = vec(0.011f, 0, 0.0066f).mul(100.0f/causticscale), t = vec(0, 0.011f, 0.0066f).mul(100.0f/causticscale); + int tex = (lastmillis/causticmillis)%NUMCAUSTICS; + float frac = float(lastmillis%causticmillis)/causticmillis; + loopi(2) + { + glActiveTexture_(GL_TEXTURE0+i); + glBindTexture(GL_TEXTURE_2D, caustictex[(tex+i)%NUMCAUSTICS]->id); + } + glActiveTexture_(GL_TEXTURE0); + SETSHADER(caustic); + LOCALPARAM(texgenS, s); + LOCALPARAM(texgenT, t); + blend *= causticcontrast; + LOCALPARAMF(frameblend, blend*(1-frac), blend*frac, blend, 1 - blend); +} + +void setupgeom(renderstate &cur) +{ + GLOBALPARAMF(colorparams, 2, 2, 2, 1); + GLOBALPARAM(camera, camera1->o); + GLOBALPARAMF(ambient, ambientcolor.x/255.0f, ambientcolor.y/255.0f, ambientcolor.z/255.0f); + GLOBALPARAMF(millis, lastmillis/1000.0f); + + glActiveTexture_(GL_TEXTURE0); +} + +void cleanupgeom(renderstate &cur) +{ + if(cur.vattribs) disablevattribs(cur); + if(cur.vbuf) disablevbuf(cur); +} + +#define FIRSTVA (reflecting ? reflectedva : visibleva) +#define NEXTVA (reflecting ? va->rnext : va->next) + +static void rendergeommultipass(renderstate &cur, int pass, bool fogpass) +{ + if(cur.vbuf) disablevbuf(cur); + if(!cur.vattribs) enablevattribs(cur, false); + cur.texgendim = -1; + for(vtxarray *va = FIRSTVA; va; va = NEXTVA) + { + if(!va->texs) continue; + if(refracting) + { + if((refracting < 0 ? va->geommin.z > reflectz : va->geommax.z <= reflectz) || va->occluded >= OCCLUDE_GEOM) continue; + if(ishiddencube(va->o, va->size)) continue; + } + else if(reflecting) + { + if(va->geommax.z <= reflectz) continue; + } + else if(va->occluded >= OCCLUDE_GEOM) continue; + if(fogpass ? va->geommax.z <= reflectz-refractfog || !refractfog : va->curvfc==VFC_FOGGED) continue; + renderva(cur, va, pass, fogpass); + } + if(geombatches.length()) renderbatches(cur, pass); +} + +VAR(oqgeom, 0, 1, 1); + +void rendergeom(float causticspass, bool fogpass) +{ + if(causticspass && (!causticscale || !causticmillis)) causticspass = 0; + + bool mainpass = !reflecting && !refracting && !drawtex && !glaring, + doOQ = oqfrags && oqgeom && mainpass, + doZP = doOQ && zpass, + doSM = shadowmap && !drawtex && !glaring; + renderstate cur; + if(mainpass) + { + flipqueries(); + vtris = vverts = 0; + } + if(!doZP) + { + if(shadowmap && mainpass) rendershadowmap(); + setupgeom(cur); + if(doSM) pushshadowmap(); + } + + finddynlights(); + + resetbatches(); + + int blends = 0; + for(vtxarray *va = FIRSTVA; va; va = NEXTVA) + { + if(!va->texs) continue; + if(refracting) + { + if((refracting < 0 ? va->geommin.z > reflectz : va->geommax.z <= reflectz) || va->occluded >= OCCLUDE_GEOM) continue; + if(ishiddencube(va->o, va->size)) continue; + } + else if(reflecting) + { + if(va->geommax.z <= reflectz) continue; + } + else if(doOQ && (zpass || va->distance > oqdist) && !insideva(va, camera1->o)) + { + if(va->parent && va->parent->occluded >= OCCLUDE_BB) + { + va->query = NULL; + va->occluded = OCCLUDE_PARENT; + continue; + } + va->occluded = va->query && va->query->owner == va && checkquery(va->query) ? min(va->occluded+1, int(OCCLUDE_BB)) : OCCLUDE_NOTHING; + va->query = newquery(va); + if((!va->query && zpass) || !va->occluded) + va->occluded = pvsoccluded(va->geommin, va->geommax) ? OCCLUDE_GEOM : OCCLUDE_NOTHING; + if(va->occluded >= OCCLUDE_GEOM) + { + if(va->query) + { + if(!zpass && geombatches.length()) renderbatches(cur, RENDERPASS_LIGHTMAP); + if(cur.vattribs) disablevattribs(cur, !doZP); + if(cur.vbuf) disablevbuf(cur); + renderquery(cur, va->query, va); + } + continue; + } + } + else + { + va->query = NULL; + va->occluded = pvsoccluded(va->geommin, va->geommax) ? OCCLUDE_GEOM : OCCLUDE_NOTHING; + if(va->occluded >= OCCLUDE_GEOM) continue; + } + + if(!doZP) blends += va->blends; + renderva(cur, va, doZP ? RENDERPASS_Z : RENDERPASS_LIGHTMAP, fogpass, doOQ); + } + + if(geombatches.length()) renderbatches(cur, RENDERPASS_LIGHTMAP); + + if(cur.vquery) disablevquery(cur); + if(cur.vattribs) disablevattribs(cur, !doZP); + if(cur.vbuf) disablevbuf(cur); + + if(!cur.colormask) { cur.colormask = true; glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); } + if(!cur.depthmask) { cur.depthmask = true; glDepthMask(GL_TRUE); } + + bool multipassing = false; + + if(doZP) + { + glFlush(); + + if(shadowmap && mainpass) rendershadowmap(); + setupgeom(cur); + if(doSM) pushshadowmap(); + + if(!multipassing) { multipassing = true; glDepthFunc(GL_LEQUAL); } + cur.texgendim = -1; + + for(vtxarray *va = visibleva; va; va = va->next) + { + if(!va->texs || va->occluded >= OCCLUDE_GEOM) continue; + blends += va->blends; + renderva(cur, va, RENDERPASS_LIGHTMAP, fogpass); + } + if(geombatches.length()) renderbatches(cur, RENDERPASS_LIGHTMAP); + for(vtxarray *va = visibleva; va; va = va->next) + { + if(!va->texs || va->occluded < OCCLUDE_GEOM) continue; + else if((va->parent && va->parent->occluded >= OCCLUDE_BB) || + (va->query && checkquery(va->query))) + { + va->occluded = OCCLUDE_BB; + continue; + } + else + { + va->occluded = pvsoccluded(va->geommin, va->geommax) ? OCCLUDE_GEOM : OCCLUDE_NOTHING; + if(va->occluded >= OCCLUDE_GEOM) continue; + } + + blends += va->blends; + renderva(cur, va, RENDERPASS_LIGHTMAP, fogpass); + } + if(geombatches.length()) renderbatches(cur, RENDERPASS_LIGHTMAP); + } + + if(blends) + { + if(cur.vbuf) disablevbuf(cur); + + if(!multipassing) { multipassing = true; glDepthFunc(GL_LEQUAL); } + glDepthMask(GL_FALSE); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE); + + cur.texgendim = -1; + cur.blending = true; + for(vtxarray *va = FIRSTVA; va; va = NEXTVA) + { + if(!va->blends) continue; + if(refracting) + { + if(refracting < 0 ? va->geommin.z > reflectz : va->geommax.z <= reflectz) continue; + if(ishiddencube(va->o, va->size)) continue; + if(va->occluded >= OCCLUDE_GEOM) continue; + } + else if(reflecting) + { + if(va->geommax.z <= reflectz) continue; + } + else if(va->occluded >= OCCLUDE_GEOM) continue; + if(fogpass ? va->geommax.z <= reflectz-refractfog || !refractfog : va->curvfc==VFC_FOGGED) continue; + renderva(cur, va, RENDERPASS_LIGHTMAP_BLEND, fogpass); + } + if(geombatches.length()) renderbatches(cur, RENDERPASS_LIGHTMAP); + cur.blending = false; + + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + glDisable(GL_BLEND); + glDepthMask(GL_TRUE); + } + + if(doSM) popshadowmap(); + + if(cur.vattribs) disablevattribs(cur); + + if(foggedvas.length()) renderfoggedvas(cur, doOQ && !zpass); + + if(causticspass) + { + if(!multipassing) { multipassing = true; glDepthFunc(GL_LEQUAL); } + glDepthMask(GL_FALSE); + glEnable(GL_BLEND); + + setupcaustics(causticspass); + glBlendFunc(GL_ZERO, GL_SRC_COLOR); + if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE); + rendergeommultipass(cur, RENDERPASS_CAUSTICS, fogpass); + if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + + glDisable(GL_BLEND); + glDepthMask(GL_TRUE); + } + + if(multipassing) glDepthFunc(GL_LESS); + + cleanupgeom(cur); +} + +void renderalphageom(bool fogpass) +{ + static vector alphavas; + alphavas.setsize(0); + bool hasback = false; + for(vtxarray *va = FIRSTVA; va; va = NEXTVA) + { + if(!va->alphatris) continue; + if(refracting) + { + if((refracting < 0 ? va->geommin.z > reflectz : va->geommax.z <= reflectz) || va->occluded >= OCCLUDE_BB) continue; + if(ishiddencube(va->o, va->size)) continue; + if(va->occluded >= OCCLUDE_GEOM && pvsoccluded(va->geommin, va->geommax)) continue; + } + else if(reflecting) + { + if(va->geommax.z <= reflectz) continue; + } + else + { + if(va->occluded >= OCCLUDE_BB) continue; + if(va->occluded >= OCCLUDE_GEOM && pvsoccluded(va->geommin, va->geommax)) continue; + } + if(fogpass ? va->geommax.z <= reflectz-refractfog || !refractfog : va->curvfc==VFC_FOGGED) continue; + alphavas.add(va); + if(va->alphabacktris) hasback = true; + } + if(alphavas.empty()) return; + + resetbatches(); + + renderstate cur; + cur.alphaing = 1; + + loop(front, 2) if(front || hasback) + { + cur.alphaing = front+1; + if(!front) glCullFace(GL_FRONT); + cur.vbuf = 0; + cur.texgendim = -1; + loopv(alphavas) renderva(cur, alphavas[i], RENDERPASS_Z); + if(cur.depthmask) { cur.depthmask = false; glDepthMask(GL_FALSE); } + cur.colormask = true; + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE); + + if(cur.vattribs) disablevattribs(cur, false); + if(cur.vbuf) disablevbuf(cur); + + setupgeom(cur); + + glDepthFunc(GL_LEQUAL); + glEnable(GL_BLEND); + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + cur.vbuf = 0; + cur.texgendim = -1; + cur.colorscale = vec(1, 1, 1); + cur.alphascale = -1; + loopv(alphavas) if(front || alphavas[i]->alphabacktris) renderva(cur, alphavas[i], RENDERPASS_LIGHTMAP, fogpass); + if(geombatches.length()) renderbatches(cur, RENDERPASS_LIGHTMAP); + + cleanupgeom(cur); + + resetfogcolor(); + if(!cur.depthmask) { cur.depthmask = true; glDepthMask(GL_TRUE); } + glDisable(GL_BLEND); + glDepthFunc(GL_LESS); + if(!front) glCullFace(GL_BACK); + } + + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, fading ? GL_FALSE : GL_TRUE); +} + +void findreflectedvas(vector &vas, int prevvfc = VFC_PART_VISIBLE) +{ + loopv(vas) + { + vtxarray *va = vas[i]; + if(prevvfc >= VFC_NOT_VISIBLE) va->curvfc = prevvfc; + if(va->curvfc == VFC_FOGGED || va->curvfc == PVS_FOGGED || va->o.z+va->size <= reflectz || isfoggedcube(va->o, va->size)) continue; + bool render = true; + if(va->curvfc == VFC_FULL_VISIBLE) + { + if(va->occluded >= OCCLUDE_BB) continue; + if(va->occluded >= OCCLUDE_GEOM) render = false; + } + else if(va->curvfc == PVS_FULL_VISIBLE) continue; + if(render) + { + if(va->curvfc >= VFC_NOT_VISIBLE) va->distance = (int)vadist(va, camera1->o); + vtxarray **vprev = &reflectedva, *vcur = reflectedva; + while(vcur && va->distance > vcur->distance) + { + vprev = &vcur->rnext; + vcur = vcur->rnext; + } + va->rnext = *vprev; + *vprev = va; + } + if(va->children.length()) findreflectedvas(va->children, va->curvfc); + } +} + +void renderreflectedgeom(bool causticspass, bool fogpass) +{ + if(reflecting) + { + reflectedva = NULL; + findreflectedvas(varoot); + rendergeom(causticspass ? 1 : 0, fogpass); + } + else rendergeom(causticspass ? 1 : 0, fogpass); +} + +static vtxarray *prevskyva = NULL; + +void renderskyva(vtxarray *va, bool explicitonly = false) +{ + if(!prevskyva || va->vbuf != prevskyva->vbuf) + { + gle::bindvbo(va->vbuf); + gle::bindebo(va->skybuf); + const vertex *ptr = 0; + gle::vertexpointer(sizeof(vertex), ptr->pos.v); + if(!prevskyva) gle::enablevertex(); + } + + drawvatris(va, explicitonly ? va->explicitsky : va->sky+va->explicitsky, explicitonly ? va->skydata+va->sky : va->skydata); + + if(!explicitonly) xtraverts += va->sky/3; + xtraverts += va->explicitsky/3; + + prevskyva = va; +} + +int renderedsky = 0, renderedexplicitsky = 0, renderedskyfaces = 0, renderedskyclip = INT_MAX; + +static inline void updateskystats(vtxarray *va) +{ + renderedsky += va->sky; + renderedexplicitsky += va->explicitsky; + renderedskyfaces |= va->skyfaces&0x3F; + if(!(va->skyfaces&0x1F) || camera1->o.z < va->skyclip) renderedskyclip = min(renderedskyclip, va->skyclip); + else renderedskyclip = 0; +} + +void renderreflectedskyvas(vector &vas, int prevvfc = VFC_PART_VISIBLE) +{ + loopv(vas) + { + vtxarray *va = vas[i]; + if(prevvfc >= VFC_NOT_VISIBLE) va->curvfc = prevvfc; + if((va->curvfc == VFC_FULL_VISIBLE && va->occluded >= OCCLUDE_BB) || va->curvfc==PVS_FULL_VISIBLE) continue; + if(va->o.z+va->size <= reflectz || ishiddencube(va->o, va->size)) continue; + if(va->sky+va->explicitsky) + { + updateskystats(va); + renderskyva(va); + } + if(va->children.length()) renderreflectedskyvas(va->children, va->curvfc); + } +} + +bool rendersky(bool explicitonly) +{ + prevskyva = NULL; + renderedsky = renderedexplicitsky = renderedskyfaces = 0; + renderedskyclip = INT_MAX; + + if(reflecting) + { + renderreflectedskyvas(varoot); + } + else for(vtxarray *va = visibleva; va; va = va->next) + { + if((va->occluded >= OCCLUDE_BB && va->skyfaces&0x80) || !(va->sky+va->explicitsky)) continue; + + // count possibly visible sky even if not actually rendered + updateskystats(va); + if(explicitonly && !va->explicitsky) continue; + renderskyva(va, explicitonly); + } + + if(prevskyva) + { + gle::disablevertex(); + gle::clearvbo(); + gle::clearebo(); + } + + return renderedsky+renderedexplicitsky > 0; +} + diff --git a/src/engine/server.cpp b/src/engine/server.cpp new file mode 100644 index 0000000..e76c67d --- /dev/null +++ b/src/engine/server.cpp @@ -0,0 +1,1154 @@ +// server.cpp: little more than enhanced multicaster +// runs dedicated or as client coroutine + +#include "engine.h" + +#define LOGSTRLEN 512 + +static FILE *logfile = NULL; + +void closelogfile() +{ + if(logfile) + { + fclose(logfile); + logfile = NULL; + } +} + +FILE *getlogfile() +{ +#ifdef WIN32 + return logfile; +#else + return logfile ? logfile : stdout; +#endif +} + +void setlogfile(const char *fname) +{ + closelogfile(); + if(fname && fname[0]) + { + fname = findfile(fname, "w"); + if(fname) logfile = fopen(fname, "w"); + } + FILE *f = getlogfile(); + if(f) setvbuf(f, NULL, _IOLBF, BUFSIZ); +} + +void logoutf(const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + logoutfv(fmt, args); + va_end(args); +} + + +static void writelog(FILE *file, const char *buf) +{ + static uchar ubuf[512]; + size_t len = strlen(buf), carry = 0; + while(carry < len) + { + size_t numu = encodeutf8(ubuf, sizeof(ubuf)-1, &((const uchar *)buf)[carry], len - carry, &carry); + if(carry >= len) ubuf[numu++] = '\n'; + fwrite(ubuf, 1, numu, file); + } +} + +static void writelogv(FILE *file, const char *fmt, va_list args) +{ + static char buf[LOGSTRLEN]; + vformatstring(buf, fmt, args, sizeof(buf)); + writelog(file, buf); +} + +#ifdef STANDALONE +void fatal(const char *fmt, ...) +{ + void cleanupserver(); + cleanupserver(); + defvformatstring(msg,fmt,fmt); + if(logfile) logoutf("%s", msg); +#ifdef WIN32 + MessageBox(NULL, msg, "Cube 2: Sauerbraten fatal error", MB_OK|MB_SYSTEMMODAL); +#else + fprintf(stderr, "server error: %s\n", msg); +#endif + closelogfile(); + exit(EXIT_FAILURE); +} + +void conoutfv(int type, const char *fmt, va_list args) +{ + logoutfv(fmt, args); +} +#endif + +#define DEFAULTCLIENTS 8 + +enum { ST_EMPTY, ST_LOCAL, ST_TCPIP }; + +struct client // server side version of "dynent" type +{ + int type; + int num; + ENetPeer *peer; + string hostname; + void *info; +}; + +vector clients; + +ENetHost *serverhost = NULL; +int laststatus = 0; +ENetSocket pongsock = ENET_SOCKET_NULL, lansock = ENET_SOCKET_NULL; + +int localclients = 0, nonlocalclients = 0; + +bool hasnonlocalclients() { return nonlocalclients!=0; } +bool haslocalclients() { return localclients!=0; } + +client &addclient(int type) +{ + client *c = NULL; + loopv(clients) if(clients[i]->type==ST_EMPTY) + { + c = clients[i]; + break; + } + if(!c) + { + c = new client; + c->num = clients.length(); + clients.add(c); + } + c->info = server::newclientinfo(); + c->type = type; + switch(type) + { + case ST_TCPIP: nonlocalclients++; break; + case ST_LOCAL: localclients++; break; + } + return *c; +} + +void delclient(client *c) +{ + if(!c) return; + switch(c->type) + { + case ST_TCPIP: nonlocalclients--; if(c->peer) c->peer->data = NULL; break; + case ST_LOCAL: localclients--; break; + case ST_EMPTY: return; + } + c->type = ST_EMPTY; + c->peer = NULL; + if(c->info) + { + server::deleteclientinfo(c->info); + c->info = NULL; + } +} + +void cleanupserver() +{ + if(serverhost) enet_host_destroy(serverhost); + serverhost = NULL; + + if(pongsock != ENET_SOCKET_NULL) enet_socket_destroy(pongsock); + if(lansock != ENET_SOCKET_NULL) enet_socket_destroy(lansock); + pongsock = lansock = ENET_SOCKET_NULL; +} + +VARF(maxclients, 0, DEFAULTCLIENTS, MAXCLIENTS, { if(!maxclients) maxclients = DEFAULTCLIENTS; }); +VARF(maxdupclients, 0, 0, MAXCLIENTS, { if(serverhost) serverhost->duplicatePeers = maxdupclients ? maxdupclients : MAXCLIENTS; }); + +void process(ENetPacket *packet, int sender, int chan); +//void disconnect_client(int n, int reason); + +int getservermtu() { return serverhost ? serverhost->mtu : -1; } +void *getclientinfo(int i) { return !clients.inrange(i) || clients[i]->type==ST_EMPTY ? NULL : clients[i]->info; } +ENetPeer *getclientpeer(int i) { return clients.inrange(i) && clients[i]->type==ST_TCPIP ? clients[i]->peer : NULL; } +int getnumclients() { return clients.length(); } +uint getclientip(int n) { return clients.inrange(n) && clients[n]->type==ST_TCPIP ? clients[n]->peer->address.host : 0; } + +void sendpacket(int n, int chan, ENetPacket *packet, int exclude) +{ + if(n<0) + { + server::recordpacket(chan, packet->data, packet->dataLength); + loopv(clients) if(i!=exclude && server::allowbroadcast(i)) sendpacket(i, chan, packet); + return; + } + switch(clients[n]->type) + { + case ST_TCPIP: + { + enet_peer_send(clients[n]->peer, chan, packet); + break; + } + +#ifndef STANDALONE + case ST_LOCAL: + localservertoclient(chan, packet); + break; +#endif + } +} + +ENetPacket *sendf(int cn, int chan, const char *format, ...) +{ + int exclude = -1; + bool reliable = false; + if(*format=='r') { reliable = true; ++format; } + packetbuf p(MAXTRANS, reliable ? ENET_PACKET_FLAG_RELIABLE : 0); + va_list args; + va_start(args, format); + while(*format) switch(*format++) + { + case 'x': + exclude = va_arg(args, int); + break; + + case 'v': + { + int n = va_arg(args, int); + int *v = va_arg(args, int *); + loopi(n) putint(p, v[i]); + break; + } + + case 'i': + { + int n = isdigit(*format) ? *format++-'0' : 1; + loopi(n) putint(p, va_arg(args, int)); + break; + } + case 'f': + { + int n = isdigit(*format) ? *format++-'0' : 1; + loopi(n) putfloat(p, (float)va_arg(args, double)); + break; + } + case 's': sendstring(va_arg(args, const char *), p); break; + case 'm': + { + int n = va_arg(args, int); + p.put(va_arg(args, uchar *), n); + break; + } + } + va_end(args); + ENetPacket *packet = p.finalize(); + sendpacket(cn, chan, packet, exclude); + return packet->referenceCount > 0 ? packet : NULL; +} + +ENetPacket *sendfile(int cn, int chan, stream *file, const char *format, ...) +{ + if(cn < 0) + { +#ifdef STANDALONE + return NULL; +#endif + } + else if(!clients.inrange(cn)) return NULL; + + int len = (int)min(file->size(), stream::offset(INT_MAX)); + if(len <= 0 || len > 16<<20) return NULL; + + packetbuf p(MAXTRANS+len, ENET_PACKET_FLAG_RELIABLE); + va_list args; + va_start(args, format); + while(*format) switch(*format++) + { + case 'i': + { + int n = isdigit(*format) ? *format++-'0' : 1; + loopi(n) putint(p, va_arg(args, int)); + break; + } + case 's': sendstring(va_arg(args, const char *), p); break; + case 'l': putint(p, len); break; + } + va_end(args); + + file->seek(0, SEEK_SET); + file->read(p.subbuf(len).buf, len); + + ENetPacket *packet = p.finalize(); + if(cn >= 0) sendpacket(cn, chan, packet, -1); +#ifndef STANDALONE + else sendclientpacket(packet, chan); +#endif + return packet->referenceCount > 0 ? packet : NULL; +} + +const char *disconnectreason(int reason) +{ + switch(reason) + { + case DISC_EOP: return "end of packet"; + case DISC_LOCAL: return "server is in local mode"; + case DISC_KICK: return "kicked/banned"; + case DISC_MSGERR: return "message error"; + case DISC_IPBAN: return "ip is banned"; + case DISC_PRIVATE: return "server is in private mode"; + case DISC_MAXCLIENTS: return "server FULL"; + case DISC_TIMEOUT: return "connection timed out"; + case DISC_OVERFLOW: return "overflow"; + case DISC_PASSWORD: return "invalid password"; + default: return NULL; + } +} + +void disconnect_client(int n, int reason) +{ + if(!clients.inrange(n) || clients[n]->type!=ST_TCPIP) return; + enet_peer_disconnect(clients[n]->peer, reason); + server::clientdisconnect(n); + delclient(clients[n]); + const char *msg = disconnectreason(reason); + string s; + if(msg) formatstring(s, "client (%s) disconnected because: %s", clients[n]->hostname, msg); + else formatstring(s, "client (%s) disconnected", clients[n]->hostname); + logoutf("%s", s); + server::sendservmsg(s); +} + +void kicknonlocalclients(int reason) +{ + loopv(clients) if(clients[i]->type==ST_TCPIP) disconnect_client(i, reason); +} + +void process(ENetPacket *packet, int sender, int chan) // sender may be -1 +{ + packetbuf p(packet); + server::parsepacket(sender, chan, p); + if(p.overread()) { disconnect_client(sender, DISC_EOP); return; } +} + +void localclienttoserver(int chan, ENetPacket *packet) +{ + client *c = NULL; + loopv(clients) if(clients[i]->type==ST_LOCAL) { c = clients[i]; break; } + if(c) process(packet, c->num, chan); +} + +#ifdef STANDALONE +bool resolverwait(const char *name, ENetAddress *address) +{ + return enet_address_set_host(address, name) >= 0; +} + +int connectwithtimeout(ENetSocket sock, const char *hostname, const ENetAddress &remoteaddress) +{ + return enet_socket_connect(sock, &remoteaddress); +} +#endif + +ENetSocket mastersock = ENET_SOCKET_NULL; +ENetAddress masteraddress = { ENET_HOST_ANY, ENET_PORT_ANY }, serveraddress = { ENET_HOST_ANY, ENET_PORT_ANY }; +int lastupdatemaster = 0, lastconnectmaster = 0, masterconnecting = 0, masterconnected = 0; +vector masterout, masterin; +int masteroutpos = 0, masterinpos = 0; +VARN(updatemaster, allowupdatemaster, 0, 1, 1); + +void disconnectmaster() +{ + if(mastersock != ENET_SOCKET_NULL) + { + server::masterdisconnected(); + enet_socket_destroy(mastersock); + mastersock = ENET_SOCKET_NULL; + } + + masterout.setsize(0); + masterin.setsize(0); + masteroutpos = masterinpos = 0; + + masteraddress.host = ENET_HOST_ANY; + masteraddress.port = ENET_PORT_ANY; + + lastupdatemaster = masterconnecting = masterconnected = 0; +} + +SVARF(mastername, server::defaultmaster(), disconnectmaster()); +VARF(masterport, 1, server::masterport(), 0xFFFF, disconnectmaster()); + +ENetSocket connectmaster(bool wait) +{ + if(!mastername[0]) return ENET_SOCKET_NULL; + if(masteraddress.host == ENET_HOST_ANY) + { + if(isdedicatedserver()) logoutf("looking up %s...", mastername); + masteraddress.port = masterport; + if(!resolverwait(mastername, &masteraddress)) return ENET_SOCKET_NULL; + } + ENetSocket sock = enet_socket_create(ENET_SOCKET_TYPE_STREAM); + if(sock == ENET_SOCKET_NULL) + { + if(isdedicatedserver()) logoutf("could not open master server socket"); + return ENET_SOCKET_NULL; + } + if(wait || serveraddress.host == ENET_HOST_ANY || !enet_socket_bind(sock, &serveraddress)) + { + enet_socket_set_option(sock, ENET_SOCKOPT_NONBLOCK, 1); + if(wait) + { + if(!connectwithtimeout(sock, mastername, masteraddress)) return sock; + } + else if(!enet_socket_connect(sock, &masteraddress)) return sock; + } + enet_socket_destroy(sock); + if(isdedicatedserver()) logoutf("could not connect to master server"); + return ENET_SOCKET_NULL; +} + +bool requestmaster(const char *req) +{ + if(mastersock == ENET_SOCKET_NULL) + { + mastersock = connectmaster(false); + if(mastersock == ENET_SOCKET_NULL) return false; + lastconnectmaster = masterconnecting = totalmillis ? totalmillis : 1; + } + + if(masterout.length() >= 4096) return false; + + masterout.put(req, strlen(req)); + return true; +} + +bool requestmasterf(const char *fmt, ...) +{ + defvformatstring(req, fmt, fmt); + return requestmaster(req); +} + +void processmasterinput() +{ + if(masterinpos >= masterin.length()) return; + + char *input = &masterin[masterinpos], *end = (char *)memchr(input, '\n', masterin.length() - masterinpos); + while(end) + { + *end = '\0'; + + const char *args = input; + while(args < end && !iscubespace(*args)) args++; + int cmdlen = args - input; + while(args < end && iscubespace(*args)) args++; + + if(matchstring(input, cmdlen, "failreg")) + conoutf(CON_ERROR, "master server registration failed: %s", args); + else if(matchstring(input, cmdlen, "succreg")) + conoutf("master server registration succeeded"); + else server::processmasterinput(input, cmdlen, args); + + end++; + masterinpos = end - masterin.getbuf(); + input = end; + end = (char *)memchr(input, '\n', masterin.length() - masterinpos); + } + + if(masterinpos >= masterin.length()) + { + masterin.setsize(0); + masterinpos = 0; + } +} + +void flushmasteroutput() +{ + if(masterconnecting && totalmillis - masterconnecting >= 60000) + { + logoutf("could not connect to master server"); + disconnectmaster(); + } + if(masterout.empty() || !masterconnected) return; + + ENetBuffer buf; + buf.data = &masterout[masteroutpos]; + buf.dataLength = masterout.length() - masteroutpos; + int sent = enet_socket_send(mastersock, NULL, &buf, 1); + if(sent >= 0) + { + masteroutpos += sent; + if(masteroutpos >= masterout.length()) + { + masterout.setsize(0); + masteroutpos = 0; + } + } + else disconnectmaster(); +} + +void flushmasterinput() +{ + if(masterin.length() >= masterin.capacity()) + masterin.reserve(4096); + + ENetBuffer buf; + buf.data = masterin.getbuf() + masterin.length(); + buf.dataLength = masterin.capacity() - masterin.length(); + int recv = enet_socket_receive(mastersock, NULL, &buf, 1); + if(recv > 0) + { + masterin.advance(recv); + processmasterinput(); + } + else disconnectmaster(); +} + +static ENetAddress pongaddr; + +void sendserverinforeply(ucharbuf &p) +{ + ENetBuffer buf; + buf.data = p.buf; + buf.dataLength = p.length(); + enet_socket_send(pongsock, &pongaddr, &buf, 1); +} + +#define MAXPINGDATA 32 + +void checkserversockets() // reply all server info requests +{ + static ENetSocketSet readset, writeset; + ENET_SOCKETSET_EMPTY(readset); + ENET_SOCKETSET_EMPTY(writeset); + ENetSocket maxsock = pongsock; + ENET_SOCKETSET_ADD(readset, pongsock); + if(mastersock != ENET_SOCKET_NULL) + { + maxsock = max(maxsock, mastersock); + ENET_SOCKETSET_ADD(readset, mastersock); + if(!masterconnected) ENET_SOCKETSET_ADD(writeset, mastersock); + } + if(lansock != ENET_SOCKET_NULL) + { + maxsock = max(maxsock, lansock); + ENET_SOCKETSET_ADD(readset, lansock); + } + if(enet_socketset_select(maxsock, &readset, &writeset, 0) <= 0) return; + + ENetBuffer buf; + uchar pong[MAXTRANS]; + loopi(2) + { + ENetSocket sock = i ? lansock : pongsock; + if(sock == ENET_SOCKET_NULL || !ENET_SOCKETSET_CHECK(readset, sock)) continue; + + buf.data = pong; + buf.dataLength = sizeof(pong); + int len = enet_socket_receive(sock, &pongaddr, &buf, 1); + if(len < 0 || len > MAXPINGDATA) continue; + ucharbuf req(pong, len), p(pong, sizeof(pong)); + p.len += len; + server::serverinforeply(req, p); + } + + if(mastersock != ENET_SOCKET_NULL) + { + if(!masterconnected) + { + if(ENET_SOCKETSET_CHECK(readset, mastersock) || ENET_SOCKETSET_CHECK(writeset, mastersock)) + { + int error = 0; + if(enet_socket_get_option(mastersock, ENET_SOCKOPT_ERROR, &error) < 0 || error) + { + logoutf("could not connect to master server"); + disconnectmaster(); + } + else + { + masterconnecting = 0; + masterconnected = totalmillis ? totalmillis : 1; + server::masterconnected(); + } + } + } + if(mastersock != ENET_SOCKET_NULL && ENET_SOCKETSET_CHECK(readset, mastersock)) flushmasterinput(); + } +} + +VAR(serveruprate, 0, 0, INT_MAX); +SVAR(serverip, ""); +VARF(serverport, 0, server::serverport(), 0xFFFF-1, { if(!serverport) serverport = server::serverport(); }); + +#ifdef STANDALONE +int curtime = 0, lastmillis = 0, elapsedtime = 0, totalmillis = 0; +#endif + +void updatemasterserver() +{ + if(!masterconnected && lastconnectmaster && totalmillis-lastconnectmaster <= 5*60*1000) return; + if(mastername[0] && allowupdatemaster) requestmasterf("regserv %d\n", serverport); + lastupdatemaster = totalmillis ? totalmillis : 1; +} + +uint totalsecs = 0; + +void updatetime() +{ + static int lastsec = 0; + if(totalmillis - lastsec >= 1000) + { + int cursecs = (totalmillis - lastsec) / 1000; + totalsecs += cursecs; + lastsec += cursecs * 1000; + } +} + +void serverslice(bool dedicated, uint timeout) // main server update, called from main loop in sp, or from below in dedicated server +{ + if(!serverhost) + { + server::serverupdate(); + server::sendpackets(); + return; + } + + // below is network only + + if(dedicated) + { + int millis = (int)enet_time_get(); + elapsedtime = millis - totalmillis; + static int timeerr = 0; + int scaledtime = server::scaletime(elapsedtime) + timeerr; + curtime = scaledtime/100; + timeerr = scaledtime%100; + if(server::ispaused()) curtime = 0; + lastmillis += curtime; + totalmillis = millis; + updatetime(); + } + server::serverupdate(); + + flushmasteroutput(); + checkserversockets(); + + if(!lastupdatemaster || totalmillis-lastupdatemaster>60*60*1000) // send alive signal to masterserver every hour of uptime + updatemasterserver(); + + if(totalmillis-laststatus>60*1000) // display bandwidth stats, useful for server ops + { + laststatus = totalmillis; + if(nonlocalclients || serverhost->totalSentData || serverhost->totalReceivedData) logoutf("status: %d remote clients, %.1f send, %.1f rec (K/sec)", nonlocalclients, serverhost->totalSentData/60.0f/1024, serverhost->totalReceivedData/60.0f/1024); + serverhost->totalSentData = serverhost->totalReceivedData = 0; + } + + ENetEvent event; + bool serviced = false; + while(!serviced) + { + if(enet_host_check_events(serverhost, &event) <= 0) + { + if(enet_host_service(serverhost, &event, timeout) <= 0) break; + serviced = true; + } + switch(event.type) + { + case ENET_EVENT_TYPE_CONNECT: + { + client &c = addclient(ST_TCPIP); + c.peer = event.peer; + c.peer->data = &c; + string hn; + copystring(c.hostname, (enet_address_get_host_ip(&c.peer->address, hn, sizeof(hn))==0) ? hn : "unknown"); + logoutf("client connected (%s)", c.hostname); + int reason = server::clientconnect(c.num, c.peer->address.host); + if(reason) disconnect_client(c.num, reason); + break; + } + case ENET_EVENT_TYPE_RECEIVE: + { + client *c = (client *)event.peer->data; + if(c) process(event.packet, c->num, event.channelID); + if(event.packet->referenceCount==0) enet_packet_destroy(event.packet); + break; + } + case ENET_EVENT_TYPE_DISCONNECT: + { + client *c = (client *)event.peer->data; + if(!c) break; + logoutf("disconnected client (%s)", c->hostname); + server::clientdisconnect(c->num); + delclient(c); + break; + } + default: + break; + } + } + if(server::sendpackets()) enet_host_flush(serverhost); +} + +void flushserver(bool force) +{ + if(server::sendpackets(force) && serverhost) enet_host_flush(serverhost); +} + +#ifndef STANDALONE +void localdisconnect(bool cleanup) +{ + bool disconnected = false; + loopv(clients) if(clients[i]->type==ST_LOCAL) + { + server::localdisconnect(i); + delclient(clients[i]); + disconnected = true; + } + if(!disconnected) return; + game::gamedisconnect(cleanup); + mainmenu = 1; +} + +void localconnect() +{ + if(initing) return; + client &c = addclient(ST_LOCAL); + copystring(c.hostname, "local"); + game::gameconnect(false); + server::localconnect(c.num); +} +#endif + +#ifdef WIN32 +#include "shellapi.h" + +#define IDI_ICON1 1 + +static string apptip = ""; +static HINSTANCE appinstance = NULL; +static ATOM wndclass = 0; +static HWND appwindow = NULL, conwindow = NULL; +static HICON appicon = NULL; +static HMENU appmenu = NULL; +static HANDLE outhandle = NULL; +static const int MAXLOGLINES = 200; +struct logline { int len; char buf[LOGSTRLEN]; }; +static queue loglines; + +static void cleanupsystemtray() +{ + NOTIFYICONDATA nid; + memset(&nid, 0, sizeof(nid)); + nid.cbSize = sizeof(nid); + nid.hWnd = appwindow; + nid.uID = IDI_ICON1; + Shell_NotifyIcon(NIM_DELETE, &nid); +} + +static bool setupsystemtray(UINT uCallbackMessage) +{ + NOTIFYICONDATA nid; + memset(&nid, 0, sizeof(nid)); + nid.cbSize = sizeof(nid); + nid.hWnd = appwindow; + nid.uID = IDI_ICON1; + nid.uCallbackMessage = uCallbackMessage; + nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP; + nid.hIcon = appicon; + strcpy(nid.szTip, apptip); + if(Shell_NotifyIcon(NIM_ADD, &nid) != TRUE) + return false; + atexit(cleanupsystemtray); + return true; +} + +#if 0 +static bool modifysystemtray() +{ + NOTIFYICONDATA nid; + memset(&nid, 0, sizeof(nid)); + nid.cbSize = sizeof(nid); + nid.hWnd = appwindow; + nid.uID = IDI_ICON1; + nid.uFlags = NIF_TIP; + strcpy(nid.szTip, apptip); + return Shell_NotifyIcon(NIM_MODIFY, &nid) == TRUE; +} +#endif + +static void cleanupwindow() +{ + if(!appinstance) return; + if(appmenu) + { + DestroyMenu(appmenu); + appmenu = NULL; + } + if(wndclass) + { + UnregisterClass(MAKEINTATOM(wndclass), appinstance); + wndclass = 0; + } +} + +static BOOL WINAPI consolehandler(DWORD dwCtrlType) +{ + switch(dwCtrlType) + { + case CTRL_C_EVENT: + case CTRL_BREAK_EVENT: + case CTRL_CLOSE_EVENT: + exit(EXIT_SUCCESS); + return TRUE; + } + return FALSE; +} + +static void writeline(logline &line) +{ + static uchar ubuf[512]; + size_t len = strlen(line.buf), carry = 0; + while(carry < len) + { + size_t numu = encodeutf8(ubuf, sizeof(ubuf), &((uchar *)line.buf)[carry], len - carry, &carry); + DWORD written = 0; + WriteConsole(outhandle, ubuf, numu, &written, NULL); + } +} + +static void setupconsole() +{ + if(conwindow) return; + if(!AllocConsole()) return; + SetConsoleCtrlHandler(consolehandler, TRUE); + conwindow = GetConsoleWindow(); + SetConsoleTitle(apptip); + //SendMessage(conwindow, WM_SETICON, ICON_SMALL, (LPARAM)appicon); + SendMessage(conwindow, WM_SETICON, ICON_BIG, (LPARAM)appicon); + outhandle = GetStdHandle(STD_OUTPUT_HANDLE); + CONSOLE_SCREEN_BUFFER_INFO coninfo; + GetConsoleScreenBufferInfo(outhandle, &coninfo); + coninfo.dwSize.Y = MAXLOGLINES; + SetConsoleScreenBufferSize(outhandle, coninfo.dwSize); + SetConsoleCP(CP_UTF8); + SetConsoleOutputCP(CP_UTF8); + loopv(loglines) writeline(loglines[i]); +} + +enum +{ + MENU_OPENCONSOLE = 0, + MENU_SHOWCONSOLE, + MENU_HIDECONSOLE, + MENU_EXIT +}; + +static LRESULT CALLBACK handlemessages(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + switch(uMsg) + { + case WM_APP: + SetForegroundWindow(hWnd); + switch(lParam) + { + //case WM_MOUSEMOVE: + // break; + case WM_LBUTTONUP: + case WM_RBUTTONUP: + { + POINT pos; + GetCursorPos(&pos); + TrackPopupMenu(appmenu, TPM_CENTERALIGN|TPM_BOTTOMALIGN|TPM_RIGHTBUTTON, pos.x, pos.y, 0, hWnd, NULL); + PostMessage(hWnd, WM_NULL, 0, 0); + break; + } + } + return 0; + case WM_COMMAND: + switch(LOWORD(wParam)) + { + case MENU_OPENCONSOLE: + setupconsole(); + if(conwindow) ModifyMenu(appmenu, 0, MF_BYPOSITION|MF_STRING, MENU_HIDECONSOLE, "Hide Console"); + break; + case MENU_SHOWCONSOLE: + ShowWindow(conwindow, SW_SHOWNORMAL); + ModifyMenu(appmenu, 0, MF_BYPOSITION|MF_STRING, MENU_HIDECONSOLE, "Hide Console"); + break; + case MENU_HIDECONSOLE: + ShowWindow(conwindow, SW_HIDE); + ModifyMenu(appmenu, 0, MF_BYPOSITION|MF_STRING, MENU_SHOWCONSOLE, "Show Console"); + break; + case MENU_EXIT: + PostMessage(hWnd, WM_CLOSE, 0, 0); + break; + } + return 0; + case WM_CLOSE: + PostQuitMessage(0); + return 0; + } + return DefWindowProc(hWnd, uMsg, wParam, lParam); +} + +static void setupwindow(const char *title) +{ + copystring(apptip, title); + //appinstance = GetModuleHandle(NULL); + if(!appinstance) fatal("failed getting application instance"); + appicon = LoadIcon(appinstance, MAKEINTRESOURCE(IDI_ICON1));//(HICON)LoadImage(appinstance, MAKEINTRESOURCE(IDI_ICON1), IMAGE_ICON, 0, 0, LR_DEFAULTSIZE); + if(!appicon) fatal("failed loading icon"); + + appmenu = CreatePopupMenu(); + if(!appmenu) fatal("failed creating popup menu"); + AppendMenu(appmenu, MF_STRING, MENU_OPENCONSOLE, "Open Console"); + AppendMenu(appmenu, MF_SEPARATOR, 0, NULL); + AppendMenu(appmenu, MF_STRING, MENU_EXIT, "Exit"); + //SetMenuDefaultItem(appmenu, 0, FALSE); + + WNDCLASS wc; + memset(&wc, 0, sizeof(wc)); + wc.hCursor = NULL; //LoadCursor(NULL, IDC_ARROW); + wc.hIcon = appicon; + wc.lpszMenuName = NULL; + wc.lpszClassName = title; + wc.style = 0; + wc.hInstance = appinstance; + wc.lpfnWndProc = handlemessages; + wc.cbWndExtra = 0; + wc.cbClsExtra = 0; + wndclass = RegisterClass(&wc); + if(!wndclass) fatal("failed registering window class"); + + appwindow = CreateWindow(MAKEINTATOM(wndclass), title, 0, CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, HWND_MESSAGE, NULL, appinstance, NULL); + if(!appwindow) fatal("failed creating window"); + + atexit(cleanupwindow); + + if(!setupsystemtray(WM_APP)) fatal("failed adding to system tray"); +} + +static char *parsecommandline(const char *src, vector &args) +{ + char *buf = new char[strlen(src) + 1], *dst = buf; + for(;;) + { + while(isspace(*src)) src++; + if(!*src) break; + args.add(dst); + for(bool quoted = false; *src && (quoted || !isspace(*src)); src++) + { + if(*src != '"') *dst++ = *src; + else if(dst > buf && src[-1] == '\\') dst[-1] = '"'; + else quoted = !quoted; + } + *dst++ = '\0'; + } + args.add(NULL); + return buf; +} + + +int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrev, LPSTR szCmdLine, int sw) +{ + vector args; + char *buf = parsecommandline(GetCommandLine(), args); + appinstance = hInst; +#ifdef STANDALONE + int standalonemain(int argc, char **argv); + int status = standalonemain(args.length()-1, args.getbuf()); + #define main standalonemain +#else + SDL_SetMainReady(); + int status = SDL_main(args.length()-1, args.getbuf()); +#endif + delete[] buf; + exit(status); + return 0; +} + +void logoutfv(const char *fmt, va_list args) +{ + if(appwindow) + { + logline &line = loglines.add(); + vformatstring(line.buf, fmt, args, sizeof(line.buf)); + if(logfile) writelog(logfile, line.buf); + line.len = min(strlen(line.buf), sizeof(line.buf)-2); + line.buf[line.len++] = '\n'; + line.buf[line.len] = '\0'; + if(outhandle) writeline(line); + } + else if(logfile) writelogv(logfile, fmt, args); +} + +#else + +void logoutfv(const char *fmt, va_list args) +{ + FILE *f = getlogfile(); + if(f) writelogv(f, fmt, args); +} + +#endif + +static bool dedicatedserver = false; + +bool isdedicatedserver() { return dedicatedserver; } + +void rundedicatedserver() +{ + dedicatedserver = true; + logoutf("dedicated server started, waiting for clients..."); +#ifdef WIN32 + SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS); + for(;;) + { + MSG msg; + while(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) + { + if(msg.message == WM_QUIT) exit(EXIT_SUCCESS); + TranslateMessage(&msg); + DispatchMessage(&msg); + } + serverslice(true, 5); + } +#else + for(;;) serverslice(true, 5); +#endif + dedicatedserver = false; +} + +bool servererror(bool dedicated, const char *desc) +{ +#ifndef STANDALONE + if(!dedicated) + { + conoutf(CON_ERROR, "%s", desc); + cleanupserver(); + } + else +#endif + fatal("%s", desc); + return false; +} + +bool setuplistenserver(bool dedicated) +{ + ENetAddress address = { ENET_HOST_ANY, enet_uint16(serverport <= 0 ? server::serverport() : serverport) }; + if(*serverip) + { + if(enet_address_set_host(&address, serverip)<0) conoutf(CON_WARN, "WARNING: server ip not resolved"); + else serveraddress.host = address.host; + } + serverhost = enet_host_create(&address, min(maxclients + server::reserveclients(), MAXCLIENTS), server::numchannels(), 0, serveruprate); + if(!serverhost) return servererror(dedicated, "could not create server host"); + serverhost->duplicatePeers = maxdupclients ? maxdupclients : MAXCLIENTS; + address.port = server::serverinfoport(serverport > 0 ? serverport : -1); + pongsock = enet_socket_create(ENET_SOCKET_TYPE_DATAGRAM); + if(pongsock != ENET_SOCKET_NULL && enet_socket_bind(pongsock, &address) < 0) + { + enet_socket_destroy(pongsock); + pongsock = ENET_SOCKET_NULL; + } + if(pongsock == ENET_SOCKET_NULL) return servererror(dedicated, "could not create server info socket"); + else enet_socket_set_option(pongsock, ENET_SOCKOPT_NONBLOCK, 1); + address.port = server::laninfoport(); + lansock = enet_socket_create(ENET_SOCKET_TYPE_DATAGRAM); + if(lansock != ENET_SOCKET_NULL && (enet_socket_set_option(lansock, ENET_SOCKOPT_REUSEADDR, 1) < 0 || enet_socket_bind(lansock, &address) < 0)) + { + enet_socket_destroy(lansock); + lansock = ENET_SOCKET_NULL; + } + if(lansock == ENET_SOCKET_NULL) conoutf(CON_WARN, "WARNING: could not create LAN server info socket"); + else enet_socket_set_option(lansock, ENET_SOCKOPT_NONBLOCK, 1); + return true; +} + +void initserver(bool listen, bool dedicated) +{ + if(dedicated) + { +#ifdef WIN32 + setupwindow("Cube 2: Sauerbraten server"); +#endif + } + + execfile("server-init.cfg", false); + + if(listen) setuplistenserver(dedicated); + + server::serverinit(); + + if(listen) + { + dedicatedserver = dedicated; + updatemasterserver(); + if(dedicated) rundedicatedserver(); // never returns +#ifndef STANDALONE + else conoutf("listen server started"); +#endif + } +} + +#ifndef STANDALONE +void startlistenserver(int *usemaster) +{ + if(serverhost) { conoutf(CON_ERROR, "listen server is already running"); return; } + + allowupdatemaster = *usemaster>0 ? 1 : 0; + + if(!setuplistenserver(false)) return; + + updatemasterserver(); + + conoutf("listen server started for %d clients%s", maxclients, allowupdatemaster ? " and listed with master server" : ""); +} +COMMAND(startlistenserver, "i"); + +void stoplistenserver() +{ + if(!serverhost) { conoutf(CON_ERROR, "listen server is not running"); return; } + + kicknonlocalclients(); + enet_host_flush(serverhost); + cleanupserver(); + + conoutf("listen server stopped"); +} +COMMAND(stoplistenserver, ""); +#endif + +bool serveroption(char *opt) +{ + switch(opt[1]) + { + case 'u': setvar("serveruprate", atoi(opt+2)); return true; + case 'c': setvar("maxclients", atoi(opt+2)); return true; + case 'i': setsvar("serverip", opt+2); return true; + case 'j': setvar("serverport", atoi(opt+2)); return true; + case 'm': setsvar("mastername", opt+2); setvar("updatemaster", mastername[0] ? 1 : 0); return true; +#ifdef STANDALONE + case 'q': logoutf("Using home directory: %s", opt); sethomedir(opt+2); return true; + case 'k': logoutf("Adding package directory: %s", opt); addpackagedir(opt+2); return true; + case 'g': logoutf("Setting log file: %s", opt); setlogfile(opt+2); return true; +#endif + default: return false; + } +} + +vector gameargs; + +#ifdef STANDALONE +int main(int argc, char **argv) +{ + setlogfile(NULL); + if(enet_initialize()<0) fatal("Unable to initialise network module"); + atexit(enet_deinitialize); + enet_time_set(0); + for(int i = 1; i resolverthreads; +vector resolverqueries; +vector resolverresults; +SDL_mutex *resolvermutex; +SDL_cond *querycond, *resultcond; + +#define RESOLVERTHREADS 2 +#define RESOLVERLIMIT 3000 + +int resolverloop(void * data) +{ + resolverthread *rt = (resolverthread *)data; + SDL_LockMutex(resolvermutex); + SDL_Thread *thread = rt->thread; + SDL_UnlockMutex(resolvermutex); + if(!thread || SDL_GetThreadID(thread) != SDL_ThreadID()) + return 0; + while(thread == rt->thread) + { + SDL_LockMutex(resolvermutex); + while(resolverqueries.empty()) SDL_CondWait(querycond, resolvermutex); + rt->query = resolverqueries.pop(); + rt->starttime = totalmillis; + SDL_UnlockMutex(resolvermutex); + + ENetAddress address = { ENET_HOST_ANY, ENET_PORT_ANY }; + enet_address_set_host(&address, rt->query); + + SDL_LockMutex(resolvermutex); + if(rt->query && thread == rt->thread) + { + resolverresult &rr = resolverresults.add(); + rr.query = rt->query; + rr.address = address; + rt->query = NULL; + rt->starttime = 0; + SDL_CondSignal(resultcond); + } + SDL_UnlockMutex(resolvermutex); + } + return 0; +} + +void resolverinit() +{ + resolvermutex = SDL_CreateMutex(); + querycond = SDL_CreateCond(); + resultcond = SDL_CreateCond(); + + SDL_LockMutex(resolvermutex); + loopi(RESOLVERTHREADS) + { + resolverthread &rt = resolverthreads.add(); + rt.query = NULL; + rt.starttime = 0; + rt.thread = SDL_CreateThread(resolverloop, "resolver", &rt); + } + SDL_UnlockMutex(resolvermutex); +} + +void resolverstop(resolverthread &rt) +{ + SDL_LockMutex(resolvermutex); + if(rt.query) + { +#if SDL_VERSION_ATLEAST(2, 0, 2) + SDL_DetachThread(rt.thread); +#endif + rt.thread = SDL_CreateThread(resolverloop, "resolver", &rt); + } + rt.query = NULL; + rt.starttime = 0; + SDL_UnlockMutex(resolvermutex); +} + +void resolverclear() +{ + if(resolverthreads.empty()) return; + + SDL_LockMutex(resolvermutex); + resolverqueries.shrink(0); + resolverresults.shrink(0); + loopv(resolverthreads) + { + resolverthread &rt = resolverthreads[i]; + resolverstop(rt); + } + SDL_UnlockMutex(resolvermutex); +} + +void resolverquery(const char *name) +{ + if(resolverthreads.empty()) resolverinit(); + + SDL_LockMutex(resolvermutex); + resolverqueries.add(name); + SDL_CondSignal(querycond); + SDL_UnlockMutex(resolvermutex); +} + +bool resolvercheck(const char **name, ENetAddress *address) +{ + bool resolved = false; + SDL_LockMutex(resolvermutex); + if(!resolverresults.empty()) + { + resolverresult &rr = resolverresults.pop(); + *name = rr.query; + address->host = rr.address.host; + resolved = true; + } + else loopv(resolverthreads) + { + resolverthread &rt = resolverthreads[i]; + if(rt.query && totalmillis - rt.starttime > RESOLVERLIMIT) + { + resolverstop(rt); + *name = rt.query; + resolved = true; + } + } + SDL_UnlockMutex(resolvermutex); + return resolved; +} + +bool resolverwait(const char *name, ENetAddress *address) +{ + if(resolverthreads.empty()) resolverinit(); + + defformatstring(text, "resolving %s... (esc to abort)", name); + renderprogress(0, text); + + SDL_LockMutex(resolvermutex); + resolverqueries.add(name); + SDL_CondSignal(querycond); + int starttime = SDL_GetTicks(), timeout = 0; + bool resolved = false; + for(;;) + { + SDL_CondWaitTimeout(resultcond, resolvermutex, 250); + loopv(resolverresults) if(resolverresults[i].query == name) + { + address->host = resolverresults[i].address.host; + resolverresults.remove(i); + resolved = true; + break; + } + if(resolved) break; + + timeout = SDL_GetTicks() - starttime; + renderprogress(min(float(timeout)/RESOLVERLIMIT, 1.0f), text); + if(interceptkey(SDLK_ESCAPE)) timeout = RESOLVERLIMIT + 1; + if(timeout > RESOLVERLIMIT) break; + } + if(!resolved && timeout > RESOLVERLIMIT) + { + loopv(resolverthreads) + { + resolverthread &rt = resolverthreads[i]; + if(rt.query == name) { resolverstop(rt); break; } + } + } + SDL_UnlockMutex(resolvermutex); + return resolved && address->host != ENET_HOST_ANY; +} + +#define CONNLIMIT 20000 + +int connectwithtimeout(ENetSocket sock, const char *hostname, const ENetAddress &address) +{ + defformatstring(text, "connecting to %s... (esc to abort)", hostname); + renderprogress(0, text); + + ENetSocketSet readset, writeset; + if(!enet_socket_connect(sock, &address)) for(int starttime = SDL_GetTicks(), timeout = 0; timeout <= CONNLIMIT;) + { + ENET_SOCKETSET_EMPTY(readset); + ENET_SOCKETSET_EMPTY(writeset); + ENET_SOCKETSET_ADD(readset, sock); + ENET_SOCKETSET_ADD(writeset, sock); + int result = enet_socketset_select(sock, &readset, &writeset, 250); + if(result < 0) break; + else if(result > 0) + { + if(ENET_SOCKETSET_CHECK(readset, sock) || ENET_SOCKETSET_CHECK(writeset, sock)) + { + int error = 0; + if(enet_socket_get_option(sock, ENET_SOCKOPT_ERROR, &error) < 0 || error) break; + return 0; + } + } + timeout = SDL_GetTicks() - starttime; + renderprogress(min(float(timeout)/CONNLIMIT, 1.0f), text); + if(interceptkey(SDLK_ESCAPE)) break; + } + + return -1; +} + +struct pingattempts +{ + enum { MAXATTEMPTS = 2 }; + + int offset, attempts[MAXATTEMPTS]; + + pingattempts() : offset(0) { clearattempts(); } + + void clearattempts() { memset(attempts, 0, sizeof(attempts)); } + + void setoffset() { offset = 1 + rnd(0xFFFFFF); } + + int encodeping(int millis) + { + millis += offset; + return millis ? millis : 1; + } + + int decodeping(int val) + { + return val - offset; + } + + int addattempt(int millis) + { + int val = encodeping(millis); + loopk(MAXATTEMPTS-1) attempts[k+1] = attempts[k]; + attempts[0] = val; + return val; + } + + bool checkattempt(int val, bool del = true) + { + if(val) loopk(MAXATTEMPTS) if(attempts[k] == val) + { + if(del) attempts[k] = 0; + return true; + } + return false; + } + +}; + +enum { UNRESOLVED = 0, RESOLVING, RESOLVED }; + +struct serverinfo : pingattempts +{ + enum + { + WAITING = INT_MAX, + + MAXPINGS = 3 + }; + + string name, map, sdesc; + int port, numplayers, resolved, ping, lastping, nextping; + int pings[MAXPINGS]; + vector attr; + ENetAddress address; + bool keep; + const char *password; + + serverinfo() + : port(-1), numplayers(0), resolved(UNRESOLVED), keep(false), password(NULL) + { + name[0] = map[0] = sdesc[0] = '\0'; + clearpings(); + setoffset(); + } + + ~serverinfo() + { + DELETEA(password); + } + + void clearpings() + { + ping = WAITING; + loopk(MAXPINGS) pings[k] = WAITING; + nextping = 0; + lastping = -1; + clearattempts(); + } + + void cleanup() + { + clearpings(); + attr.setsize(0); + numplayers = 0; + } + + void reset() + { + lastping = -1; + } + + void checkdecay(int decay) + { + if(lastping >= 0 && totalmillis - lastping >= decay) + cleanup(); + if(lastping < 0) lastping = totalmillis; + } + + void calcping() + { + int numpings = 0, totalpings = 0; + loopk(MAXPINGS) if(pings[k] != WAITING) { totalpings += pings[k]; numpings++; } + ping = numpings ? totalpings/numpings : WAITING; + } + + void addping(int rtt, int millis) + { + if(millis >= lastping) lastping = -1; + pings[nextping] = rtt; + nextping = (nextping+1)%MAXPINGS; + calcping(); + } + + static bool compare(serverinfo *a, serverinfo *b) + { + bool ac = server::servercompatible(a->name, a->sdesc, a->map, a->ping, a->attr, a->numplayers), + bc = server::servercompatible(b->name, b->sdesc, b->map, b->ping, b->attr, b->numplayers); + if(ac > bc) return true; + if(bc > ac) return false; + if(a->keep > b->keep) return true; + if(a->keep < b->keep) return false; + if(a->numplayers < b->numplayers) return false; + if(a->numplayers > b->numplayers) return true; + if(a->ping > b->ping) return false; + if(a->ping < b->ping) return true; + int cmp = strcmp(a->name, b->name); + if(cmp != 0) return cmp < 0; + if(a->port < b->port) return true; + if(a->port > b->port) return false; + return false; + } +}; + +vector servers; +ENetSocket pingsock = ENET_SOCKET_NULL; +int lastinfo = 0; + +static serverinfo *newserver(const char *name, int port, uint ip = ENET_HOST_ANY) +{ + serverinfo *si = new serverinfo; + si->address.host = ip; + si->address.port = server::serverinfoport(port); + if(ip!=ENET_HOST_ANY) si->resolved = RESOLVED; + + si->port = port; + if(name) copystring(si->name, name); + else if(ip==ENET_HOST_ANY || enet_address_get_host_ip(&si->address, si->name, sizeof(si->name)) < 0) + { + delete si; + return NULL; + + } + + servers.add(si); + + return si; +} + +void addserver(const char *name, int port, const char *password, bool keep) +{ + if(port <= 0) port = server::serverport(); + loopv(servers) + { + serverinfo *s = servers[i]; + if(strcmp(s->name, name) || s->port != port) continue; + if(password && (!s->password || strcmp(s->password, password))) + { + DELETEA(s->password); + s->password = newstring(password); + } + if(keep && !s->keep) s->keep = true; + return; + } + serverinfo *s = newserver(name, port); + if(!s) return; + if(password) s->password = newstring(password); + s->keep = keep; +} + +VARP(searchlan, 0, 0, 1); +VARP(servpingrate, 1000, 5000, 60000); +VARP(servpingdecay, 1000, 15000, 60000); +VARP(maxservpings, 0, 10, 1000); + +pingattempts lanpings; + +template static inline void buildping(ENetBuffer &buf, uchar (&ping)[N], pingattempts &a) +{ + ucharbuf p(ping, N); + putint(p, a.addattempt(totalmillis)); + buf.data = ping; + buf.dataLength = p.length(); +} + +void pingservers() +{ + if(pingsock == ENET_SOCKET_NULL) + { + pingsock = enet_socket_create(ENET_SOCKET_TYPE_DATAGRAM); + if(pingsock == ENET_SOCKET_NULL) + { + lastinfo = totalmillis; + return; + } + enet_socket_set_option(pingsock, ENET_SOCKOPT_NONBLOCK, 1); + enet_socket_set_option(pingsock, ENET_SOCKOPT_BROADCAST, 1); + + lanpings.setoffset(); + } + + ENetBuffer buf; + uchar ping[MAXTRANS]; + + static int lastping = 0; + if(lastping >= servers.length()) lastping = 0; + loopi(maxservpings ? min(servers.length(), maxservpings) : servers.length()) + { + serverinfo &si = *servers[lastping]; + if(++lastping >= servers.length()) lastping = 0; + if(si.address.host == ENET_HOST_ANY) continue; + buildping(buf, ping, si); + enet_socket_send(pingsock, &si.address, &buf, 1); + + si.checkdecay(servpingdecay); + } + if(searchlan) + { + ENetAddress address; + address.host = ENET_HOST_BROADCAST; + address.port = server::laninfoport(); + buildping(buf, ping, lanpings); + enet_socket_send(pingsock, &address, &buf, 1); + } + lastinfo = totalmillis; +} + +void checkresolver() +{ + int resolving = 0; + loopv(servers) + { + serverinfo &si = *servers[i]; + if(si.resolved == RESOLVED) continue; + if(si.address.host == ENET_HOST_ANY) + { + if(si.resolved == UNRESOLVED) { si.resolved = RESOLVING; resolverquery(si.name); } + resolving++; + } + } + if(!resolving) return; + + const char *name = NULL; + for(;;) + { + ENetAddress addr = { ENET_HOST_ANY, ENET_PORT_ANY }; + if(!resolvercheck(&name, &addr)) break; + loopv(servers) + { + serverinfo &si = *servers[i]; + if(name == si.name) + { + si.resolved = RESOLVED; + si.address.host = addr.host; + break; + } + } + } +} + +static int lastreset = 0; + +void checkpings() +{ + if(pingsock==ENET_SOCKET_NULL) return; + enet_uint32 events = ENET_SOCKET_WAIT_RECEIVE; + ENetBuffer buf; + ENetAddress addr; + uchar ping[MAXTRANS]; + char text[MAXTRANS]; + buf.data = ping; + buf.dataLength = sizeof(ping); + while(enet_socket_wait(pingsock, &events, 0) >= 0 && events) + { + int len = enet_socket_receive(pingsock, &addr, &buf, 1); + if(len <= 0) return; + ucharbuf p(ping, len); + int millis = getint(p); + serverinfo *si = NULL; + loopv(servers) if(addr.host == servers[i]->address.host && addr.port == servers[i]->address.port) { si = servers[i]; break; } + if(si) + { + if(!si->checkattempt(millis)) continue; + millis = si->decodeping(millis); + } + else if(!searchlan || !lanpings.checkattempt(millis, false)) continue; + else + { + si = newserver(NULL, server::serverport(addr.port), addr.host); + millis = lanpings.decodeping(millis); + } + int rtt = clamp(totalmillis - millis, 0, min(servpingdecay, totalmillis)); + if(millis >= lastreset && rtt < servpingdecay) si->addping(rtt, millis); + si->numplayers = getint(p); + int numattr = getint(p); + si->attr.setsize(0); + loopj(numattr) { int attr = getint(p); if(p.overread()) break; si->attr.add(attr); } + getstring(text, p); + filtertext(si->map, text, false); + getstring(text, p); + filtertext(si->sdesc, text, true, true); + } +} + +void sortservers() +{ + servers.sort(serverinfo::compare); +} +COMMAND(sortservers, ""); + +VARP(autosortservers, 0, 1, 1); +VARP(autoupdateservers, 0, 1, 1); + +void refreshservers() +{ + static int lastrefresh = 0; + if(lastrefresh==totalmillis) return; + if(totalmillis - lastrefresh > 1000) + { + loopv(servers) servers[i]->reset(); + lastreset = totalmillis; + } + lastrefresh = totalmillis; + + checkresolver(); + checkpings(); + if(totalmillis - lastinfo >= servpingrate/(maxservpings ? max(1, (servers.length() + maxservpings - 1) / maxservpings) : 1)) pingservers(); + if(autosortservers) sortservers(); +} + +serverinfo *selectedserver = NULL; + +const char *showservers(g3d_gui *cgui, uint *header, int pagemin, int pagemax) +{ + refreshservers(); + if(servers.empty()) + { + if(header) execute(header); + return NULL; + } + serverinfo *sc = NULL; + for(int start = 0; start < servers.length();) + { + if(start > 0) cgui->tab(); + if(header) execute(header); + int end = servers.length(); + cgui->pushlist(); + loopi(10) + { + if(!game::serverinfostartcolumn(cgui, i)) break; + for(int j = start; j < end; j++) + { + if(!i && j+1 - start >= pagemin && (j+1 - start >= pagemax || cgui->shouldtab())) { end = j; break; } + serverinfo &si = *servers[j]; + const char *sdesc = si.sdesc; + if(si.address.host == ENET_HOST_ANY) sdesc = "[unknown host]"; + else if(si.ping == serverinfo::WAITING) sdesc = "[waiting for response]"; + if(game::serverinfoentry(cgui, i, si.name, si.port, sdesc, si.map, sdesc == si.sdesc ? si.ping : -1, si.attr, si.numplayers)) + sc = &si; + } + game::serverinfoendcolumn(cgui, i); + } + cgui->poplist(); + start = end; + } + if(selectedserver || !sc) return NULL; + selectedserver = sc; + return "connectselected"; +} + +void connectselected() +{ + if(!selectedserver) return; + connectserv(selectedserver->name, selectedserver->port, selectedserver->password); + selectedserver = NULL; +} + +COMMAND(connectselected, ""); + +void clearservers(bool full = false) +{ + resolverclear(); + if(full) servers.deletecontents(); + else loopvrev(servers) if(!servers[i]->keep) delete servers.remove(i); + selectedserver = NULL; +} + +#define RETRIEVELIMIT 20000 + +void retrieveservers(vector &data) +{ + ENetSocket sock = connectmaster(true); + if(sock == ENET_SOCKET_NULL) return; + + extern char *mastername; + defformatstring(text, "retrieving servers from %s... (esc to abort)", mastername); + renderprogress(0, text); + + int starttime = SDL_GetTicks(), timeout = 0; + const char *req = "list\n"; + int reqlen = strlen(req); + ENetBuffer buf; + while(reqlen > 0) + { + enet_uint32 events = ENET_SOCKET_WAIT_SEND; + if(enet_socket_wait(sock, &events, 250) >= 0 && events) + { + buf.data = (void *)req; + buf.dataLength = reqlen; + int sent = enet_socket_send(sock, NULL, &buf, 1); + if(sent < 0) break; + req += sent; + reqlen -= sent; + if(reqlen <= 0) break; + } + timeout = SDL_GetTicks() - starttime; + renderprogress(min(float(timeout)/RETRIEVELIMIT, 1.0f), text); + if(interceptkey(SDLK_ESCAPE)) timeout = RETRIEVELIMIT + 1; + if(timeout > RETRIEVELIMIT) break; + } + + if(reqlen <= 0) for(;;) + { + enet_uint32 events = ENET_SOCKET_WAIT_RECEIVE; + if(enet_socket_wait(sock, &events, 250) >= 0 && events) + { + if(data.length() >= data.capacity()) data.reserve(4096); + buf.data = data.getbuf() + data.length(); + buf.dataLength = data.capacity() - data.length(); + int recv = enet_socket_receive(sock, NULL, &buf, 1); + if(recv <= 0) break; + data.advance(recv); + } + timeout = SDL_GetTicks() - starttime; + renderprogress(min(float(timeout)/RETRIEVELIMIT, 1.0f), text); + if(interceptkey(SDLK_ESCAPE)) timeout = RETRIEVELIMIT + 1; + if(timeout > RETRIEVELIMIT) break; + } + + if(data.length()) data.add('\0'); + enet_socket_destroy(sock); +} + +bool updatedservers = false; + +void updatefrommaster() +{ + vector data; + retrieveservers(data); + if(data.empty()) conoutf(CON_ERROR, "master server not replying"); + else + { + clearservers(); + char *line = data.getbuf(); + while(char *end = (char *)memchr(line, '\n', data.length() - (line - data.getbuf()))) + { + *end = '\0'; + + const char *args = line; + while(args < end && !iscubespace(*args)) args++; + int cmdlen = args - line; + while(args < end && iscubespace(*args)) args++; + + if(matchstring(line, cmdlen, "addserver")) + { + string ip; + int port; + if(sscanf(args, "%100s %d", ip, &port) == 2) addserver(ip, port); + } + else if(matchstring(line, cmdlen, "echo")) conoutf("\f1%s", args); + + line = end + 1; + } + } + refreshservers(); + updatedservers = true; +} + +void initservers() +{ + selectedserver = NULL; + if(autoupdateservers && !updatedservers) updatefrommaster(); +} + +ICOMMAND(addserver, "sis", (const char *name, int *port, const char *password), addserver(name, *port, password[0] ? password : NULL)); +ICOMMAND(keepserver, "sis", (const char *name, int *port, const char *password), addserver(name, *port, password[0] ? password : NULL, true)); +ICOMMAND(clearservers, "i", (int *full), clearservers(*full!=0)); +COMMAND(updatefrommaster, ""); +COMMAND(initservers, ""); + +void writeservercfg() +{ + if(!game::savedservers()) return; + stream *f = openutf8file(path(game::savedservers(), true), "w"); + if(!f) return; + int kept = 0; + loopv(servers) + { + serverinfo *s = servers[i]; + if(s->keep) + { + if(!kept) f->printf("// servers that should never be cleared from the server list\n\n"); + if(s->password) f->printf("keepserver %s %d %s\n", escapeid(s->name), s->port, escapestring(s->password)); + else f->printf("keepserver %s %d\n", escapeid(s->name), s->port); + kept++; + } + } + if(kept) f->printf("\n"); + f->printf("// servers connected to are added here automatically\n\n"); + loopv(servers) + { + serverinfo *s = servers[i]; + if(!s->keep) + { + if(s->password) f->printf("addserver %s %d %s\n", escapeid(s->name), s->port, escapestring(s->password)); + else f->printf("addserver %s %d\n", escapeid(s->name), s->port); + } + } + delete f; +} + diff --git a/src/engine/shader.cpp b/src/engine/shader.cpp new file mode 100644 index 0000000..da17d4f --- /dev/null +++ b/src/engine/shader.cpp @@ -0,0 +1,1522 @@ +// shader.cpp: OpenGL GLSL shader management + +#include "engine.h" + +Shader *Shader::lastshader = NULL; + +Shader *nullshader = NULL, *hudshader = NULL, *hudnotextureshader = NULL, *textureshader = NULL, *notextureshader = NULL, *nocolorshader = NULL, *foggedshader = NULL, *foggednotextureshader = NULL, *stdworldshader = NULL; + +static hashnameset globalparams(256); +static hashtable localparams(256); +static hashnameset shaders(256); +static Shader *slotshader = NULL; +static vector slotparams; +static bool standardshaders = false, forceshaders = true, loadedshaders = false; + +VAR(reservevpparams, 1, 16, 0); +VAR(maxvsuniforms, 1, 0, 0); +VAR(maxfsuniforms, 1, 0, 0); +VAR(maxvaryings, 1, 0, 0); +VAR(dbgshader, 0, 0, 2); + +void loadshaders() +{ + standardshaders = true; + execfile("data/glsl.cfg"); + standardshaders = false; + + nullshader = lookupshaderbyname("null"); + hudshader = lookupshaderbyname("hud"); + hudnotextureshader = lookupshaderbyname("hudnotexture"); + stdworldshader = lookupshaderbyname("stdworld"); + if(!nullshader || !hudshader || !hudnotextureshader || !stdworldshader) fatal("cannot find shader definitions"); + + dummyslot.shader = stdworldshader; + + textureshader = lookupshaderbyname("texture"); + notextureshader = lookupshaderbyname("notexture"); + nocolorshader = lookupshaderbyname("nocolor"); + foggedshader = lookupshaderbyname("fogged"); + foggednotextureshader = lookupshaderbyname("foggednotexture"); + + nullshader->set(); + + loadedshaders = true; +} + +Shader *lookupshaderbyname(const char *name) +{ + Shader *s = shaders.access(name); + return s && s->loaded() ? s : NULL; +} + +Shader *generateshader(const char *name, const char *fmt, ...) +{ + if(!loadedshaders) return NULL; + Shader *s = name ? lookupshaderbyname(name) : NULL; + if(!s) + { + defvformatstring(cmd, fmt, fmt); + bool wasstandard = standardshaders; + standardshaders = true; + execute(cmd); + standardshaders = wasstandard; + s = name ? lookupshaderbyname(name) : NULL; + if(!s) s = nullshader; + } + return s; +} + +static void showglslinfo(GLenum type, GLuint obj, const char *name, const char **parts = NULL, int numparts = 0) +{ + GLint length = 0; + if(type) glGetShaderiv_(obj, GL_INFO_LOG_LENGTH, &length); + else glGetProgramiv_(obj, GL_INFO_LOG_LENGTH, &length); + if(length > 1) + { + conoutf(CON_ERROR, "GLSL ERROR (%s:%s)", type == GL_VERTEX_SHADER ? "VS" : (type == GL_FRAGMENT_SHADER ? "FS" : "PROG"), name); + FILE *l = getlogfile(); + if(l) + { + GLchar *log = new GLchar[length]; + if(type) glGetShaderInfoLog_(obj, length, &length, log); + else glGetProgramInfoLog_(obj, length, &length, log); + fprintf(l, "%s\n", log); + bool partlines = log[0] != '0'; + int line = 0; + loopi(numparts) + { + const char *part = parts[i]; + int startline = line; + while(*part) + { + const char *next = strchr(part, '\n'); + if(++line > 1000) goto done; + if(partlines) fprintf(l, "%d(%d): ", i, line - startline); else fprintf(l, "%d: ", line); + fwrite(part, 1, next ? next - part + 1 : strlen(part), l); + if(!next) { fputc('\n', l); break; } + part = next + 1; + } + } + done: + delete[] log; + } + } +} + +static void compileglslshader(GLenum type, GLuint &obj, const char *def, const char *name, bool msg = true) +{ + const char *source = def + strspn(def, " \t\r\n"); + const char *parts[16]; + int numparts = 0; + static const struct { int version; const char * const header; } glslversions[] = + { + { 330, "#version 330\n" }, + { 150, "#version 150\n" }, + { 130, "#version 130\n" }, + { 120, "#version 120\n" } + }; + loopi(sizeof(glslversions)/sizeof(glslversions[0])) if(glslversion >= glslversions[i].version) + { + parts[numparts++] = glslversions[i].header; + break; + } + if(glslversion >= 130) + { + if(type == GL_VERTEX_SHADER) parts[numparts++] = + "#define attribute in\n" + "#define varying out\n"; + else if(type == GL_FRAGMENT_SHADER) + { + parts[numparts++] = "#define varying in\n"; + if(glslversion < 150) parts[numparts++] = "precision highp float;\n"; + if(glversion >= 300) parts[numparts++] = + "out vec4 cube2_FragColor;\n" + "#define gl_FragColor cube2_FragColor\n"; + } + parts[numparts++] = + "#define texture2D(sampler, coords) texture(sampler, coords)\n" + "#define texture2DProj(sampler, coords) textureProj(sampler, coords)\n" + "#define textureCube(sampler, coords) texture(sampler, coords)\n"; + } + parts[numparts++] = source; + + obj = glCreateShader_(type); + glShaderSource_(obj, numparts, (const GLchar **)parts, NULL); + glCompileShader_(obj); + GLint success; + glGetShaderiv_(obj, GL_COMPILE_STATUS, &success); + if(!success) + { + if(msg) showglslinfo(type, obj, name, parts, numparts); + glDeleteShader_(obj); + obj = 0; + } + else if(dbgshader > 1 && msg) showglslinfo(type, obj, name, parts, numparts); +} + +VAR(dbgubo, 0, 0, 1); + +static void bindglsluniform(Shader &s, UniformLoc &u) +{ + u.loc = glGetUniformLocation_(s.program, u.name); + if(!u.blockname || !hasUBO) return; + GLuint bidx = glGetUniformBlockIndex_(s.program, u.blockname); + GLuint uidx = GL_INVALID_INDEX; + glGetUniformIndices_(s.program, 1, &u.name, &uidx); + if(bidx != GL_INVALID_INDEX && uidx != GL_INVALID_INDEX) + { + GLint sizeval = 0, offsetval = 0, strideval = 0; + glGetActiveUniformBlockiv_(s.program, bidx, GL_UNIFORM_BLOCK_DATA_SIZE, &sizeval); + if(sizeval <= 0) return; + glGetActiveUniformsiv_(s.program, 1, &uidx, GL_UNIFORM_OFFSET, &offsetval); + if(u.stride > 0) + { + glGetActiveUniformsiv_(s.program, 1, &uidx, GL_UNIFORM_ARRAY_STRIDE, &strideval); + if(strideval > u.stride) return; + } + u.offset = offsetval; + u.size = sizeval; + glUniformBlockBinding_(s.program, bidx, u.binding); + if(dbgubo) conoutf(CON_DEBUG, "UBO: %s:%s:%d, offset: %d, size: %d, stride: %d", u.name, u.blockname, u.binding, offsetval, sizeval, strideval); + } +} + +static void linkglslprogram(Shader &s, bool msg = true) +{ + s.program = s.vsobj && s.psobj ? glCreateProgram_() : 0; + GLint success = 0; + if(s.program) + { + glAttachShader_(s.program, s.vsobj); + glAttachShader_(s.program, s.psobj); + uint attribs = 0; + loopv(s.attriblocs) + { + AttribLoc &a = s.attriblocs[i]; + glBindAttribLocation_(s.program, a.loc, a.name); + attribs |= 1<= 300) + { + glBindFragDataLocation_(s.program, 0, "cube2_FragColor"); + } + glLinkProgram_(s.program); + glGetProgramiv_(s.program, GL_LINK_STATUS, &success); + } + if(success) + { + glUseProgram_(s.program); + loopi(8) + { + static const char * const texnames[8] = { "tex0", "tex1", "tex2", "tex3", "tex4", "tex5", "tex6", "tex7" }; + GLint loc = glGetUniformLocation_(s.program, texnames[i]); + if(loc != -1) glUniform1i_(loc, i); + } + loopv(s.defaultparams) + { + SlotShaderParamState ¶m = s.defaultparams[i]; + param.loc = glGetUniformLocation_(s.program, param.name); + } + loopv(s.uniformlocs) bindglsluniform(s, s.uniformlocs[i]); + glUseProgram_(0); + } + else if(s.program) + { + if(msg) showglslinfo(GL_FALSE, s.program, s.name); + glDeleteProgram_(s.program); + s.program = 0; + } +} + +int getlocalparam(const char *name) +{ + return localparams.access(name, int(localparams.numelems)); +} + +static int addlocalparam(Shader &s, const char *name, int loc, int size, GLenum format) +{ + int idx = getlocalparam(name); + if(idx >= s.localparamremap.length()) + { + int n = idx + 1 - s.localparamremap.length(); + memset(s.localparamremap.pad(n), 0xFF, n); + } + s.localparamremap[idx] = s.localparams.length(); + + LocalShaderParamState &l = s.localparams.add(); + l.name = name; + l.loc = loc; + l.size = size; + l.format = format; + return idx; +} + +GlobalShaderParamState *getglobalparam(const char *name) +{ + GlobalShaderParamState *param = globalparams.access(name); + if(!param) + { + param = &globalparams[name]; + param->name = name; + memset(param->buf, -1, sizeof(param->buf)); + param->version = -1; + } + return param; +} + +static GlobalShaderParamUse *addglobalparam(Shader &s, GlobalShaderParamState *param, int loc, int size, GLenum format) +{ + GlobalShaderParamUse &g = s.globalparams.add(); + g.param = param; + g.version = -2; + g.loc = loc; + g.size = size; + g.format = format; + return &g; +} + +static void setglsluniformformat(Shader &s, const char *name, GLenum format, int size) +{ + switch(format) + { + case GL_FLOAT: + case GL_FLOAT_VEC2: + case GL_FLOAT_VEC3: + case GL_FLOAT_VEC4: + case GL_INT: + case GL_INT_VEC2: + case GL_INT_VEC3: + case GL_INT_VEC4: + case GL_BOOL: + case GL_BOOL_VEC2: + case GL_BOOL_VEC3: + case GL_BOOL_VEC4: + case GL_FLOAT_MAT2: + case GL_FLOAT_MAT3: + case GL_FLOAT_MAT4: + break; + default: + return; + } + if(!strncmp(name, "gl_", 3)) return; + + int loc = glGetUniformLocation_(s.program, name); + if(loc < 0) return; + loopvj(s.defaultparams) if(s.defaultparams[j].loc == loc) + { + s.defaultparams[j].format = format; + return; + } + loopvj(s.uniformlocs) if(s.uniformlocs[j].loc == loc) return; + loopvj(s.globalparams) if(s.globalparams[j].loc == loc) return; + loopvj(s.localparams) if(s.localparams[j].loc == loc) return; + + name = getshaderparamname(name); + GlobalShaderParamState *param = globalparams.access(name); + if(param) addglobalparam(s, param, loc, size, format); + else addlocalparam(s, name, loc, size, format); +} + +static void allocglslactiveuniforms(Shader &s) +{ + GLint numactive = 0; + glGetProgramiv_(s.program, GL_ACTIVE_UNIFORMS, &numactive); + string name; + loopi(numactive) + { + GLsizei namelen = 0; + GLint size = 0; + GLenum format = GL_FLOAT_VEC4; + name[0] = '\0'; + glGetActiveUniform_(s.program, i, sizeof(name)-1, &namelen, &size, &format, name); + if(namelen <= 0 || size <= 0) continue; + name[clamp(int(namelen), 0, (int)sizeof(name)-2)] = '\0'; + char *brak = strchr(name, '['); + if(brak) *brak = '\0'; + setglsluniformformat(s, name, format, size); + } +} + +void Shader::allocparams(Slot *slot) +{ + if(slot) + { +#define UNIFORMTEX(name, tmu) \ + { \ + loc = glGetUniformLocation_(program, name); \ + int val = tmu; \ + if(loc != -1) glUniform1i_(loc, val); \ + } + int loc, tmu = 2; + if(type & SHADER_NORMALSLMS) + { + UNIFORMTEX("lmcolor", 1); + UNIFORMTEX("lmdir", 2); + tmu++; + } + else UNIFORMTEX("lightmap", 1); + if(type & SHADER_ENVMAP) UNIFORMTEX("envmap", tmu++); + UNIFORMTEX("shadowmap", 7); + int stex = 0; + loopv(slot->sts) + { + Slot::Tex &t = slot->sts[i]; + switch(t.type) + { + case TEX_DIFFUSE: UNIFORMTEX("diffusemap", 0); break; + case TEX_NORMAL: UNIFORMTEX("normalmap", tmu++); break; + case TEX_GLOW: UNIFORMTEX("glowmap", tmu++); break; + case TEX_DECAL: UNIFORMTEX("decal", tmu++); break; + case TEX_SPEC: if(t.combined<0) UNIFORMTEX("specmap", tmu++); break; + case TEX_DEPTH: if(t.combined<0) UNIFORMTEX("depthmap", tmu++); break; + case TEX_ALPHA: if(t.combined<0) UNIFORMTEX("alphamap", tmu++); break; + case TEX_UNKNOWN: + { + defformatstring(sname, "stex%d", stex++); + UNIFORMTEX(sname, tmu++); + break; + } + } + } + } + allocglslactiveuniforms(*this); +} + +int GlobalShaderParamState::nextversion = 0; + +void GlobalShaderParamState::resetversions() +{ + enumerate(shaders, Shader, s, + { + loopv(s.globalparams) + { + GlobalShaderParamUse &u = s.globalparams[i]; + if(u.version != u.param->version) u.version = -2; + } + }); + nextversion = 0; + enumerate(globalparams, GlobalShaderParamState, g, { g.version = ++nextversion; }); + enumerate(shaders, Shader, s, + { + loopv(s.globalparams) + { + GlobalShaderParamUse &u = s.globalparams[i]; + if(u.version >= 0) u.version = u.param->version; + } + }); +} + +static float *findslotparam(Slot &s, const char *name, float *noval = NULL) +{ + loopv(s.params) + { + SlotShaderParam ¶m = s.params[i]; + if(name == param.name) return param.val; + } + loopv(s.shader->defaultparams) + { + SlotShaderParamState ¶m = s.shader->defaultparams[i]; + if(name == param.name) return param.val; + } + return noval; +} + +static float *findslotparam(VSlot &s, const char *name, float *noval = NULL) +{ + loopv(s.params) + { + SlotShaderParam ¶m = s.params[i]; + if(name == param.name) return param.val; + } + return findslotparam(*s.slot, name, noval); +} + +static inline void setslotparam(SlotShaderParamState &l, const float *val) +{ + switch(l.format) + { + case GL_BOOL: + case GL_FLOAT: glUniform1fv_(l.loc, 1, val); break; + case GL_BOOL_VEC2: + case GL_FLOAT_VEC2: glUniform2fv_(l.loc, 1, val); break; + case GL_BOOL_VEC3: + case GL_FLOAT_VEC3: glUniform3fv_(l.loc, 1, val); break; + case GL_BOOL_VEC4: + case GL_FLOAT_VEC4: glUniform4fv_(l.loc, 1, val); break; + case GL_INT: glUniform1i_(l.loc, int(val[0])); break; + case GL_INT_VEC2: glUniform2i_(l.loc, int(val[0]), int(val[1])); break; + case GL_INT_VEC3: glUniform3i_(l.loc, int(val[0]), int(val[1]), int(val[2])); break; + case GL_INT_VEC4: glUniform4i_(l.loc, int(val[0]), int(val[1]), int(val[2]), int(val[3])); break; + } +} + +#define SETSLOTPARAM(l, mask, i, val) do { \ + if(!(mask&(1<invalid() ? 0 : reusevs->vsobj; + else compileglslshader(GL_VERTEX_SHADER, vsobj, vsstr, name, dbgshader || !variantshader); + if(!psstr) psobj = !reuseps || reuseps->invalid() ? 0 : reuseps->psobj; + else compileglslshader(GL_FRAGMENT_SHADER, psobj, psstr, name, dbgshader || !variantshader); + linkglslprogram(*this, !variantshader); + return program!=0; +} + +void Shader::cleanup(bool invalid) +{ + detailshader = NULL; + used = false; + if(vsobj) { if(!reusevs) glDeleteShader_(vsobj); vsobj = 0; } + if(psobj) { if(!reuseps) glDeleteShader_(psobj); psobj = 0; } + if(program) { glDeleteProgram_(program); program = 0; } + localparams.setsize(0); + localparamremap.setsize(0); + globalparams.setsize(0); + if(standard || invalid) + { + type = SHADER_INVALID; + DELETEA(vsstr); + DELETEA(psstr); + DELETEA(defer); + variants.setsize(0); + DELETEA(variantrows); + defaultparams.setsize(0); + attriblocs.setsize(0); + uniformlocs.setsize(0); + altshader = NULL; + loopi(MAXSHADERDETAIL) fastshader[i] = this; + reusevs = reuseps = NULL; + } + else loopv(defaultparams) defaultparams[i].loc = -1; + owner = NULL; +} + +bool Shader::isnull(const Shader *s) { return !s; } + +static void genattriblocs(Shader &s, const char *vs, const char *ps, Shader *reusevs, Shader *reuseps) +{ + static int len = strlen("//:attrib"); + string name; + int loc; + if(reusevs) s.attriblocs = reusevs->attriblocs; + else while((vs = strstr(vs, "//:attrib"))) + { + if(sscanf(vs, "//:attrib %100s %d", name, &loc) == 2) + s.attriblocs.add(AttribLoc(getshaderparamname(name), loc)); + vs += len; + } +} + +static void genuniformlocs(Shader &s, const char *vs, const char *ps, Shader *reusevs, Shader *reuseps) +{ + static int len = strlen("//:uniform"); + string name, blockname; + int binding, stride; + if(reusevs) s.uniformlocs = reusevs->uniformlocs; + else while((vs = strstr(vs, "//:uniform"))) + { + int numargs = sscanf(vs, "//:uniform %100s %100s %d %d", name, blockname, &binding, &stride); + if(numargs >= 3) s.uniformlocs.add(UniformLoc(getshaderparamname(name), getshaderparamname(blockname), binding, numargs >= 4 ? stride : 0)); + else if(numargs >= 1) s.uniformlocs.add(UniformLoc(getshaderparamname(name))); + vs += len; + } +} + +Shader *newshader(int type, const char *name, const char *vs, const char *ps, Shader *variant = NULL, int row = 0) +{ + if(Shader::lastshader) + { + glUseProgram_(0); + Shader::lastshader = NULL; + } + + Shader *exists = shaders.access(name); + char *rname = exists ? exists->name : newstring(name); + Shader &s = shaders[rname]; + s.name = rname; + s.vsstr = newstring(vs); + s.psstr = newstring(ps); + DELETEA(s.defer); + s.type = type; + s.variantshader = variant; + s.standard = standardshaders; + if(forceshaders) s.forced = true; + s.reusevs = s.reuseps = NULL; + if(variant) + { + int row = 0, col = 0; + if(!vs[0] || sscanf(vs, "%d , %d", &row, &col) >= 1) + { + DELETEA(s.vsstr); + s.reusevs = !vs[0] ? variant : variant->getvariant(col, row); + } + row = col = 0; + if(!ps[0] || sscanf(ps, "%d , %d", &row, &col) >= 1) + { + DELETEA(s.psstr); + s.reuseps = !ps[0] ? variant : variant->getvariant(col, row); + } + } + if(variant) loopv(variant->defaultparams) s.defaultparams.add(variant->defaultparams[i]); + else loopv(slotparams) s.defaultparams.add(slotparams[i]); + s.attriblocs.setsize(0); + s.uniformlocs.setsize(0); + genattriblocs(s, vs, ps, s.reusevs, s.reuseps); + genuniformlocs(s, vs, ps, s.reusevs, s.reuseps); + if(!s.compile()) + { + s.cleanup(true); + if(variant) shaders.remove(rname); + return NULL; + } + if(variant) variant->addvariant(row, &s); + s.fixdetailshader(); + return &s; +} + +void setupshaders() +{ + GLint val; + glGetIntegerv(GL_MAX_VERTEX_UNIFORM_COMPONENTS, &val); + maxvsuniforms = val/4; + glGetIntegerv(GL_MAX_FRAGMENT_UNIFORM_COMPONENTS, &val); + maxfsuniforms = val/4; + if(glversion < 300) + { + glGetIntegerv(GL_MAX_VARYING_COMPONENTS, &val); + maxvaryings = val; + } + + standardshaders = true; + nullshader = newshader(0, "null", + "attribute vec4 vvertex;\n" + "void main(void) {\n" + " gl_Position = vvertex;\n" + "}\n", + "void main(void) {\n" + " gl_FragColor = vec4(1.0, 0.0, 1.0, 0.0);\n" + "}\n"); + hudshader = newshader(0, "hud", + "attribute vec4 vvertex, vcolor;\n" + "attribute vec2 vtexcoord0;\n" + "uniform mat4 hudmatrix;\n" + "varying vec2 texcoord0;\n" + "varying vec4 color;\n" + "void main(void) {\n" + " gl_Position = hudmatrix * vvertex;\n" + " texcoord0 = vtexcoord0;\n" + " color = vcolor;\n" + "}\n", + "varying vec2 texcoord0;\n" + "varying vec4 color;\n" + "uniform sampler2D tex0;\n" + "void main(void) {\n" + " gl_FragColor = color * texture2D(tex0, texcoord0);\n" + "}\n"); + hudnotextureshader = newshader(0, "hudnotexture", + "attribute vec4 vvertex, vcolor;\n" + "uniform mat4 hudmatrix;\n" + "varying vec4 color;\n" + "void main(void) {\n" + " gl_Position = hudmatrix * vvertex;\n" + " color = vcolor;\n" + "}\n", + "varying vec4 color;\n" + "void main(void) {\n" + " gl_FragColor = color;\n" + "}\n"); + standardshaders = false; + + if(!nullshader || !hudshader || !hudnotextureshader) fatal("failed to setup shaders"); + + dummyslot.shader = nullshader; +} + +static const char *findglslmain(const char *s) +{ + const char *main = strstr(s, "main"); + if(!main) return NULL; + for(; main >= s; main--) switch(*main) { case '\r': case '\n': case ';': return main + 1; } + return s; +} + +static void gengenericvariant(Shader &s, const char *sname, const char *vs, const char *ps, int row = 0) +{ + int rowoffset = 0; + bool vschanged = false, pschanged = false; + vector vsv, psv; + vsv.put(vs, strlen(vs)+1); + psv.put(ps, strlen(ps)+1); + + static const int len = strlen("//:variant"), olen = strlen("override"); + for(char *vspragma = vsv.getbuf();; vschanged = true) + { + vspragma = strstr(vspragma, "//:variant"); + if(!vspragma) break; + if(sscanf(vspragma + len, "row %d", &rowoffset) == 1) continue; + memset(vspragma, ' ', len); + vspragma += len; + if(!strncmp(vspragma, "override", olen)) + { + memset(vspragma, ' ', olen); + vspragma += olen; + char *end = vspragma + strcspn(vspragma, "\n\r"); + end += strspn(end, "\n\r"); + int endlen = strcspn(end, "\n\r"); + memset(end, ' ', endlen); + } + } + for(char *pspragma = psv.getbuf();; pschanged = true) + { + pspragma = strstr(pspragma, "//:variant"); + if(!pspragma) break; + if(sscanf(pspragma + len, "row %d", &rowoffset) == 1) continue; + memset(pspragma, ' ', len); + pspragma += len; + if(!strncmp(pspragma, "override", olen)) + { + memset(pspragma, ' ', olen); + pspragma += olen; + char *end = pspragma + strcspn(pspragma, "\n\r"); + end += strspn(end, "\n\r"); + int endlen = strcspn(end, "\n\r"); + memset(end, ' ', endlen); + } + } + row += rowoffset; + if(row < 0 || row >= MAXVARIANTROWS) return; + int col = s.numvariants(row); + defformatstring(varname, "%s", col, row, sname); + string reuse; + if(col) formatstring(reuse, "%d", row); + else copystring(reuse, ""); + newshader(s.type, varname, vschanged ? vsv.getbuf() : reuse, pschanged ? psv.getbuf() : reuse, &s, row); +} + +static bool genwatervariant(Shader &s, const char *sname, const char *vs, const char *ps, int row = 2) +{ + if(!strstr(vs, "//:water") && !strstr(ps, "//:water")) return false; + + vector vsw, psw; + + const char *vsmain = findglslmain(vs), *vsend = strrchr(vs, '}'); + if(!vsmain || !vsend) return false; + vsw.put(vs, vsmain - vs); + const char *fadeparams = "\nuniform vec4 waterfadeparams;\nvarying float fadedepth;\n"; + vsw.put(fadeparams, strlen(fadeparams)); + vsw.put(vsmain, vsend - vsmain); + const char *fadedef = "\nfadedepth = vvertex.z*waterfadeparams.x + waterfadeparams.y;\n"; + vsw.put(fadedef, strlen(fadedef)); + vsw.put(vsend, strlen(vsend)+1); + + const char *psmain = findglslmain(ps), *psend = strrchr(ps, '}'); + if(!psmain || !psend) return false; + psw.put(ps, psmain - ps); + const char *fadeinterp = "\nvarying float fadedepth;\n"; + psw.put(fadeinterp, strlen(fadeinterp)); + psw.put(psmain, psend - psmain); + const char *fade = "\ngl_FragColor.a = fadedepth;\n"; + psw.put(fade, strlen(fade)); + psw.put(psend, strlen(psend)+1); + + defformatstring(name, "%s", sname); + Shader *variant = newshader(s.type, name, vsw.getbuf(), psw.getbuf(), &s, row); + return variant!=NULL; +} + +bool minimizedynlighttcusage() { return glversion < 300 && maxvaryings < 48; } + +static void gendynlightvariant(Shader &s, const char *sname, const char *vs, const char *ps, int row = 0) +{ + int numlights = minimizedynlighttcusage() ? 1 : MAXDYNLIGHTS; + + const char *vspragma = strstr(vs, "//:dynlight"), *pspragma = strstr(ps, "//:dynlight"); + if(!vspragma || !pspragma) return; + + string pslight; + vspragma += strcspn(vspragma, "\n"); + if(*vspragma) vspragma++; + + if(sscanf(pspragma, "//:dynlight %100s", pslight)!=1) return; + + pspragma += strcspn(pspragma, "\n"); + if(*pspragma) pspragma++; + + const char *vsmain = findglslmain(vs), *psmain = findglslmain(ps); + if(vsmain > vspragma) vsmain = vs; + if(psmain > pspragma) psmain = ps; + + vector vsdl, psdl; + loopi(MAXDYNLIGHTS) + { + vsdl.setsize(0); + psdl.setsize(0); + if(vsmain >= vs) vsdl.put(vs, vsmain - vs); + if(psmain >= ps) psdl.put(ps, psmain - ps); + + defformatstring(pos, "uniform vec4 dynlightpos[%d];\n", i+1); + vsdl.put(pos, strlen(pos)); + psdl.put(pos, strlen(pos)); + defformatstring(color, "uniform vec3 dynlightcolor[%d];\n", i+1); + psdl.put(color, strlen(color)); + + loopk(min(i+1, numlights)) + { + defformatstring(dir, "%sdynlight%ddir%s", !k ? "varying vec3 " : " ", k, k==i || k+1==numlights ? ";\n" : ","); + vsdl.put(dir, strlen(dir)); + psdl.put(dir, strlen(dir)); + } + + vsdl.put(vsmain, vspragma-vsmain); + psdl.put(psmain, pspragma-psmain); + + loopk(i+1) + { + defformatstring(tc, + k%s", i+1, sname); + Shader *variant = newshader(s.type, name, vsdl.getbuf(), psdl.getbuf(), &s, row); + if(!variant) return; + if(row < 4) genwatervariant(s, name, vsdl.getbuf(), psdl.getbuf(), row+2); + } +} + +static void genshadowmapvariant(Shader &s, const char *sname, const char *vs, const char *ps, int row = 1) +{ + const char *vspragma = strstr(vs, "//:shadowmap"), *pspragma = strstr(ps, "//:shadowmap"); + if(!vspragma || !pspragma) return; + + string pslight; + vspragma += strcspn(vspragma, "\n"); + if(*vspragma) vspragma++; + + if(sscanf(pspragma, "//:shadowmap %100s", pslight)!=1) return; + + pspragma += strcspn(pspragma, "\n"); + if(*pspragma) pspragma++; + + const char *vsmain = findglslmain(vs), *psmain = findglslmain(ps); + if(vsmain > vspragma) vsmain = vs; + if(psmain > pspragma) psmain = ps; + + vector vssm, pssm; + if(vsmain >= vs) vssm.put(vs, vsmain - vs); + if(psmain >= ps) pssm.put(ps, psmain - ps); + + const char *vsdecl = + "uniform mat4 shadowmapproject;\n" + "varying vec3 shadowmaptc;\n"; + vssm.put(vsdecl, strlen(vsdecl)); + + const char *psdecl = + "uniform sampler2D shadowmap;\n" + "uniform vec4 shadowmapambient;\n" + "varying vec3 shadowmaptc;\n"; + pssm.put(psdecl, strlen(psdecl)); + + vssm.put(vsmain, vspragma-vsmain); + pssm.put(psmain, pspragma-psmain); + + extern int smoothshadowmappeel; + const char *tcgen = + "shadowmaptc = vec3(shadowmapproject * vvertex);\n"; + vssm.put(tcgen, strlen(tcgen)); + const char *sm = + smoothshadowmappeel ? + "vec4 smvals = texture2D(shadowmap, shadowmaptc.xy);\n" + "vec2 smdiff = clamp(smvals.xz - shadowmaptc.zz*smvals.y, 0.0, 1.0);\n" + "float shadowed = clamp((smdiff.x > 0.0 ? smvals.w : 0.0) - 8.0*smdiff.y, 0.0, 1.0);\n" : + + "vec4 smvals = texture2D(shadowmap, shadowmaptc.xy);\n" + "float smtest = shadowmaptc.z*smvals.y;\n" + "float shadowed = smtest < smvals.x && smtest > smvals.z ? smvals.w : 0.0;\n"; + pssm.put(sm, strlen(sm)); + defformatstring(smlight, + "%s.rgb -= shadowed*clamp(%s.rgb - shadowmapambient.rgb, 0.0, 1.0);\n", + pslight, pslight); + pssm.put(smlight, strlen(smlight)); + + vssm.put(vspragma, strlen(vspragma)+1); + pssm.put(pspragma, strlen(pspragma)+1); + + defformatstring(name, "%s", sname); + Shader *variant = newshader(s.type, name, vssm.getbuf(), pssm.getbuf(), &s, row); + if(!variant) return; + genwatervariant(s, name, vssm.getbuf(), pssm.getbuf(), row+2); + + if(strstr(vs, "//:dynlight")) gendynlightvariant(s, name, vssm.getbuf(), pssm.getbuf(), row); +} + +static void genfogshader(vector &vsbuf, vector &psbuf, const char *vs, const char *ps) +{ + const char *vspragma = strstr(vs, "//:fog"), *pspragma = strstr(ps, "//:fog"); + if(!vspragma && !pspragma) return; + static const int pragmalen = strlen("//:fog"); + const char *vsmain = findglslmain(vs), *vsend = strrchr(vs, '}'); + if(vsmain && vsend) + { + vsbuf.put(vs, vsmain - vs); + const char *fogparams = "\nuniform vec4 fogplane;\nvarying float fogcoord;\n"; + vsbuf.put(fogparams, strlen(fogparams)); + vsbuf.put(vsmain, vsend - vsmain); + const char *vsfog = "\nfogcoord = dot(fogplane, gl_Position);\n"; + vsbuf.put(vsfog, strlen(vsfog)); + vsbuf.put(vsend, strlen(vsend)+1); + } + const char *psmain = findglslmain(ps), *psend = strrchr(ps, '}'); + if(psmain && psend) + { + psbuf.put(ps, psmain - ps); + const char *fogparams = + "\nuniform vec3 fogcolor;\n" + "uniform vec2 fogparams;\n" + "varying float fogcoord;\n"; + psbuf.put(fogparams, strlen(fogparams)); + psbuf.put(psmain, psend - psmain); + const char *psdef = "\n#define FOG_COLOR "; + const char *psfog = + pspragma && !strncmp(pspragma+pragmalen, "rgba", 4) ? + "\ngl_FragColor = mix((FOG_COLOR), gl_FragColor, clamp(fogcoord*fogparams.x + fogparams.y, 0.0, 1.0));\n" : + "\ngl_FragColor.rgb = mix((FOG_COLOR).rgb, gl_FragColor.rgb, clamp(fogcoord*fogparams.x + fogparams.y, 0.0, 1.0));\n"; + int clen = 0; + if(pspragma) + { + pspragma += pragmalen; + while(iscubealpha(*pspragma)) pspragma++; + while(*pspragma && !iscubespace(*pspragma)) pspragma++; + pspragma += strspn(pspragma, " \t\v\f"); + clen = strcspn(pspragma, "\r\n"); + } + if(clen <= 0) { pspragma = "fogcolor"; clen = strlen(pspragma); } + psbuf.put(psdef, strlen(psdef)); + psbuf.put(pspragma, clen); + psbuf.put(psfog, strlen(psfog)); + psbuf.put(psend, strlen(psend)+1); + } +} + +static void genuniformdefs(vector &vsbuf, vector &psbuf, const char *vs, const char *ps, Shader *variant = NULL) +{ + if(variant ? variant->defaultparams.empty() : slotparams.empty()) return; + const char *vsmain = findglslmain(vs), *psmain = findglslmain(ps); + if(!vsmain || !psmain) return; + vsbuf.put(vs, vsmain - vs); + psbuf.put(ps, psmain - ps); + if(variant) loopv(variant->defaultparams) + { + defformatstring(uni, "\nuniform vec4 %s;\n", variant->defaultparams[i].name); + vsbuf.put(uni, strlen(uni)); + psbuf.put(uni, strlen(uni)); + } + else loopv(slotparams) + { + defformatstring(uni, "\nuniform vec4 %s;\n", slotparams[i].name); + vsbuf.put(uni, strlen(uni)); + psbuf.put(uni, strlen(uni)); + } + vsbuf.put(vsmain, strlen(vsmain)+1); + psbuf.put(psmain, strlen(psmain)+1); +} + +VAR(defershaders, 0, 1, 1); + +void defershader(int *type, const char *name, const char *contents) +{ + Shader *exists = shaders.access(name); + if(exists && !exists->invalid()) return; + if(!defershaders) { execute(contents); return; } + char *rname = exists ? exists->name : newstring(name); + Shader &s = shaders[rname]; + s.name = rname; + DELETEA(s.defer); + s.defer = newstring(contents); + s.type = SHADER_DEFERRED | *type; + s.standard = standardshaders; +} + +void Shader::force() +{ + if(!deferred() || !defer) return; + + char *cmd = defer; + defer = NULL; + bool wasstandard = standardshaders, wasforcing = forceshaders; + int oldflags = identflags; + standardshaders = standard; + forceshaders = false; + identflags &= ~IDF_PERSIST; + slotparams.shrink(0); + execute(cmd); + identflags = oldflags; + forceshaders = wasforcing; + standardshaders = wasstandard; + delete[] cmd; + + if(deferred()) + { + DELETEA(defer); + type = SHADER_INVALID; + } +} + +void fixshaderdetail() +{ + // must null out separately because fixdetailshader can recursively set it + enumerate(shaders, Shader, s, { if(!s.forced) s.detailshader = NULL; }); + enumerate(shaders, Shader, s, { if(s.forced) s.fixdetailshader(); }); + linkslotshaders(); +} + +int Shader::uniformlocversion() +{ + static int version = 0; + if(++version >= 0) return version; + version = 0; + enumerate(shaders, Shader, s, { loopvj(s.uniformlocs) s.uniformlocs[j].version = -1; }); + return version; +} + +VARFP(shaderdetail, 0, MAXSHADERDETAIL, MAXSHADERDETAIL, fixshaderdetail()); + +void Shader::fixdetailshader(bool shouldforce, bool recurse) +{ + Shader *alt = this; + detailshader = NULL; + do + { + Shader *cur = shaderdetail < MAXSHADERDETAIL ? alt->fastshader[shaderdetail] : alt; + if(cur->deferred() && shouldforce) cur->force(); + if(!cur->invalid()) + { + if(cur->deferred()) break; + detailshader = cur; + break; + } + alt = alt->altshader; + } while(alt && alt!=this); + + if(recurse && detailshader) loopv(detailshader->variants) detailshader->variants[i]->fixdetailshader(shouldforce, false); +} + +Shader *useshaderbyname(const char *name) +{ + Shader *s = shaders.access(name); + if(!s) return NULL; + if(!s->detailshader) s->fixdetailshader(); + s->forced = true; + return s; +} + +void shader(int *type, char *name, char *vs, char *ps) +{ + if(lookupshaderbyname(name)) return; + + defformatstring(info, "shader %s", name); + renderprogress(loadprogress, info); + + vector vsbuf, psbuf, vsbak, psbak; +#define GENSHADER(cond, body) \ + if(cond) \ + { \ + if(vsbuf.length()) { vsbak.setsize(0); vsbak.put(vs, strlen(vs)+1); vs = vsbak.getbuf(); vsbuf.setsize(0); } \ + if(psbuf.length()) { psbak.setsize(0); psbak.put(ps, strlen(ps)+1); ps = psbak.getbuf(); psbuf.setsize(0); } \ + body; \ + if(vsbuf.length()) vs = vsbuf.getbuf(); \ + if(psbuf.length()) ps = psbuf.getbuf(); \ + } + GENSHADER(slotparams.length(), genuniformdefs(vsbuf, psbuf, vs, ps)); + GENSHADER(strstr(vs, "//:fog") || strstr(ps, "//:fog"), genfogshader(vsbuf, psbuf, vs, ps)); + Shader *s = newshader(*type, name, vs, ps); + if(s) + { + if(strstr(vs, "//:water")) genwatervariant(*s, s->name, vs, ps); + if(strstr(vs, "//:shadowmap")) genshadowmapvariant(*s, s->name, vs, ps); + if(strstr(vs, "//:dynlight")) gendynlightvariant(*s, s->name, vs, ps); + } + slotparams.shrink(0); +} + +void variantshader(int *type, char *name, int *row, char *vs, char *ps) +{ + if(*row < 0) + { + shader(type, name, vs, ps); + return; + } + else if(*row >= MAXVARIANTROWS) return; + + Shader *s = lookupshaderbyname(name); + if(!s) return; + + defformatstring(varname, "%s", s->numvariants(*row), *row, name); + //defformatstring(info, "shader %s", varname); + //renderprogress(loadprogress, info); + vector vsbuf, psbuf, vsbak, psbak; + GENSHADER(s->defaultparams.length(), genuniformdefs(vsbuf, psbuf, vs, ps, s)); + GENSHADER(strstr(vs, "//:fog") || strstr(ps, "//:fog"), genfogshader(vsbuf, psbuf, vs, ps)); + Shader *v = newshader(*type, varname, vs, ps, s, *row); + if(v) + { + if(strstr(vs, "//:dynlight")) gendynlightvariant(*s, varname, vs, ps, *row); + if(strstr(ps, "//:variant") || strstr(vs, "//:variant")) gengenericvariant(*s, varname, vs, ps, *row); + } +} + +void setshader(char *name) +{ + slotparams.shrink(0); + Shader *s = shaders.access(name); + if(!s) + { + conoutf(CON_ERROR, "no such shader: %s", name); + } + else slotshader = s; +} + +void resetslotshader() +{ + slotshader = NULL; + slotparams.shrink(0); +} + +void setslotshader(Slot &s) +{ + s.shader = slotshader; + if(!s.shader) + { + s.shader = stdworldshader; + return; + } + loopv(slotparams) s.params.add(slotparams[i]); +} + +static void linkslotshaderparams(vector ¶ms, Shader *sh, bool load) +{ + if(sh) loopv(params) + { + int loc = -1; + SlotShaderParam ¶m = params[i]; + loopv(sh->defaultparams) + { + SlotShaderParamState &dparam = sh->defaultparams[i]; + if(dparam.name==param.name) + { + if(memcmp(param.val, dparam.val, sizeof(param.val))) loc = i; + break; + } + } + param.loc = loc; + } + else if(load) loopv(params) params[i].loc = -1; +} + +void linkslotshader(Slot &s, bool load) +{ + if(!s.shader) return; + + if(load && !s.shader->detailshader) s.shader->fixdetailshader(); + + linkslotshaderparams(s.params, s.shader->detailshader, load); +} + +void linkvslotshader(VSlot &s, bool load) +{ + if(!s.slot->shader) return; + + Shader *sh = s.slot->shader->detailshader; + linkslotshaderparams(s.params, sh, load); + + if(!sh) return; + + if(s.slot->texmask&(1<altshader = alt; + orig->fixdetailshader(false); +} + +void fastshader(char *nice, char *fast, int *detail) +{ + Shader *ns = shaders.access(nice), *fs = shaders.access(fast); + if(!ns || !fs) return; + loopi(min(*detail+1, MAXSHADERDETAIL)) ns->fastshader[i] = fs; + ns->fixdetailshader(false); +} + +COMMAND(shader, "isss"); +COMMAND(variantshader, "isiss"); +COMMAND(setshader, "s"); +COMMAND(altshader, "ss"); +COMMAND(fastshader, "ssi"); +COMMAND(defershader, "iss"); +ICOMMAND(forceshader, "s", (const char *name), useshaderbyname(name)); + +ICOMMAND(isshaderdefined, "s", (char *name), intret(lookupshaderbyname(name) ? 1 : 0)); + +static hashset shaderparamnames(256); + +const char *getshaderparamname(const char *name, bool insert) +{ + const char *exists = shaderparamnames.find(name, NULL); + if(exists || !insert) return exists; + return shaderparamnames.add(newstring(name)); +} + +void addslotparam(const char *name, float x, float y, float z, float w) +{ + if(name) name = getshaderparamname(name); + loopv(slotparams) + { + SlotShaderParam ¶m = slotparams[i]; + if(param.name==name) + { + param.val[0] = x; + param.val[1] = y; + param.val[2] = z; + param.val[3] = w; + return; + } + } + SlotShaderParam param = {name, -1, {x, y, z, w}}; + slotparams.add(param); +} + +ICOMMAND(setuniformparam, "sffff", (char *name, float *x, float *y, float *z, float *w), addslotparam(name, *x, *y, *z, *w)); +ICOMMAND(setshaderparam, "sffff", (char *name, float *x, float *y, float *z, float *w), addslotparam(name, *x, *y, *z, *w)); +ICOMMAND(defuniformparam, "sffff", (char *name, float *x, float *y, float *z, float *w), addslotparam(name, *x, *y, *z, *w)); + +#define NUMPOSTFXBINDS 10 + +struct postfxtex +{ + GLuint id; + int scale, used; + + postfxtex() : id(0), scale(0), used(-1) {} +}; +vector postfxtexs; +int postfxbinds[NUMPOSTFXBINDS]; +GLuint postfxfb = 0; +int postfxw = 0, postfxh = 0; + +struct postfxpass +{ + Shader *shader; + vec4 params; + uint inputs, freeinputs; + int outputbind, outputscale; + + postfxpass() : shader(NULL), inputs(1), freeinputs(1), outputbind(0), outputscale(0) {} +}; +vector postfxpasses; + +static int allocatepostfxtex(int scale) +{ + loopv(postfxtexs) + { + postfxtex &t = postfxtexs[i]; + if(t.scale==scale && t.used < 0) return i; + } + postfxtex &t = postfxtexs.add(); + t.scale = scale; + glGenTextures(1, &t.id); + createtexture(t.id, max(screenw>>scale, 1), max(screenh>>scale, 1), NULL, 3, 1, GL_RGB); + return postfxtexs.length()-1; +} + +void cleanuppostfx(bool fullclean) +{ + if(fullclean && postfxfb) + { + glDeleteFramebuffers_(1, &postfxfb); + postfxfb = 0; + } + + loopv(postfxtexs) glDeleteTextures(1, &postfxtexs[i].id); + postfxtexs.shrink(0); + + postfxw = 0; + postfxh = 0; +} + +void renderpostfx() +{ + if(postfxpasses.empty()) return; + + if(postfxw != screenw || postfxh != screenh) + { + cleanuppostfx(false); + postfxw = screenw; + postfxh = screenh; + } + + int binds[NUMPOSTFXBINDS]; + loopi(NUMPOSTFXBINDS) binds[i] = -1; + loopv(postfxtexs) postfxtexs[i].used = -1; + + binds[0] = allocatepostfxtex(0); + postfxtexs[binds[0]].used = 0; + glBindTexture(GL_TEXTURE_2D, postfxtexs[binds[0]].id); + glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, screenw, screenh); + + if(postfxpasses.length() > 1) + { + if(!postfxfb) glGenFramebuffers_(1, &postfxfb); + glBindFramebuffer_(GL_FRAMEBUFFER, postfxfb); + } + + GLOBALPARAMF(millis, lastmillis/1000.0f); + + loopv(postfxpasses) + { + postfxpass &p = postfxpasses[i]; + + int tex = -1; + if(!postfxpasses.inrange(i+1)) + { + if(postfxpasses.length() > 1) glBindFramebuffer_(GL_FRAMEBUFFER, 0); + } + else + { + tex = allocatepostfxtex(p.outputscale); + glFramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, postfxtexs[tex].id, 0); + } + + int w = tex >= 0 ? max(screenw>>postfxtexs[tex].scale, 1) : screenw, + h = tex >= 0 ? max(screenh>>postfxtexs[tex].scale, 1) : screenh; + glViewport(0, 0, w, h); + p.shader->set(); + LOCALPARAM(params, p.params); + int tw = w, th = h, tmu = 0; + loopj(NUMPOSTFXBINDS) if(p.inputs&(1<= 0) + { + if(!tmu) + { + tw = max(screenw>>postfxtexs[binds[j]].scale, 1); + th = max(screenh>>postfxtexs[binds[j]].scale, 1); + } + else glActiveTexture_(GL_TEXTURE0 + tmu); + glBindTexture(GL_TEXTURE_2D, postfxtexs[binds[j]].id); + ++tmu; + } + if(tmu) glActiveTexture_(GL_TEXTURE0); + LOCALPARAMF(postfxscale, 1.0f/tw, 1.0f/th); + screenquad(1, 1); + + loopj(NUMPOSTFXBINDS) if(p.freeinputs&(1<= 0) + { + postfxtexs[binds[j]].used = -1; + binds[j] = -1; + } + if(tex >= 0) + { + if(binds[p.outputbind] >= 0) postfxtexs[binds[p.outputbind]].used = -1; + binds[p.outputbind] = tex; + postfxtexs[tex].used = p.outputbind; + } + } +} + +static bool addpostfx(const char *name, int outputbind, int outputscale, uint inputs, uint freeinputs, const vec4 ¶ms) +{ + if(!*name) return false; + Shader *s = useshaderbyname(name); + if(!s) + { + conoutf(CON_ERROR, "no such postfx shader: %s", name); + return false; + } + postfxpass &p = postfxpasses.add(); + p.shader = s; + p.outputbind = outputbind; + p.outputscale = outputscale; + p.inputs = inputs; + p.freeinputs = freeinputs; + p.params = params; + return true; +} + +void clearpostfx() +{ + postfxpasses.shrink(0); + cleanuppostfx(false); +} + +COMMAND(clearpostfx, ""); + +ICOMMAND(addpostfx, "siisffff", (char *name, int *bind, int *scale, char *inputs, float *x, float *y, float *z, float *w), +{ + int inputmask = inputs[0] ? 0 : 1; + int freemask = inputs[0] ? 0 : 1; + bool freeinputs = true; + for(; *inputs; inputs++) if(isdigit(*inputs)) + { + inputmask |= 1<<(*inputs-'0'); + if(freeinputs) freemask |= 1<<(*inputs-'0'); + } + else if(*inputs=='+') freeinputs = false; + else if(*inputs=='-') freeinputs = true; + inputmask &= (1<reusevs && v->reusevs->invalid()) || + (v->reuseps && v->reuseps->invalid()) || + !v->compile()) + v->cleanup(true); + } + } + if(s.forced && !s.detailshader) s.fixdetailshader(); + }); +} + +void setupblurkernel(int radius, float sigma, float *weights, float *offsets) +{ + if(radius<1 || radius>MAXBLURRADIUS) return; + sigma *= 2*radius; + float total = 1.0f/sigma; + weights[0] = total; + offsets[0] = 0; + // rely on bilinear filtering to sample 2 pixels at once + // transforms a*X + b*Y into (u+v)*[X*u/(u+v) + Y*(1 - u/(u+v))] + loopi(radius) + { + float weight1 = exp(-((2*i)*(2*i)) / (2*sigma*sigma)) / sigma, + weight2 = exp(-((2*i+1)*(2*i+1)) / (2*sigma*sigma)) / sigma, + scale = weight1 + weight2, + offset = 2*i+1 + weight2 / scale; + weights[i+1] = scale; + offsets[i+1] = offset; + total += 2*scale; + } + loopi(radius+1) weights[i] /= total; + for(int i = radius+1; i <= MAXBLURRADIUS; i++) weights[i] = offsets[i] = 0; +} + +void setblurshader(int pass, int size, int radius, float *weights, float *offsets) +{ + if(radius<1 || radius>MAXBLURRADIUS) return; + static Shader *blurshader[7][2] = { { NULL, NULL }, { NULL, NULL }, { NULL, NULL }, { NULL, NULL }, { NULL, NULL }, { NULL, NULL }, { NULL, NULL } }; + Shader *&s = blurshader[radius-1][pass]; + if(!s) + { + defformatstring(name, "blur%c%d", 'x'+pass, radius); + s = lookupshaderbyname(name); + } + s->set(); + LOCALPARAMV(weights, weights, 8); + float scaledoffsets[8]; + loopk(8) scaledoffsets[k] = offsets[k]/size; + LOCALPARAMV(offsets, scaledoffsets, 8); +} + diff --git a/src/engine/shadowmap.cpp b/src/engine/shadowmap.cpp new file mode 100644 index 0000000..4dafbd8 --- /dev/null +++ b/src/engine/shadowmap.cpp @@ -0,0 +1,329 @@ +#include "engine.h" +#include "rendertarget.h" + +VARP(shadowmap, 0, 0, 1); + +extern void cleanshadowmap(); +VARFP(shadowmapsize, 7, 9, 11, cleanshadowmap()); +VARP(shadowmapradius, 64, 96, 256); +VAR(shadowmapheight, 0, 32, 128); +VARP(shadowmapdist, 128, 256, 512); +VARFP(fpshadowmap, 0, 0, 1, cleanshadowmap()); +VARFP(shadowmapprecision, 0, 0, 1, cleanshadowmap()); +bvec shadowmapambientcolor(0, 0, 0); +HVARFR(shadowmapambient, 0, 0, 0xFFFFFF, +{ + if(shadowmapambient <= 255) shadowmapambient |= (shadowmapambient<<8) | (shadowmapambient<<16); + shadowmapambientcolor = bvec((shadowmapambient>>16)&0xFF, (shadowmapambient>>8)&0xFF, shadowmapambient&0xFF); +}); +VARP(shadowmapintensity, 0, 40, 100); + +VARP(blurshadowmap, 0, 1, 3); +VARP(blursmsigma, 1, 100, 200); + +#define SHADOWSKEW 0.7071068f + +vec shadowoffset(0, 0, 0), shadowfocus(0, 0, 0), shadowdir(0, SHADOWSKEW, 1); +VAR(shadowmapcasters, 1, 0, 0); +float shadowmapmaxz = 0; + +void setshadowdir(int angle) +{ + shadowdir = vec(0, SHADOWSKEW, 1); + shadowdir.rotate_around_z(angle*RAD); +} + +VARFR(shadowmapangle, 0, 0, 360, setshadowdir(shadowmapangle)); + +void guessshadowdir() +{ + if(shadowmapangle) return; + vec dir; + if(!sunlightcolor.iszero()) dir = sunlightdir; + else + { + vec lightpos(0, 0, 0), casterpos(0, 0, 0); + int numlights = 0, numcasters = 0; + const vector &ents = entities::getents(); + loopv(ents) + { + extentity &e = *ents[i]; + switch(e.type) + { + case ET_LIGHT: + if(!e.attr1) { lightpos.add(e.o); numlights++; } + break; + + case ET_MAPMODEL: + casterpos.add(e.o); + numcasters++; + break; + + default: + if(e.typeyaw*RAD); + + vec dir; + vecfromyawpitch(camera1->yaw, camera1->pitch, 1, 0, dir); + dir.z = 0; + dir.mul(shadowmapradius); + + vec dirx, diry; + vecfromyawpitch(camera1->yaw, 0, 0, 1, dirx); + vecfromyawpitch(camera1->yaw, 0, 1, 0, diry); + shadowoffset.x = -fmod(dirx.dot(camera1->o) - skewdir.x*camera1->o.z, 2.0f*shadowmapradius/vieww); + shadowoffset.y = -fmod(diry.dot(camera1->o) - skewdir.y*camera1->o.z, 2.0f*shadowmapradius/viewh); + + shadowmatrix.ortho(-shadowmapradius, shadowmapradius, -shadowmapradius, shadowmapradius, -shadowmapdist, shadowmapdist); + shadowmatrix.mul(matrix3(vec(1, 0, 0), vec(0, 1, 0), vec(skewdir.x, skewdir.y, 1))); + shadowmatrix.translate(skewdir.x*shadowmapheight + shadowoffset.x, skewdir.y*shadowmapheight + shadowoffset.y + dir.magnitude(), -shadowmapheight); + shadowmatrix.rotate_around_z((camera1->yaw+180)*-RAD); + shadowmatrix.translate(vec(camera1->o).neg()); + GLOBALPARAM(shadowmatrix, shadowmatrix); + + shadowfocus = camera1->o; + shadowfocus.add(dir); + shadowfocus.add(vec(shadowdir).mul(shadowmapheight)); + shadowfocus.add(dirx.mul(shadowoffset.x)); + shadowfocus.add(diry.mul(shadowoffset.y)); + + gle::colorf(0, 0, 0); + + GLOBALPARAMF(shadowmapbias, -shadowmapbias/float(shadowmapdist), 1 - (shadowmapbias + (smoothshadowmappeel ? 0 : shadowmappeelbias))/float(shadowmapdist)); + + shadowmapcasters = 0; + shadowmapmaxz = shadowfocus.z - shadowmapdist; + shadowmapping = true; + rendergame(); + shadowmapping = false; + shadowmapmaxz = min(shadowmapmaxz, shadowfocus.z); + + if(shadowmapcasters && smdepthpeel) + { + int sx, sy, sw, sh; + bool scissoring = rtscissor && scissorblur(sx, sy, sw, sh) && sw > 0 && sh > 0; + if(scissoring) glScissor(sx, sy, sw, sh); + if(!rtscissor || scissoring) rendershadowmapreceivers(); + } + + return shadowmapcasters>0; + } + + bool flipdebug() const { return false; } + + void dodebug(int w, int h) + { + if(shadowmapcasters) + { + glColorMask(GL_TRUE, GL_FALSE, GL_FALSE, GL_FALSE); + debugscissor(w, h); + glColorMask(GL_FALSE, GL_FALSE, GL_TRUE, GL_FALSE); + debugblurtiles(w, h); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + } + } +} shadowmaptex; + +void cleanshadowmap() +{ + shadowmaptex.cleanup(true); +} + +void calcshadowmapbb(const vec &o, float xyrad, float zrad, float &x1, float &y1, float &x2, float &y2) +{ + vec skewdir(shadowdir); + skewdir.rotate_around_z(-camera1->yaw*RAD); + + vec ro(o); + ro.sub(camera1->o); + ro.rotate_around_z(-(camera1->yaw+180)*RAD); + ro.x += ro.z * skewdir.x + shadowoffset.x; + ro.y += ro.z * skewdir.y + shadowmapradius * cosf(camera1->pitch*RAD) + shadowoffset.y; + + vec high(ro), low(ro); + high.x += zrad * skewdir.x; + high.y += zrad * skewdir.y; + low.x -= zrad * skewdir.x; + low.y -= zrad * skewdir.y; + + x1 = (min(high.x, low.x) - xyrad) / shadowmapradius; + y1 = (min(high.y, low.y) - xyrad) / shadowmapradius; + x2 = (max(high.x, low.x) + xyrad) / shadowmapradius; + y2 = (max(high.y, low.y) + xyrad) / shadowmapradius; +} + +bool addshadowmapcaster(const vec &o, float xyrad, float zrad) +{ + if(o.z + zrad <= shadowfocus.z - shadowmapdist || o.z - zrad >= shadowfocus.z) return false; + + shadowmapmaxz = max(shadowmapmaxz, o.z + zrad); + + float x1, y1, x2, y2; + calcshadowmapbb(o, xyrad, zrad, x1, y1, x2, y2); + + if(!shadowmaptex.addblurtiles(x1, y1, x2, y2, 2)) return false; + + shadowmapcasters++; + return true; +} + +bool isshadowmapreceiver(vtxarray *va) +{ + if(!shadowmap || !shadowmapcasters) return false; + + if(va->shadowmapmax.z <= shadowfocus.z - shadowmapdist || va->shadowmapmin.z >= shadowmapmaxz) return false; + + float xyrad = SQRT2*0.5f*max(va->shadowmapmax.x-va->shadowmapmin.x, va->shadowmapmax.y-va->shadowmapmin.y), + zrad = 0.5f*(va->shadowmapmax.z-va->shadowmapmin.z), + x1, y1, x2, y2; + if(xyrad<0 || zrad<0) return false; + + vec center = vec(va->shadowmapmin).add(vec(va->shadowmapmax)).mul(0.5f); + calcshadowmapbb(center, xyrad, zrad, x1, y1, x2, y2); + + return shadowmaptex.checkblurtiles(x1, y1, x2, y2, 2); + +#if 0 + // cheaper inexact test + float dz = va->o.z + va->size/2 - shadowfocus.z; + float cx = shadowfocus.x + dz*shadowdir.x, cy = shadowfocus.y + dz*shadowdir.y; + float skew = va->size/2*SHADOWSKEW; + if(!shadowmap || !shadowmaptex || + va->o.z + va->size <= shadowfocus.z - shadowmapdist || va->o.z >= shadowmapmaxz || + va->o.x + va->size <= cx - shadowmapradius-skew || va->o.x >= cx + shadowmapradius+skew || + va->o.y + va->size <= cy - shadowmapradius-skew || va->o.y >= cy + shadowmapradius+skew) + return false; + return true; +#endif +} + +bool isshadowmapcaster(const vec &o, float rad) +{ + // cheaper inexact test + float dz = o.z - shadowfocus.z; + float cx = shadowfocus.x + dz*shadowdir.x, cy = shadowfocus.y + dz*shadowdir.y; + float skew = rad*SHADOWSKEW; + if(!shadowmapping || + o.z + rad <= shadowfocus.z - shadowmapdist || o.z - rad >= shadowfocus.z || + o.x + rad <= cx - shadowmapradius-skew || o.x - rad >= cx + shadowmapradius+skew || + o.y + rad <= cy - shadowmapradius-skew || o.y - rad >= cy + shadowmapradius+skew) + return false; + return true; +} + +void pushshadowmap() +{ + if(!shadowmap || !shadowmaptex.rendertex) return; + + glActiveTexture_(GL_TEXTURE7); + glBindTexture(GL_TEXTURE_2D, shadowmaptex.rendertex); + + matrix4 m = shadowmatrix; + m.projective(-1, 1-shadowmapbias/float(shadowmapdist)); + GLOBALPARAM(shadowmapproject, m); + + glActiveTexture_(GL_TEXTURE0); + + float r, g, b; + if(!shadowmapambient) + { + if(skylightcolor[0] || skylightcolor[1] || skylightcolor[2]) + { + r = max(25.0f, 0.4f*ambientcolor[0] + 0.6f*max(ambientcolor[0], skylightcolor[0])); + g = max(25.0f, 0.4f*ambientcolor[1] + 0.6f*max(ambientcolor[1], skylightcolor[1])); + b = max(25.0f, 0.4f*ambientcolor[2] + 0.6f*max(ambientcolor[2], skylightcolor[2])); + } + else + { + r = max(25.0f, 2.0f*ambientcolor[0]); + g = max(25.0f, 2.0f*ambientcolor[1]); + b = max(25.0f, 2.0f*ambientcolor[2]); + } + } + else { r = shadowmapambientcolor[0]; g = shadowmapambientcolor[1]; b = shadowmapambientcolor[2]; } + GLOBALPARAMF(shadowmapambient, r/255.0f, g/255.0f, b/255.0f); +} + +void popshadowmap() +{ + if(!shadowmap || !shadowmaptex.rendertex) return; +} + +void rendershadowmap() +{ + if(!shadowmap) return; + + shadowmaptex.render(1< weights[k]) + { + for(int l = min(sorted-1, 2); l >= k; l--) + { + weights[l+1] = weights[l]; + bones[l+1] = bones[l]; + } + weights[k] = weight; + bones[k] = bone; + return sorted<4 ? sorted+1 : sorted; + } + if(sorted>=4) return sorted; + weights[sorted] = weight; + bones[sorted] = bone; + return sorted+1; + } + + void finalize(int sorted) + { + loopj(4-sorted) { weights[sorted+j] = 0; bones[sorted+j] = 0; } + if(sorted <= 0) return; + float total = 0; + loopj(sorted) total += weights[j]; + total = 1.0f/total; + loopj(sorted) weights[j] *= total; + } + + void serialize(vvertw &v) + { + if(interpindex >= 0) + { + v.weights[0] = 255; + loopk(3) v.weights[k+1] = 0; + v.bones[0] = 2*interpindex; + loopk(3) v.bones[k+1] = v.bones[0]; + } + else + { + int total = 0; + loopk(4) total += (v.weights[k] = uchar(0.5f + weights[k]*255)); + while(total > 255) + { + loopk(4) if(v.weights[k] > 0 && total > 255) { v.weights[k]--; total--; } + } + while(total < 255) + { + loopk(4) if(v.weights[k] < 255 && total < 255) { v.weights[k]++; total++; } + } + loopk(4) v.bones[k] = 2*interpbones[k]; + } + } + }; + + + struct animcacheentry + { + animstate as[MAXANIMPARTS]; + float pitch; + int millis; + uchar *partmask; + ragdolldata *ragdoll; + + animcacheentry() : ragdoll(NULL) + { + loopk(MAXANIMPARTS) as[k].cur.fr1 = as[k].prev.fr1 = -1; + } + + bool operator==(const animcacheentry &c) const + { + loopi(MAXANIMPARTS) if(as[i]!=c.as[i]) return false; + return pitch==c.pitch && partmask==c.partmask && ragdoll==c.ragdoll && (!ragdoll || min(millis, c.millis) >= ragdoll->lastmove); + } + }; + + struct vbocacheentry : animcacheentry + { + GLuint vbuf; + int owner; + + vbocacheentry() : vbuf(0), owner(-1) {} + }; + + struct skelcacheentry : animcacheentry + { + dualquat *bdata; + int version; + bool dirty; + + skelcacheentry() : bdata(NULL), version(-1), dirty(false) {} + + void nextversion() + { + version = Shader::uniformlocversion(); + dirty = true; + } + }; + + struct blendcacheentry : skelcacheentry + { + int owner; + + blendcacheentry() : owner(-1) {} + }; + + struct skelmeshgroup; + + struct skelmesh : mesh + { + vert *verts; + bumpvert *bumpverts; + tri *tris; + int numverts, numtris, maxweights; + + int voffset, eoffset, elen; + ushort minvert, maxvert; + + skelmesh() : verts(NULL), bumpverts(NULL), tris(NULL), numverts(0), numtris(0), maxweights(0) + { + } + + virtual ~skelmesh() + { + DELETEA(verts); + DELETEA(bumpverts); + DELETEA(tris); + } + + int addblendcombo(const blendcombo &c) + { + maxweights = max(maxweights, c.size()); + return ((skelmeshgroup *)group)->addblendcombo(c); + } + + void smoothnorms(float limit = 0, bool areaweight = true) + { + mesh::smoothnorms(verts, numverts, tris, numtris, limit, areaweight); + } + + void buildnorms(bool areaweight = true) + { + mesh::buildnorms(verts, numverts, tris, numtris, areaweight); + } + + void calctangents(bool areaweight = true) + { + if(bumpverts) return; + bumpverts = new bumpvert[numverts]; + mesh::calctangents(bumpverts, verts, verts, numverts, tris, numtris, areaweight); + } + + void calcbb(vec &bbmin, vec &bbmax, const matrix4x3 &m) + { + loopj(numverts) + { + vec v = m.transform(verts[j].pos); + loopi(3) + { + bbmin[i] = min(bbmin[i], v[i]); + bbmax[i] = max(bbmax[i], v[i]); + } + } + } + + void genBIH(BIH::mesh &m) + { + m.tris = (const BIH::tri *)tris; + m.numtris = numtris; + m.pos = (const uchar *)&verts->pos; + m.posstride = sizeof(vert); + m.tc = (const uchar *)&verts->tc; + m.tcstride = sizeof(vert); + } + + static inline void assignvert(vvertn &vv, int j, vert &v, blendcombo &c) + { + vv.pos = v.pos; + vv.norm = v.norm; + vv.tc = v.tc; + } + + inline void assignvert(vvertbump &vv, int j, vert &v, blendcombo &c) + { + vv.pos = v.pos; + vv.tc = v.tc; + vv.tangent = bumpverts[j].tangent; + } + + static inline void assignvert(vvertnw &vv, int j, vert &v, blendcombo &c) + { + vv.pos = v.pos; + vv.norm = v.norm; + vv.tc = v.tc; + c.serialize(vv); + } + + inline void assignvert(vvertbumpw &vv, int j, vert &v, blendcombo &c) + { + vv.pos = v.pos; + vv.tc = v.tc; + vv.tangent = bumpverts[j].tangent; + c.serialize(vv); + } + + template + int genvbo(vector &idxs, int offset, vector &vverts) + { + voffset = offset; + eoffset = idxs.length(); + loopi(numverts) + { + vert &v = verts[i]; + assignvert(vverts.add(), i, v, ((skelmeshgroup *)group)->blendcombos[v.blend]); + } + loopi(numtris) loopj(3) idxs.add(voffset + tris[i].vert[j]); + elen = idxs.length()-eoffset; + minvert = voffset; + maxvert = voffset + numverts-1; + return numverts; + } + + template + int genvbo(vector &idxs, int offset, vector &vverts, int *htdata, int htlen) + { + voffset = offset; + eoffset = idxs.length(); + minvert = 0xFFFF; + loopi(numtris) + { + tri &t = tris[i]; + loopj(3) + { + int index = t.vert[j]; + vert &v = verts[index]; + T vv; + assignvert(vv, index, v, ((skelmeshgroup *)group)->blendcombos[v.blend]); + int htidx = hthash(v.pos)&(htlen-1); + loopk(htlen) + { + int &vidx = htdata[(htidx+k)&(htlen-1)]; + if(vidx < 0) { vidx = idxs.add(ushort(vverts.length())); vverts.add(vv); break; } + else if(!memcmp(&vverts[vidx], &vv, sizeof(vv))) { minvert = min(minvert, idxs.add(ushort(vidx))); break; } + } + } + } + elen = idxs.length()-eoffset; + minvert = min(minvert, ushort(voffset)); + maxvert = max(minvert, ushort(vverts.length()-1)); + return vverts.length()-voffset; + } + + int genvbo(vector &idxs, int offset) + { + loopi(numverts) verts[i].interpindex = ((skelmeshgroup *)group)->remapblend(verts[i].blend); + + voffset = offset; + eoffset = idxs.length(); + loopi(numtris) + { + tri &t = tris[i]; + loopj(3) idxs.add(voffset+t.vert[j]); + } + minvert = voffset; + maxvert = voffset + numverts-1; + elen = idxs.length()-eoffset; + return numverts; + } + + template + static inline void fillvert(T &vv, int j, vert &v) + { + vv.tc = v.tc; + } + + template + void fillverts(T *vdata) + { + vdata += voffset; + loopi(numverts) fillvert(vdata[i], i, verts[i]); + } + + void interpverts(const dualquat * RESTRICT bdata1, const dualquat * RESTRICT bdata2, bool tangents, void * RESTRICT vdata, skin &s) + { + const int blendoffset = ((skelmeshgroup *)group)->skel->numgpubones; + bdata2 -= blendoffset; + + #define IPLOOP(type, dosetup, dotransform) \ + loopi(numverts) \ + { \ + const vert &src = verts[i]; \ + type &dst = ((type * RESTRICT)vdata)[i]; \ + dosetup; \ + const dualquat &b = (src.interpindex < blendoffset ? bdata1 : bdata2)[src.interpindex]; \ + dst.pos = b.transform(src.pos); \ + dotransform; \ + } + + if(tangents) + { + IPLOOP(vvertbump, bumpvert &bsrc = bumpverts[i], + { + quat q = b.transform(bsrc.tangent); + fixqtangent(q, bsrc.tangent.w); + dst.tangent = q; + }); + } + else + { + IPLOOP(vvertn, , + { + dst.norm = b.transformnormal(src.norm); + }); + } + + #undef IPLOOP + } + + void setshader(Shader *s) + { + skelmeshgroup *g = (skelmeshgroup *)group; + if(glaring) + { + if(!g->skel->usegpuskel) s->setvariant(0, 1); + else s->setvariant(min(maxweights, g->vweights), 1); + } + else if(!g->skel->usegpuskel) s->set(); + else s->setvariant(min(maxweights, g->vweights)-1, 0); + } + + void render(const animstate *as, skin &s, vbocacheentry &vc) + { + if(!Shader::lastshader) return; + glDrawRangeElements_(GL_TRIANGLES, minvert, maxvert, elen, GL_UNSIGNED_SHORT, &((skelmeshgroup *)group)->edata[eoffset]); + glde++; + xtravertsva += numverts; + } + }; + + + struct tag + { + char *name; + int bone; + matrix4x3 matrix; + + tag() : name(NULL) {} + ~tag() { DELETEA(name); } + }; + + struct skelanimspec + { + char *name; + int frame, range; + + skelanimspec() : name(NULL), frame(0), range(0) {} + ~skelanimspec() + { + DELETEA(name); + } + }; + + struct boneinfo + { + const char *name; + int parent, children, next, group, scheduled, interpindex, interpparent, ragdollindex, correctindex; + float pitchscale, pitchoffset, pitchmin, pitchmax; + dualquat base, invbase; + + boneinfo() : name(NULL), parent(-1), children(-1), next(-1), group(INT_MAX), scheduled(-1), interpindex(-1), interpparent(-1), ragdollindex(-1), correctindex(-1), pitchscale(0), pitchoffset(0), pitchmin(0), pitchmax(0) {} + ~boneinfo() + { + DELETEA(name); + } + }; + + struct antipode + { + int parent, child; + + antipode(int parent, int child) : parent(parent), child(child) {} + }; + + struct pitchdep + { + int bone, parent; + dualquat pose; + }; + + struct pitchtarget + { + int bone, frame, corrects, deps; + float pitchmin, pitchmax, deviated; + dualquat pose; + }; + + struct pitchcorrect + { + int bone, target, parent; + float pitchmin, pitchmax, pitchscale, pitchangle, pitchtotal; + + pitchcorrect() : parent(-1), pitchangle(0), pitchtotal(0) {} + }; + + struct skeleton + { + char *name; + int shared; + vector users; + boneinfo *bones; + int numbones, numinterpbones, numgpubones, numframes; + dualquat *framebones; + vector skelanims; + vector tags; + vector antipodes; + ragdollskel *ragdoll; + vector pitchdeps; + vector pitchtargets; + vector pitchcorrects; + + bool usegpuskel; + vector skelcache; + hashtable blendoffsets; + + skeleton() : name(NULL), shared(0), bones(NULL), numbones(0), numinterpbones(0), numgpubones(0), numframes(0), framebones(NULL), ragdoll(NULL), usegpuskel(false), blendoffsets(32) + { + } + + ~skeleton() + { + DELETEA(name); + DELETEA(bones); + DELETEA(framebones); + DELETEP(ragdoll); + loopv(skelcache) + { + DELETEA(skelcache[i].bdata); + } + } + + skelanimspec *findskelanim(const char *name, char sep = '\0') + { + int len = sep ? strlen(name) : 0; + loopv(skelanims) + { + if(skelanims[i].name) + { + if(sep) + { + const char *end = strchr(skelanims[i].name, ':'); + if(end && end - skelanims[i].name == len && !memcmp(name, skelanims[i].name, len)) return &skelanims[i]; + } + if(!strcmp(name, skelanims[i].name)) return &skelanims[i]; + } + } + return NULL; + } + + skelanimspec &addskelanim(const char *name) + { + skelanimspec &sa = skelanims.add(); + sa.name = name ? newstring(name) : NULL; + return sa; + } + + int findbone(const char *name) + { + loopi(numbones) if(bones[i].name && !strcmp(bones[i].name, name)) return i; + return -1; + } + + int findtag(const char *name) + { + loopv(tags) if(!strcmp(tags[i].name, name)) return i; + return -1; + } + + bool addtag(const char *name, int bone, const matrix4x3 &matrix) + { + int idx = findtag(name); + if(idx >= 0) + { + if(!testtags) return false; + tag &t = tags[idx]; + t.bone = bone; + t.matrix = matrix; + } + else + { + tag &t = tags.add(); + t.name = newstring(name); + t.bone = bone; + t.matrix = matrix; + } + return true; + } + + void calcantipodes() + { + antipodes.shrink(0); + vector schedule; + loopi(numbones) + { + if(bones[i].group >= numbones) + { + bones[i].scheduled = schedule.length(); + schedule.add(i); + } + else bones[i].scheduled = -1; + } + loopv(schedule) + { + int bone = schedule[i]; + const boneinfo &info = bones[bone]; + loopj(numbones) if(abs(bones[j].group) == bone && bones[j].scheduled < 0) + { + antipodes.add(antipode(info.interpindex, bones[j].interpindex)); + bones[j].scheduled = schedule.length(); + schedule.add(j); + } + if(i + 1 == schedule.length()) + { + int conflict = INT_MAX; + loopj(numbones) if(bones[j].group < numbones && bones[j].scheduled < 0) conflict = min(conflict, abs(bones[j].group)); + if(conflict < numbones) + { + bones[conflict].scheduled = schedule.length(); + schedule.add(conflict); + } + } + } + } + + void remapbones() + { + loopi(numbones) + { + boneinfo &info = bones[i]; + info.interpindex = -1; + info.ragdollindex = -1; + } + numgpubones = 0; + loopv(users) + { + skelmeshgroup *group = users[i]; + loopvj(group->blendcombos) + { + blendcombo &c = group->blendcombos[j]; + loopk(4) + { + if(!c.weights[k]) { c.interpbones[k] = k > 0 ? c.interpbones[k-1] : 0; continue; } + boneinfo &info = bones[c.bones[k]]; + if(info.interpindex < 0) info.interpindex = numgpubones++; + c.interpbones[k] = info.interpindex; + if(info.group < 0) continue; + loopl(4) + { + if(!c.weights[l]) break; + if(l == k) continue; + int parent = c.bones[l]; + if(info.parent == parent || (info.parent >= 0 && info.parent == bones[parent].parent)) { info.group = -info.parent; break; } + if(info.group <= parent) continue; + int child = c.bones[k]; + while(parent > child) parent = bones[parent].parent; + if(parent != child) info.group = c.bones[l]; + } + } + } + } + numinterpbones = numgpubones; + loopv(tags) + { + boneinfo &info = bones[tags[i].bone]; + if(info.interpindex < 0) info.interpindex = numinterpbones++; + } + if(ragdoll) + { + loopv(ragdoll->joints) + { + boneinfo &info = bones[ragdoll->joints[i].bone]; + if(info.interpindex < 0) info.interpindex = numinterpbones++; + info.ragdollindex = i; + } + } + loopi(numbones) + { + boneinfo &info = bones[i]; + if(info.interpindex < 0) continue; + for(int parent = info.parent; parent >= 0 && bones[parent].interpindex < 0; parent = bones[parent].parent) + bones[parent].interpindex = numinterpbones++; + } + loopi(numbones) + { + boneinfo &info = bones[i]; + if(info.interpindex < 0) continue; + info.interpparent = info.parent >= 0 ? bones[info.parent].interpindex : -1; + } + if(ragdoll) + { + loopi(numbones) + { + boneinfo &info = bones[i]; + if(info.interpindex < 0 || info.ragdollindex >= 0) continue; + for(int parent = info.parent; parent >= 0; parent = bones[parent].parent) + { + if(bones[parent].ragdollindex >= 0) { ragdoll->addreljoint(i, bones[parent].ragdollindex); break; } + } + } + } + calcantipodes(); + } + + + void addpitchdep(int bone, int frame) + { + for(; bone >= 0; bone = bones[bone].parent) + { + int pos = pitchdeps.length(); + loopvj(pitchdeps) if(bone <= pitchdeps[j].bone) + { + if(bone == pitchdeps[j].bone) goto nextbone; + pos = j; + break; + } + { + pitchdep d; + d.bone = bone; + d.parent = -1; + d.pose = framebones[frame*numbones + bone]; + pitchdeps.insert(pos, d); + } + nextbone:; + } + } + + int findpitchdep(int bone) + { + loopv(pitchdeps) if(bone <= pitchdeps[i].bone) return bone == pitchdeps[i].bone ? i : -1; + return -1; + } + + int findpitchcorrect(int bone) + { + loopv(pitchcorrects) if(bone <= pitchcorrects[i].bone) return bone == pitchcorrects[i].bone ? i : -1; + return -1; + } + + void initpitchdeps() + { + pitchdeps.setsize(0); + if(pitchtargets.empty()) return; + loopv(pitchtargets) + { + pitchtarget &t = pitchtargets[i]; + t.deps = -1; + addpitchdep(t.bone, t.frame); + } + loopv(pitchdeps) + { + pitchdep &d = pitchdeps[i]; + int parent = bones[d.bone].parent; + if(parent >= 0) + { + int j = findpitchdep(parent); + if(j >= 0) + { + d.parent = j; + d.pose.mul(pitchdeps[j].pose, dualquat(d.pose)); + } + } + } + loopv(pitchtargets) + { + pitchtarget &t = pitchtargets[i]; + int j = findpitchdep(t.bone); + if(j >= 0) + { + t.deps = j; + t.pose = pitchdeps[j].pose; + } + t.corrects = -1; + for(int parent = t.bone; parent >= 0; parent = bones[parent].parent) + { + t.corrects = findpitchcorrect(parent); + if(t.corrects >= 0) break; + } + } + loopv(pitchcorrects) + { + pitchcorrect &c = pitchcorrects[i]; + bones[c.bone].correctindex = i; + c.parent = -1; + for(int parent = c.bone;;) + { + parent = bones[parent].parent; + if(parent < 0) break; + c.parent = findpitchcorrect(parent); + if(c.parent >= 0) break; + } + } + } + + void optimize() + { + cleanup(); + if(ragdoll) ragdoll->setup(); + remapbones(); + initpitchdeps(); + } + + void expandbonemask(uchar *expansion, int bone, int val) + { + expansion[bone] = val; + bone = bones[bone].children; + while(bone>=0) { expandbonemask(expansion, bone, val); bone = bones[bone].next; } + } + + void applybonemask(ushort *mask, uchar *partmask, int partindex) + { + if(!mask || *mask==BONEMASK_END) return; + uchar *expansion = new uchar[numbones]; + memset(expansion, *mask&BONEMASK_NOT ? 1 : 0, numbones); + while(*mask!=BONEMASK_END) + { + expandbonemask(expansion, *mask&BONEMASK_BONE, *mask&BONEMASK_NOT ? 0 : 1); + mask++; + } + loopi(numbones) if(expansion[i]) partmask[i] = partindex; + delete[] expansion; + } + + void linkchildren() + { + loopi(numbones) + { + boneinfo &b = bones[i]; + b.children = -1; + if(b.parent<0) b.next = -1; + else + { + b.next = bones[b.parent].children; + bones[b.parent].children = i; + } + } + } + + int availgpubones() const { return min(maxvsuniforms - reservevpparams - 10, maxskelanimdata) / 2; } + bool gpuaccelerate() const { return numframes && gpuskel && numgpubones<=availgpubones(); } + + float calcdeviation(const vec &axis, const vec &forward, const dualquat &pose1, const dualquat &pose2) + { + vec forward1 = pose1.transformnormal(forward).project(axis).normalize(), + forward2 = pose2.transformnormal(forward).project(axis).normalize(), + daxis = vec().cross(forward1, forward2); + float dx = clamp(forward1.dot(forward2), -1.0f, 1.0f), dy = clamp(daxis.magnitude(), -1.0f, 1.0f); + if(daxis.dot(axis) < 0) dy = -dy; + return atan2f(dy, dx)/RAD; + } + + void calcpitchcorrects(float pitch, const vec &axis, const vec &forward) + { + loopv(pitchtargets) + { + pitchtarget &t = pitchtargets[i]; + t.deviated = calcdeviation(axis, forward, t.pose, pitchdeps[t.deps].pose); + } + loopv(pitchcorrects) + { + pitchcorrect &c = pitchcorrects[i]; + c.pitchangle = c.pitchtotal = 0; + } + loopvj(pitchtargets) + { + pitchtarget &t = pitchtargets[j]; + float tpitch = pitch - t.deviated; + for(int parent = t.corrects; parent >= 0; parent = pitchcorrects[parent].parent) + tpitch -= pitchcorrects[parent].pitchangle; + if(t.pitchmin || t.pitchmax) tpitch = clamp(tpitch, t.pitchmin, t.pitchmax); + loopv(pitchcorrects) + { + pitchcorrect &c = pitchcorrects[i]; + if(c.target != j) continue; + float total = c.parent >= 0 ? pitchcorrects[c.parent].pitchtotal : 0, + avail = tpitch - total, + used = tpitch*c.pitchscale; + if(c.pitchmin || c.pitchmax) + { + if(used < 0) used = clamp(c.pitchmin, used, 0.0f); + else used = clamp(c.pitchmax, 0.0f, used); + } + if(used < 0) used = clamp(avail, used, 0.0f); + else used = clamp(avail, 0.0f, used); + c.pitchangle = used; + c.pitchtotal = used + total; + } + } + } + + #define INTERPBONE(bone) \ + const animstate &s = as[partmask[bone]]; \ + const framedata &f = partframes[partmask[bone]]; \ + dualquat d; \ + (d = f.fr1[bone]).mul((1-s.cur.t)*s.interp); \ + d.accumulate(f.fr2[bone], s.cur.t*s.interp); \ + if(s.interp<1) \ + { \ + d.accumulate(f.pfr1[bone], (1-s.prev.t)*(1-s.interp)); \ + d.accumulate(f.pfr2[bone], s.prev.t*(1-s.interp)); \ + } + + void interpbones(const animstate *as, float pitch, const vec &axis, const vec &forward, int numanimparts, const uchar *partmask, skelcacheentry &sc) + { + if(!sc.bdata) sc.bdata = new dualquat[numinterpbones]; + sc.nextversion(); + struct framedata + { + const dualquat *fr1, *fr2, *pfr1, *pfr2; + } partframes[MAXANIMPARTS]; + loopi(numanimparts) + { + partframes[i].fr1 = &framebones[as[i].cur.fr1*numbones]; + partframes[i].fr2 = &framebones[as[i].cur.fr2*numbones]; + if(as[i].interp<1) + { + partframes[i].pfr1 = &framebones[as[i].prev.fr1*numbones]; + partframes[i].pfr2 = &framebones[as[i].prev.fr2*numbones]; + } + } + loopv(pitchdeps) + { + pitchdep &p = pitchdeps[i]; + INTERPBONE(p.bone); + d.normalize(); + if(p.parent >= 0) p.pose.mul(pitchdeps[p.parent].pose, d); + else p.pose = d; + } + calcpitchcorrects(pitch, axis, forward); + loopi(numbones) if(bones[i].interpindex>=0) + { + INTERPBONE(i); + const boneinfo &b = bones[i]; + d.normalize(); + if(b.interpparent<0) sc.bdata[b.interpindex] = d; + else sc.bdata[b.interpindex].mul(sc.bdata[b.interpparent], d); + float angle; + if(b.pitchscale) { angle = b.pitchscale*pitch + b.pitchoffset; if(b.pitchmin || b.pitchmax) angle = clamp(angle, b.pitchmin, b.pitchmax); } + else if(b.correctindex >= 0) angle = pitchcorrects[b.correctindex].pitchangle; + else continue; + if(as->cur.anim&ANIM_NOPITCH || (as->interp < 1 && as->prev.anim&ANIM_NOPITCH)) + angle *= (as->cur.anim&ANIM_NOPITCH ? 0 : as->interp) + (as->interp < 1 && as->prev.anim&ANIM_NOPITCH ? 0 : 1-as->interp); + sc.bdata[b.interpindex].mulorient(quat(axis, angle*RAD), b.base); + } + loopv(antipodes) sc.bdata[antipodes[i].child].fixantipodal(sc.bdata[antipodes[i].parent]); + } + + void initragdoll(ragdolldata &d, skelcacheentry &sc, part *p) + { + const dualquat *bdata = sc.bdata; + loopv(ragdoll->joints) + { + const ragdollskel::joint &j = ragdoll->joints[i]; + const boneinfo &b = bones[j.bone]; + const dualquat &q = bdata[b.interpindex]; + loopk(3) if(j.vert[k] >= 0) + { + ragdollskel::vert &v = ragdoll->verts[j.vert[k]]; + ragdolldata::vert &dv = d.verts[j.vert[k]]; + dv.pos.add(q.transform(v.pos).mul(v.weight)); + } + } + if(ragdoll->animjoints) loopv(ragdoll->joints) + { + const ragdollskel::joint &j = ragdoll->joints[i]; + const boneinfo &b = bones[j.bone]; + const dualquat &q = bdata[b.interpindex]; + d.calcanimjoint(i, matrix4x3(q)); + } + loopv(ragdoll->verts) + { + ragdolldata::vert &dv = d.verts[i]; + matrixstack[matrixpos].transform(vec(dv.pos).add(p->translate).mul(p->model->scale), dv.pos); + } + loopv(ragdoll->reljoints) + { + const ragdollskel::reljoint &r = ragdoll->reljoints[i]; + const ragdollskel::joint &j = ragdoll->joints[r.parent]; + const boneinfo &br = bones[r.bone], &bj = bones[j.bone]; + d.reljoints[i].mul(dualquat(bdata[bj.interpindex]).invert(), bdata[br.interpindex]); + } + } + + void genragdollbones(ragdolldata &d, skelcacheentry &sc, part *p) + { + if(!sc.bdata) sc.bdata = new dualquat[numinterpbones]; + sc.nextversion(); + loopv(ragdoll->joints) + { + const ragdollskel::joint &j = ragdoll->joints[i]; + const boneinfo &b = bones[j.bone]; + vec pos(0, 0, 0); + loopk(3) if(j.vert[k]>=0) pos.add(d.verts[j.vert[k]].pos); + pos.mul(j.weight/p->model->scale).sub(p->translate); + matrix4x3 m; + m.mul(d.tris[j.tri], pos, d.animjoints ? d.animjoints[i] : j.orient); + sc.bdata[b.interpindex] = dualquat(m); + } + loopv(ragdoll->reljoints) + { + const ragdollskel::reljoint &r = ragdoll->reljoints[i]; + const ragdollskel::joint &j = ragdoll->joints[r.parent]; + const boneinfo &br = bones[r.bone], &bj = bones[j.bone]; + sc.bdata[br.interpindex].mul(sc.bdata[bj.interpindex], d.reljoints[i]); + } + loopv(antipodes) sc.bdata[antipodes[i].child].fixantipodal(sc.bdata[antipodes[i].parent]); + } + + void concattagtransform(part *p, int i, const matrix4x3 &m, matrix4x3 &n) + { + matrix4x3 t; + t.mul(bones[tags[i].bone].base, tags[i].matrix); + t.posttranslate(p->translate, p->model->scale); + n.mul(m, t); + } + + void calctags(part *p, skelcacheentry *sc = NULL) + { + loopv(p->links) + { + linkedpart &l = p->links[i]; + tag &t = tags[l.tag]; + dualquat q; + if(sc) q.mul(sc->bdata[bones[t.bone].interpindex], bones[t.bone].base); + else q = bones[t.bone].base; + matrix4x3 m; + m.mul(q, t.matrix); + m.d.add(p->translate).mul(p->model->scale); + l.matrix = m; + } + } + + void cleanup(bool full = true) + { + loopv(skelcache) + { + skelcacheentry &sc = skelcache[i]; + loopj(MAXANIMPARTS) sc.as[j].cur.fr1 = -1; + DELETEA(sc.bdata); + } + skelcache.setsize(0); + blendoffsets.clear(); + if(full) loopv(users) users[i]->cleanup(); + } + + bool canpreload() { return !numframes || gpuaccelerate(); } + + void preload() + { + if(!numframes) return; + if(skelcache.empty()) + { + usegpuskel = gpuaccelerate(); + } + } + + skelcacheentry &checkskelcache(part *p, const animstate *as, float pitch, const vec &axis, const vec &forward, ragdolldata *rdata) + { + if(skelcache.empty()) + { + usegpuskel = gpuaccelerate(); + } + + int numanimparts = ((skelpart *)as->owner)->numanimparts; + uchar *partmask = ((skelpart *)as->owner)->partmask; + skelcacheentry *sc = NULL; + bool match = false; + loopv(skelcache) + { + skelcacheentry &c = skelcache[i]; + loopj(numanimparts) if(c.as[j]!=as[j]) goto mismatch; + if(c.pitch != pitch || c.partmask != partmask || c.ragdoll != rdata || (rdata && c.millis < rdata->lastmove)) goto mismatch; + match = true; + sc = &c; + break; + mismatch: + if(c.millis < lastmillis) { sc = &c; break; } + } + if(!sc) sc = &skelcache.add(); + if(!match) + { + loopi(numanimparts) sc->as[i] = as[i]; + sc->pitch = pitch; + sc->partmask = partmask; + sc->ragdoll = rdata; + if(rdata) genragdollbones(*rdata, *sc, p); + else interpbones(as, pitch, axis, forward, numanimparts, partmask, *sc); + } + sc->millis = lastmillis; + return *sc; + } + + int getblendoffset(UniformLoc &u) + { + int &offset = blendoffsets.access(Shader::lastshader->program, -1); + if(offset < 0) + { + defformatstring(offsetname, "%s[%d]", u.name, 2*numgpubones); + offset = glGetUniformLocation_(Shader::lastshader->program, offsetname); + } + return offset; + } + + void setglslbones(UniformLoc &u, skelcacheentry &sc, skelcacheentry &bc, int count) + { + if(u.version == bc.version && u.data == bc.bdata) return; + glUniform4fv_(u.loc, 2*numgpubones, sc.bdata[0].real.v); + if(count > 0) + { + int offset = getblendoffset(u); + if(offset >= 0) glUniform4fv_(offset, 2*count, bc.bdata[0].real.v); + } + u.version = bc.version; + u.data = bc.bdata; + } + + void setgpubones(skelcacheentry &sc, blendcacheentry *bc, int count) + { + if(!Shader::lastshader) return; + if(Shader::lastshader->uniformlocs.length() < 1) return; + UniformLoc &u = Shader::lastshader->uniformlocs[0]; + setglslbones(u, sc, bc ? *bc : sc, count); + } + + bool shouldcleanup() const + { + return numframes && (skelcache.empty() || gpuaccelerate()!=usegpuskel); + } + }; + + struct skelmeshgroup : meshgroup + { + skeleton *skel; + + vector blendcombos; + int numblends[4]; + + static const int MAXBLENDCACHE = 16; + blendcacheentry blendcache[MAXBLENDCACHE]; + + static const int MAXVBOCACHE = 16; + vbocacheentry vbocache[MAXVBOCACHE]; + + ushort *edata; + GLuint ebuf; + bool vtangents; + int vlen, vertsize, vblends, vweights; + uchar *vdata; + + skelmeshgroup() : skel(NULL), edata(NULL), ebuf(0), vtangents(false), vlen(0), vertsize(0), vblends(0), vweights(0), vdata(NULL) + { + memset(numblends, 0, sizeof(numblends)); + } + + virtual ~skelmeshgroup() + { + if(skel) + { + if(skel->shared) skel->users.removeobj(this); + else DELETEP(skel); + } + if(ebuf) glDeleteBuffers_(1, &ebuf); + loopi(MAXBLENDCACHE) + { + DELETEA(blendcache[i].bdata); + } + loopi(MAXVBOCACHE) + { + if(vbocache[i].vbuf) glDeleteBuffers_(1, &vbocache[i].vbuf); + } + DELETEA(vdata); + } + + void shareskeleton(char *name) + { + if(!name) + { + skel = new skeleton; + skel->users.add(this); + return; + } + + static hashnameset skeletons; + if(skeletons.access(name)) skel = skeletons[name]; + else + { + skel = new skeleton; + skel->name = newstring(name); + skeletons.add(skel); + } + skel->users.add(this); + skel->shared++; + } + + int findtag(const char *name) + { + return skel->findtag(name); + } + + void *animkey() { return skel; } + int totalframes() const { return max(skel->numframes, 1); } + + virtual skelanimspec *loadanim(const char *filename) { return NULL; } + + void genvbo(bool tangents, vbocacheentry &vc) + { + if(!vc.vbuf) glGenBuffers_(1, &vc.vbuf); + if(ebuf) return; + + vector idxs; + + if(tangents) loopv(meshes) ((skelmesh *)meshes[i])->calctangents(); + + vtangents = tangents; + vlen = 0; + vblends = 0; + if(skel->numframes && !skel->usegpuskel) + { + vweights = 1; + loopv(blendcombos) + { + blendcombo &c = blendcombos[i]; + c.interpindex = c.weights[1] ? skel->numgpubones + vblends++ : -1; + } + + vertsize = tangents ? sizeof(vvertbump) : sizeof(vvertn); + loopv(meshes) vlen += ((skelmesh *)meshes[i])->genvbo(idxs, vlen); + DELETEA(vdata); + vdata = new uchar[vlen*vertsize]; + #define FILLVDATA(type) do { \ + loopv(meshes) ((skelmesh *)meshes[i])->fillverts((type *)vdata); \ + } while(0) + if(tangents) FILLVDATA(vvertbump); + else FILLVDATA(vvertn); + #undef FILLVDATA + } + else + { + if(skel->numframes) + { + vweights = 4; + int availbones = skel->availgpubones() - skel->numgpubones; + while(vweights > 1 && availbones >= numblends[vweights-1]) availbones -= numblends[--vweights]; + loopv(blendcombos) + { + blendcombo &c = blendcombos[i]; + c.interpindex = c.size() > vweights ? skel->numgpubones + vblends++ : -1; + } + } + else + { + vweights = 0; + loopv(blendcombos) blendcombos[i].interpindex = -1; + } + + gle::bindvbo(vc.vbuf); + #define GENVBO(type, args) do { \ + vertsize = sizeof(type); \ + vector vverts; \ + loopv(meshes) vlen += ((skelmesh *)meshes[i])->genvbo args; \ + glBufferData_(GL_ARRAY_BUFFER, vverts.length()*sizeof(type), vverts.getbuf(), GL_STATIC_DRAW); \ + } while(0) + #define GENVBOANIM(type) GENVBO(type, (idxs, vlen, vverts)) + #define GENVBOSTAT(type) GENVBO(type, (idxs, vlen, vverts, htdata, htlen)) + if(skel->numframes) + { + if(tangents) GENVBOANIM(vvertbumpw); + else GENVBOANIM(vvertnw); + } + else + { + int numverts = 0, htlen = 128; + loopv(meshes) numverts += ((skelmesh *)meshes[i])->numverts; + while(htlen < numverts) htlen *= 2; + if(numverts*4 > htlen*3) htlen *= 2; + int *htdata = new int[htlen]; + memset(htdata, -1, htlen*sizeof(int)); + if(tangents) GENVBOSTAT(vvertbump); + else GENVBOSTAT(vvertn); + delete[] htdata; + } + #undef GENVBO + #undef GENVBOANIM + #undef GENVBOSTAT + gle::clearvbo(); + } + + glGenBuffers_(1, &ebuf); + gle::bindebo(ebuf); + glBufferData_(GL_ELEMENT_ARRAY_BUFFER, idxs.length()*sizeof(ushort), idxs.getbuf(), GL_STATIC_DRAW); + gle::clearebo(); + } + + void bindvbo(const animstate *as, vbocacheentry &vc, skelcacheentry *sc = NULL, blendcacheentry *bc = NULL) + { + vvert *vverts = 0; + bindpos(ebuf, vc.vbuf, &vverts->pos, vertsize); + if(as->cur.anim&ANIM_NOSKIN) + { + if(enabletc) disabletc(); + if(enablenormals) disablenormals(); + if(enabletangents) disabletangents(); + } + else + { + if(vtangents) + { + if(enablenormals) disablenormals(); + vvertbump *vvertbumps = 0; + bindtangents(&vvertbumps->tangent, vertsize); + } + else + { + if(enabletangents) disabletangents(); + vvertn *vvertns = 0; + bindnormals(&vvertns->norm, vertsize); + } + + bindtc(&vverts->tc, vertsize); + } + if(!sc || !skel->usegpuskel) + { + if(enablebones) disablebones(); + } + else + { + if(vtangents) + { + vvertbumpw *vvertbumpws = 0; + bindbones(vvertbumpws->weights, vvertbumpws->bones, vertsize); + } + else + { + vvertnw *vvertnws = 0; + bindbones(vvertnws->weights, vvertnws->bones, vertsize); + } + } + } + + void concattagtransform(part *p, int i, const matrix4x3 &m, matrix4x3 &n) + { + skel->concattagtransform(p, i, m, n); + } + + int addblendcombo(const blendcombo &c) + { + loopv(blendcombos) if(blendcombos[i]==c) + { + blendcombos[i].uses += c.uses; + return i; + } + numblends[c.size()-1]++; + blendcombo &a = blendcombos.add(c); + return a.interpindex = blendcombos.length()-1; + } + + void sortblendcombos() + { + blendcombos.sort(blendcombo::sortcmp); + int *remap = new int[blendcombos.length()]; + loopv(blendcombos) remap[blendcombos[i].interpindex] = i; + loopv(meshes) + { + skelmesh *m = (skelmesh *)meshes[i]; + loopj(m->numverts) + { + vert &v = m->verts[j]; + v.blend = remap[v.blend]; + } + } + delete[] remap; + } + + int remapblend(int blend) + { + const blendcombo &c = blendcombos[blend]; + return c.weights[1] ? c.interpindex : c.interpbones[0]; + } + + static inline void blendbones(dualquat &d, const dualquat *bdata, const blendcombo &c) + { + d = bdata[c.interpbones[0]]; + d.mul(c.weights[0]); + d.accumulate(bdata[c.interpbones[1]], c.weights[1]); + if(c.weights[2]) + { + d.accumulate(bdata[c.interpbones[2]], c.weights[2]); + if(c.weights[3]) d.accumulate(bdata[c.interpbones[3]], c.weights[3]); + } + } + + void blendbones(const skelcacheentry &sc, blendcacheentry &bc) + { + bc.nextversion(); + if(!bc.bdata) bc.bdata = new dualquat[vblends]; + dualquat *dst = bc.bdata - skel->numgpubones; + bool normalize = !skel->usegpuskel || vweights<=1; + loopv(blendcombos) + { + const blendcombo &c = blendcombos[i]; + if(c.interpindex<0) break; + dualquat &d = dst[c.interpindex]; + blendbones(d, sc.bdata, c); + if(normalize) d.normalize(); + } + } + + void cleanup() + { + loopi(MAXBLENDCACHE) + { + blendcacheentry &c = blendcache[i]; + DELETEA(c.bdata); + c.owner = -1; + } + loopi(MAXVBOCACHE) + { + vbocacheentry &c = vbocache[i]; + if(c.vbuf) { glDeleteBuffers_(1, &c.vbuf); c.vbuf = 0; } + c.owner = -1; + } + if(ebuf) { glDeleteBuffers_(1, &ebuf); ebuf = 0; } + if(skel) skel->cleanup(false); + } + + #define SEARCHCACHE(cachesize, cacheentry, cache, reusecheck) \ + loopi(cachesize) \ + { \ + cacheentry &c = cache[i]; \ + if(c.owner==owner) \ + { \ + if(c==sc) return c; \ + else c.owner = -1; \ + break; \ + } \ + } \ + loopi(cachesize-1) \ + { \ + cacheentry &c = cache[i]; \ + if(reusecheck c.owner < 0 || c.millis < lastmillis) \ + return c; \ + } \ + return cache[cachesize-1]; + + vbocacheentry &checkvbocache(skelcacheentry &sc, int owner) + { + SEARCHCACHE(MAXVBOCACHE, vbocacheentry, vbocache, !c.vbuf || ); + } + + blendcacheentry &checkblendcache(skelcacheentry &sc, int owner) + { + SEARCHCACHE(MAXBLENDCACHE, blendcacheentry, blendcache, ) + } + + void preload(part *p) + { + if(!skel->canpreload()) return; + bool tangents = false; + loopv(p->skins) if(p->skins[i].tangents()) tangents = true; + if(skel->shouldcleanup()) skel->cleanup(); + else if(tangents!=vtangents) cleanup(); + skel->preload(); + if(!vbocache->vbuf) genvbo(tangents, *vbocache); + } + + void render(const animstate *as, float pitch, const vec &axis, const vec &forward, dynent *d, part *p) + { + bool tangents = false; + loopv(p->skins) if(p->skins[i].tangents()) tangents = true; + if(skel->shouldcleanup()) { skel->cleanup(); disablevbo(); } + else if(tangents!=vtangents) { cleanup(); disablevbo(); } + + if(!skel->numframes) + { + if(!(as->cur.anim&ANIM_NORENDER)) + { + if(!vbocache->vbuf) genvbo(tangents, *vbocache); + bindvbo(as, *vbocache); + loopv(meshes) + { + skelmesh *m = (skelmesh *)meshes[i]; + p->skins[i].bind(m, as); + m->render(as, p->skins[i], *vbocache); + } + } + skel->calctags(p); + return; + } + + skelcacheentry &sc = skel->checkskelcache(p, as, pitch, axis, forward, as->cur.anim&ANIM_RAGDOLL || !d || !d->ragdoll || d->ragdoll->skel != skel->ragdoll ? NULL : d->ragdoll); + if(!(as->cur.anim&ANIM_NORENDER)) + { + int owner = &sc-&skel->skelcache[0]; + vbocacheentry &vc = skel->usegpuskel ? *vbocache : checkvbocache(sc, owner); + vc.millis = lastmillis; + if(!vc.vbuf) genvbo(tangents, vc); + blendcacheentry *bc = NULL; + if(vblends) + { + bc = &checkblendcache(sc, owner); + bc->millis = lastmillis; + if(bc->owner!=owner) + { + bc->owner = owner; + *(animcacheentry *)bc = sc; + blendbones(sc, *bc); + } + } + if(!skel->usegpuskel && vc.owner!=owner) + { + vc.owner = owner; + (animcacheentry &)vc = sc; + loopv(meshes) + { + skelmesh &m = *(skelmesh *)meshes[i]; + m.interpverts(sc.bdata, bc ? bc->bdata : NULL, tangents, vdata + m.voffset*vertsize, p->skins[i]); + } + gle::bindvbo(vc.vbuf); + glBufferData_(GL_ARRAY_BUFFER, vlen*vertsize, vdata, GL_STREAM_DRAW); + } + + bindvbo(as, vc, &sc, bc); + loopv(meshes) + { + skelmesh *m = (skelmesh *)meshes[i]; + p->skins[i].bind(m, as); + if(skel->usegpuskel) skel->setgpubones(sc, bc, vblends); + m->render(as, p->skins[i], vc); + } + } + + skel->calctags(p, &sc); + + if(as->cur.anim&ANIM_RAGDOLL && skel->ragdoll && !d->ragdoll) + { + d->ragdoll = new ragdolldata(skel->ragdoll, p->model->scale); + skel->initragdoll(*d->ragdoll, sc, p); + d->ragdoll->init(d); + } + } + }; + + struct animpartmask + { + animpartmask *next; + int numbones; + uchar bones[1]; + }; + + struct skelpart : part + { + animpartmask *buildingpartmask; + + uchar *partmask; + + skelpart(animmodel *model, int index = 0) : part(model, index), buildingpartmask(NULL), partmask(NULL) + { + } + + virtual ~skelpart() + { + DELETEA(buildingpartmask); + } + + uchar *sharepartmask(animpartmask *o) + { + static animpartmask *partmasks = NULL; + animpartmask *p = partmasks; + for(; p; p = p->next) if(p->numbones==o->numbones && !memcmp(p->bones, o->bones, p->numbones)) + { + delete[] (uchar *)o; + return p->bones; + } + + o->next = p; + partmasks = o; + return o->bones; + } + + animpartmask *newpartmask() + { + animpartmask *p = (animpartmask *)new uchar[sizeof(animpartmask) + ((skelmeshgroup *)meshes)->skel->numbones-1]; + p->numbones = ((skelmeshgroup *)meshes)->skel->numbones; + memset(p->bones, 0, p->numbones); + return p; + } + + void initanimparts() + { + DELETEA(buildingpartmask); + buildingpartmask = newpartmask(); + } + + bool addanimpart(ushort *bonemask) + { + if(!buildingpartmask || numanimparts>=MAXANIMPARTS) return false; + ((skelmeshgroup *)meshes)->skel->applybonemask(bonemask, buildingpartmask->bones, numanimparts); + numanimparts++; + return true; + } + + void endanimparts() + { + if(buildingpartmask) + { + partmask = sharepartmask(buildingpartmask); + buildingpartmask = NULL; + } + + ((skelmeshgroup *)meshes)->skel->optimize(); + } + + void loaded() + { + endanimparts(); + part::loaded(); + } + }; + + skelmodel(const char *name) : animmodel(name) + { + } + + int linktype(animmodel *m) const + { + return type()==m->type() && + ((skelmeshgroup *)parts[0]->meshes)->skel == ((skelmeshgroup *)m->parts[0]->meshes)->skel ? + LINK_REUSE : + LINK_TAG; + } + + bool skeletal() const { return true; } + + skelpart &addpart() + { + flushpart(); + skelpart *p = new skelpart(this, parts.length()); + parts.add(p); + return *p; + } +}; + +struct skeladjustment +{ + float yaw, pitch, roll; + vec translate; + + skeladjustment(float yaw, float pitch, float roll, const vec &translate) : yaw(yaw), pitch(pitch), roll(roll), translate(translate) {} + + void adjust(dualquat &dq) + { + if(yaw) dq.mulorient(quat(vec(0, 0, 1), yaw*RAD)); + if(pitch) dq.mulorient(quat(vec(0, -1, 0), pitch*RAD)); + if(roll) dq.mulorient(quat(vec(-1, 0, 0), roll*RAD)); + if(!translate.iszero()) dq.translate(translate); + } +}; + +template struct skelloader : modelloader +{ + static vector adjustments; + + skelloader(const char *name) : modelloader(name) {} + + void flushpart() + { + adjustments.setsize(0); + } +}; + +template vector skelloader::adjustments; + +template struct skelcommands : modelcommands +{ + typedef modelcommands commands; + typedef struct MDL::skeleton skeleton; + typedef struct MDL::skelmeshgroup meshgroup; + typedef struct MDL::skelpart part; + typedef struct MDL::skin skin; + typedef struct MDL::boneinfo boneinfo; + typedef struct MDL::skelanimspec animspec; + typedef struct MDL::pitchdep pitchdep; + typedef struct MDL::pitchtarget pitchtarget; + typedef struct MDL::pitchcorrect pitchcorrect; + + static void loadpart(char *meshfile, char *skelname, float *smooth) + { + if(!MDL::loading) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; } + defformatstring(filename, "%s/%s", MDL::dir, meshfile); + part &mdl = MDL::loading->addpart(); + mdl.pitchscale = mdl.pitchoffset = mdl.pitchmin = mdl.pitchmax = 0; + mdl.meshes = MDL::loading->sharemeshes(path(filename), skelname[0] ? skelname : NULL, double(*smooth > 0 ? cos(clamp(*smooth, 0.0f, 180.0f)*RAD) : 2)); + if(!mdl.meshes) conoutf(CON_ERROR, "could not load %s", filename); + else + { + mdl.initanimparts(); + mdl.initskins(); + } + } + + static void settag(char *name, char *tagname, float *tx, float *ty, float *tz, float *rx, float *ry, float *rz) + { + if(!MDL::loading || MDL::loading->parts.empty()) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; } + part &mdl = *(part *)MDL::loading->parts.last(); + int i = mdl.meshes ? ((meshgroup *)mdl.meshes)->skel->findbone(name) : -1; + if(i >= 0) + { + float cx = *rx ? cosf(*rx/2*RAD) : 1, sx = *rx ? sinf(*rx/2*RAD) : 0, + cy = *ry ? cosf(*ry/2*RAD) : 1, sy = *ry ? sinf(*ry/2*RAD) : 0, + cz = *rz ? cosf(*rz/2*RAD) : 1, sz = *rz ? sinf(*rz/2*RAD) : 0; + matrix4x3 m(matrix3(quat(sx*cy*cz - cx*sy*sz, cx*sy*cz + sx*cy*sz, cx*cy*sz - sx*sy*cz, cx*cy*cz + sx*sy*sz)), + vec(*tx, *ty, *tz)); + ((meshgroup *)mdl.meshes)->skel->addtag(tagname, i, m); + return; + } + conoutf(CON_ERROR, "could not find bone %s for tag %s", name, tagname); + } + + static void setpitch(char *name, float *pitchscale, float *pitchoffset, float *pitchmin, float *pitchmax) + { + if(!MDL::loading || MDL::loading->parts.empty()) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; } + part &mdl = *(part *)MDL::loading->parts.last(); + + if(name[0]) + { + int i = mdl.meshes ? ((meshgroup *)mdl.meshes)->skel->findbone(name) : -1; + if(i>=0) + { + boneinfo &b = ((meshgroup *)mdl.meshes)->skel->bones[i]; + b.pitchscale = *pitchscale; + b.pitchoffset = *pitchoffset; + if(*pitchmin || *pitchmax) + { + b.pitchmin = *pitchmin; + b.pitchmax = *pitchmax; + } + else + { + b.pitchmin = -360*fabs(b.pitchscale) + b.pitchoffset; + b.pitchmax = 360*fabs(b.pitchscale) + b.pitchoffset; + } + return; + } + conoutf(CON_ERROR, "could not find bone %s to pitch", name); + return; + } + + mdl.pitchscale = *pitchscale; + mdl.pitchoffset = *pitchoffset; + if(*pitchmin || *pitchmax) + { + mdl.pitchmin = *pitchmin; + mdl.pitchmax = *pitchmax; + } + else + { + mdl.pitchmin = -360*fabs(mdl.pitchscale) + mdl.pitchoffset; + mdl.pitchmax = 360*fabs(mdl.pitchscale) + mdl.pitchoffset; + } + } + + static void setpitchtarget(char *name, char *animfile, int *frameoffset, float *pitchmin, float *pitchmax) + { + if(!MDL::loading || MDL::loading->parts.empty()) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; } + part &mdl = *(part *)MDL::loading->parts.last(); + if(!mdl.meshes) return; + defformatstring(filename, "%s/%s", MDL::dir, animfile); + animspec *sa = ((meshgroup *)mdl.meshes)->loadanim(path(filename)); + if(!sa) { conoutf(CON_ERROR, "could not load %s anim file %s", MDL::formatname(), filename); return; } + skeleton *skel = ((meshgroup *)mdl.meshes)->skel; + int bone = skel ? skel->findbone(name) : -1; + if(bone < 0) + { + conoutf(CON_ERROR, "could not find bone %s to pitch target", name); + return; + } + loopv(skel->pitchtargets) if(skel->pitchtargets[i].bone == bone) return; + pitchtarget &t = skel->pitchtargets.add(); + t.bone = bone; + t.frame = sa->frame + clamp(*frameoffset, 0, sa->range-1); + t.pitchmin = *pitchmin; + t.pitchmax = *pitchmax; + } + + static void setpitchcorrect(char *name, char *targetname, float *scale, float *pitchmin, float *pitchmax) + { + if(!MDL::loading || MDL::loading->parts.empty()) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; } + part &mdl = *(part *)MDL::loading->parts.last(); + if(!mdl.meshes) return; + skeleton *skel = ((meshgroup *)mdl.meshes)->skel; + int bone = skel ? skel->findbone(name) : -1; + if(bone < 0) + { + conoutf(CON_ERROR, "could not find bone %s to pitch correct", name); + return; + } + if(skel->findpitchcorrect(bone) >= 0) return; + int targetbone = skel->findbone(targetname), target = -1; + if(targetbone >= 0) loopv(skel->pitchtargets) if(skel->pitchtargets[i].bone == targetbone) { target = i; break; } + if(target < 0) + { + conoutf(CON_ERROR, "could not find pitch target %s to pitch correct %s", targetname, name); + return; + } + pitchcorrect c; + c.bone = bone; + c.target = target; + c.pitchmin = *pitchmin; + c.pitchmax = *pitchmax; + c.pitchscale = *scale; + int pos = skel->pitchcorrects.length(); + loopv(skel->pitchcorrects) if(bone <= skel->pitchcorrects[i].bone) { pos = i; break; } + skel->pitchcorrects.insert(pos, c); + } + + static void setanim(char *anim, char *animfile, float *speed, int *priority, int *startoffset, int *endoffset) + { + if(!MDL::loading || MDL::loading->parts.empty()) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; } + + vector anims; + findanims(anim, anims); + if(anims.empty()) conoutf(CON_ERROR, "could not find animation %s", anim); + else + { + part *p = (part *)MDL::loading->parts.last(); + if(!p->meshes) return; + defformatstring(filename, "%s/%s", MDL::dir, animfile); + animspec *sa = ((meshgroup *)p->meshes)->loadanim(path(filename)); + if(!sa) conoutf(CON_ERROR, "could not load %s anim file %s", MDL::formatname(), filename); + else loopv(anims) + { + int start = sa->frame, end = sa->range; + if(*startoffset > 0) start += min(*startoffset, end-1); + else if(*startoffset < 0) start += max(end + *startoffset, 0); + end -= start - sa->frame; + if(*endoffset > 0) end = min(end, *endoffset); + else if(*endoffset < 0) end = max(end + *endoffset, 1); + MDL::loading->parts.last()->setanim(p->numanimparts-1, anims[i], start, end, *speed, *priority); + } + } + } + + static void setanimpart(char *maskstr) + { + if(!MDL::loading || MDL::loading->parts.empty()) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; } + + part *p = (part *)MDL::loading->parts.last(); + + vector bonestrs; + explodelist(maskstr, bonestrs); + vector bonemask; + loopv(bonestrs) + { + char *bonestr = bonestrs[i]; + int bone = p->meshes ? ((meshgroup *)p->meshes)->skel->findbone(bonestr[0]=='!' ? bonestr+1 : bonestr) : -1; + if(bone<0) { conoutf(CON_ERROR, "could not find bone %s for anim part mask [%s]", bonestr, maskstr); bonestrs.deletearrays(); return; } + bonemask.add(bone | (bonestr[0]=='!' ? BONEMASK_NOT : 0)); + } + bonestrs.deletearrays(); + bonemask.sort(); + if(bonemask.length()) bonemask.add(BONEMASK_END); + + if(!p->addanimpart(bonemask.getbuf())) conoutf(CON_ERROR, "too many animation parts"); + } + + static void setadjust(char *name, float *yaw, float *pitch, float *roll, float *tx, float *ty, float *tz) + { + if(!MDL::loading || MDL::loading->parts.empty()) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; } + part &mdl = *(part *)MDL::loading->parts.last(); + + if(!name[0]) return; + int i = mdl.meshes ? ((meshgroup *)mdl.meshes)->skel->findbone(name) : -1; + if(i < 0) { conoutf(CON_ERROR, "could not find bone %s to adjust", name); return; } + while(!MDL::adjustments.inrange(i)) MDL::adjustments.add(skeladjustment(0, 0, 0, vec(0, 0, 0))); + MDL::adjustments[i] = skeladjustment(*yaw, *pitch, *roll, vec(*tx/4, *ty/4, *tz/4)); + } + + skelcommands() + { + if(MDL::multiparted()) this->modelcommand(loadpart, "load", "ssf"); + this->modelcommand(settag, "tag", "ssffffff"); + this->modelcommand(setpitch, "pitch", "sffff"); + this->modelcommand(setpitchtarget, "pitchtarget", "ssiff"); + this->modelcommand(setpitchcorrect, "pitchcorrect", "ssfff"); + if(MDL::animated()) + { + this->modelcommand(setanim, "anim", "ssfiii"); + this->modelcommand(setanimpart, "animpart", "s"); + this->modelcommand(setadjust, "adjust", "sffffff"); + } + } +}; + diff --git a/src/engine/smd.h b/src/engine/smd.h new file mode 100644 index 0000000..771ec9a --- /dev/null +++ b/src/engine/smd.h @@ -0,0 +1,447 @@ +struct smd; + +struct smdbone +{ + string name; + int parent; + smdbone() : parent(-1) { name[0] = '\0'; } +}; + +struct smd : skelloader +{ + smd(const char *name) : skelloader(name) {} + + static const char *formatname() { return "smd"; } + int type() const { return MDL_SMD; } + + struct smdmesh : skelmesh + { + }; + + struct smdmeshgroup : skelmeshgroup + { + smdmeshgroup() + { + } + + bool skipcomment(char *&curbuf) + { + while(*curbuf && isspace(*curbuf)) curbuf++; + switch(*curbuf) + { + case '#': + case ';': + case '\r': + case '\n': + case '\0': + return true; + case '/': + if(curbuf[1] == '/') return true; + break; + } + return false; + } + + void skipsection(stream *f, char *buf, size_t bufsize) + { + while(f->getline(buf, bufsize)) + { + char *curbuf = buf; + if(skipcomment(curbuf)) continue; + if(!strncmp(curbuf, "end", 3)) break; + } + } + + void readname(char *&curbuf, char *name, size_t namesize) + { + char *curname = name; + while(*curbuf && isspace(*curbuf)) curbuf++; + bool allowspace = false; + if(*curbuf == '"') { curbuf++; allowspace = true; } + while(*curbuf) + { + char c = *curbuf++; + if(c == '"') break; + if(isspace(c) && !allowspace) break; + if(curname < &name[namesize-1]) *curname++ = c; + } + *curname = '\0'; + } + + void readnodes(stream *f, char *buf, size_t bufsize, vector &bones) + { + while(f->getline(buf, bufsize)) + { + char *curbuf = buf; + if(skipcomment(curbuf)) continue; + if(!strncmp(curbuf, "end", 3)) break; + int id = strtol(curbuf, &curbuf, 10); + string name; + readname(curbuf, name, sizeof(name)); + int parent = strtol(curbuf, &curbuf, 10); + if(id < 0 || id > 255 || parent > 255 || !name[0]) continue; + while(!bones.inrange(id)) bones.add(); + smdbone &bone = bones[id]; + copystring(bone.name, name); + bone.parent = parent; + } + } + + void readmaterial(char *&curbuf, char *name, size_t namesize) + { + char *curname = name; + while(*curbuf && isspace(*curbuf)) curbuf++; + while(*curbuf) + { + char c = *curbuf++; + if(isspace(c)) break; + if(c == '.') + { + while(*curbuf && !isspace(*curbuf)) curbuf++; + break; + } + if(curname < &name[namesize-1]) *curname++ = c; + } + *curname = '\0'; + } + + struct smdmeshdata + { + smdmesh *mesh; + vector verts; + vector tris; + + void finalize() + { + if(verts.empty() || tris.empty()) return; + vert *mverts = new vert[mesh->numverts + verts.length()]; + if(mesh->numverts) + { + memcpy(mverts, mesh->verts, mesh->numverts*sizeof(vert)); + delete[] mesh->verts; + } + memcpy(&mverts[mesh->numverts], verts.getbuf(), verts.length()*sizeof(vert)); + mesh->numverts += verts.length(); + mesh->verts = mverts; + tri *mtris = new tri[mesh->numtris + tris.length()]; + if(mesh->numtris) + { + memcpy(mtris, mesh->tris, mesh->numtris*sizeof(tri)); + delete[] mesh->tris; + } + memcpy(&mtris[mesh->numtris], tris.getbuf(), tris.length()*sizeof(tri)); + mesh->numtris += tris.length(); + mesh->tris = mtris; + } + }; + + struct smdvertkey : vert + { + smdmeshdata *mesh; + + smdvertkey(smdmeshdata *mesh) : mesh(mesh) {} + }; + + void readtriangles(stream *f, char *buf, size_t bufsize) + { + smdmeshdata *curmesh = NULL; + hashtable materials(1<<6); + hashset verts(1<<12); + while(f->getline(buf, bufsize)) + { + char *curbuf = buf; + if(skipcomment(curbuf)) continue; + if(!strncmp(curbuf, "end", 3)) break; + string material; + readmaterial(curbuf, material, sizeof(material)); + if(!curmesh || strcmp(curmesh->mesh->name, material)) + { + curmesh = materials.access(material); + if(!curmesh) + { + smdmesh *m = new smdmesh; + m->group = this; + m->name = newstring(material); + meshes.add(m); + curmesh = &materials[m->name]; + curmesh->mesh = m; + } + } + tri curtri; + loopi(3) + { + char *curbuf; + do + { + if(!f->getline(buf, bufsize)) goto endsection; + curbuf = buf; + } while(skipcomment(curbuf)); + smdvertkey key(curmesh); + int parent = -1, numlinks = 0, len = 0; + if(sscanf(curbuf, " %d %f %f %f %f %f %f %f %f %d%n", &parent, &key.pos.x, &key.pos.y, &key.pos.z, &key.norm.x, &key.norm.y, &key.norm.z, &key.tc.x, &key.tc.y, &numlinks, &len) < 9) goto endsection; + curbuf += len; + key.pos.y = -key.pos.y; + key.norm.y = -key.norm.y; + key.tc.y = 1 - key.tc.y; + blendcombo c; + int sorted = 0; + float pweight = 0, tweight = 0; + for(; numlinks > 0; numlinks--) + { + int bone = -1, len = 0; + float weight = 0; + if(sscanf(curbuf, " %d %f%n", &bone, &weight, &len) < 2) break; + curbuf += len; + tweight += weight; + if(bone == parent) pweight += weight; + else sorted = c.addweight(sorted, weight, bone); + } + if(tweight < 1) pweight += 1 - tweight; + if(pweight > 0) sorted = c.addweight(sorted, pweight, parent); + c.finalize(sorted); + key.blend = curmesh->mesh->addblendcombo(c); + int index = verts.access(key, curmesh->verts.length()); + if(index == curmesh->verts.length()) curmesh->verts.add(key); + curtri.vert[2-i] = index; + } + curmesh->tris.add(curtri); + } + endsection: + enumerate(materials, smdmeshdata, data, data.finalize()); + } + + void readskeleton(stream *f, char *buf, size_t bufsize) + { + int frame = -1; + while(f->getline(buf, bufsize)) + { + char *curbuf = buf; + if(skipcomment(curbuf)) continue; + if(sscanf(curbuf, " time %d", &frame) == 1) continue; + else if(!strncmp(curbuf, "end", 3)) break; + else if(frame != 0) continue; + int bone; + vec pos, rot; + if(sscanf(curbuf, " %d %f %f %f %f %f %f", &bone, &pos.x, &pos.y, &pos.z, &rot.x, &rot.y, &rot.z) != 7) + continue; + if(bone < 0 || bone >= skel->numbones) + continue; + rot.x = -rot.x; + rot.z = -rot.z; + float cx = cosf(rot.x/2), sx = sinf(rot.x/2), + cy = cosf(rot.y/2), sy = sinf(rot.y/2), + cz = cosf(rot.z/2), sz = sinf(rot.z/2); + pos.y = -pos.y; + dualquat dq(quat(sx*cy*cz - cx*sy*sz, + cx*sy*cz + sx*cy*sz, + cx*cy*sz - sx*sy*cz, + cx*cy*cz + sx*sy*sz), + pos); + boneinfo &b = skel->bones[bone]; + if(b.parent < 0) b.base = dq; + else b.base.mul(skel->bones[b.parent].base, dq); + (b.invbase = b.base).invert(); + } + } + + bool loadmesh(const char *filename) + { + stream *f = openfile(filename, "r"); + if(!f) return false; + + char buf[512]; + int version = -1; + while(f->getline(buf, sizeof(buf))) + { + char *curbuf = buf; + if(skipcomment(curbuf)) continue; + if(sscanf(curbuf, " version %d", &version) == 1) + { + if(version != 1) { delete f; return false; } + } + else if(!strncmp(curbuf, "nodes", 5)) + { + if(skel->numbones > 0) { skipsection(f, buf, sizeof(buf)); continue; } + vector bones; + readnodes(f, buf, sizeof(buf), bones); + if(bones.empty()) continue; + skel->numbones = bones.length(); + skel->bones = new boneinfo[skel->numbones]; + loopv(bones) + { + boneinfo &dst = skel->bones[i]; + smdbone &src = bones[i]; + dst.name = newstring(src.name); + dst.parent = src.parent; + } + skel->linkchildren(); + } + else if(!strncmp(curbuf, "triangles", 9)) + readtriangles(f, buf, sizeof(buf)); + else if(!strncmp(curbuf, "skeleton", 8)) + { + if(skel->shared > 1) skipsection(f, buf, sizeof(buf)); + else readskeleton(f, buf, sizeof(buf)); + } + else if(!strncmp(curbuf, "vertexanimation", 15)) + skipsection(f, buf, sizeof(buf)); + } + + sortblendcombos(); + + delete f; + return true; + } + + int readframes(stream *f, char *buf, size_t bufsize, vector &animbones) + { + int frame = -1, numframes = 0, lastbone = skel->numbones; + while(f->getline(buf, bufsize)) + { + char *curbuf = buf; + if(skipcomment(curbuf)) continue; + int nextframe = -1; + if(sscanf(curbuf, " time %d", &nextframe) == 1) + { + for(; lastbone < skel->numbones; lastbone++) animbones[frame*skel->numbones + lastbone] = animbones[lastbone]; + if(nextframe >= numframes) + { + databuf framebones = animbones.reserve(skel->numbones * (nextframe + 1 - numframes)); + loopi(nextframe - numframes) framebones.put(animbones.getbuf(), skel->numbones); + animbones.addbuf(framebones); + animbones.advance(skel->numbones); + numframes = nextframe + 1; + } + frame = nextframe; + lastbone = 0; + continue; + } + else if(!strncmp(curbuf, "end", 3)) break; + int bone; + vec pos, rot; + if(sscanf(curbuf, " %d %f %f %f %f %f %f", &bone, &pos.x, &pos.y, &pos.z, &rot.x, &rot.y, &rot.z) != 7) + continue; + if(bone < 0 || bone >= skel->numbones) + continue; + for(; lastbone < bone; lastbone++) animbones[frame*skel->numbones + lastbone] = animbones[lastbone]; + lastbone++; + float cx = cosf(rot.x/2), sx = sinf(rot.x/2), + cy = cosf(rot.y/2), sy = sinf(rot.y/2), + cz = cosf(rot.z/2), sz = sinf(rot.z/2); + pos.y = -pos.y; + dualquat dq(quat(-(sx*cy*cz - cx*sy*sz), + cx*sy*cz + sx*cy*sz, + -(cx*cy*sz - sx*sy*cz), + cx*cy*cz + sx*sy*sz), + pos); + if(adjustments.inrange(bone)) adjustments[bone].adjust(dq); + dq.mul(skel->bones[bone].invbase); + dualquat &dst = animbones[frame*skel->numbones + bone]; + if(skel->bones[bone].parent < 0) dst = dq; + else dst.mul(skel->bones[skel->bones[bone].parent].base, dq); + dst.fixantipodal(skel->numframes > 0 ? skel->framebones[bone] : animbones[bone]); + } + for(; lastbone < skel->numbones; lastbone++) animbones[frame*skel->numbones + lastbone] = animbones[lastbone]; + return numframes; + } + + skelanimspec *loadanim(const char *filename) + { + skelanimspec *sa = skel->findskelanim(filename); + if(sa || skel->numbones <= 0) return sa; + + stream *f = openfile(filename, "r"); + if(!f) return NULL; + + char buf[512]; + int version = -1; + vector animbones; + while(f->getline(buf, sizeof(buf))) + { + char *curbuf = buf; + if(skipcomment(curbuf)) continue; + if(sscanf(curbuf, " version %d", &version) == 1) + { + if(version != 1) { delete f; return NULL; } + } + else if(!strncmp(curbuf, "nodes", 5)) + { + vector bones; + readnodes(f, buf, sizeof(buf), bones); + if(bones.length() != skel->numbones) { delete f; return NULL; } + } + else if(!strncmp(curbuf, "triangles", 9)) + skipsection(f, buf, sizeof(buf)); + else if(!strncmp(curbuf, "skeleton", 8)) + readframes(f, buf, sizeof(buf), animbones); + else if(!strncmp(curbuf, "vertexanimation", 15)) + skipsection(f, buf, sizeof(buf)); + } + int numframes = animbones.length() / skel->numbones; + dualquat *framebones = new dualquat[(skel->numframes+numframes)*skel->numbones]; + if(skel->framebones) + { + memcpy(framebones, skel->framebones, skel->numframes*skel->numbones*sizeof(dualquat)); + delete[] skel->framebones; + } + memcpy(&framebones[skel->numframes*skel->numbones], animbones.getbuf(), numframes*skel->numbones*sizeof(dualquat)); + skel->framebones = framebones; + sa = &skel->addskelanim(filename); + sa->frame = skel->numframes; + sa->range = numframes; + skel->numframes += numframes; + + delete f; + + return sa; + } + + bool load(const char *meshfile) + { + name = newstring(meshfile); + + if(!loadmesh(meshfile)) return false; + + return true; + } + }; + + meshgroup *loadmeshes(const char *name, va_list args) + { + smdmeshgroup *group = new smdmeshgroup; + group->shareskeleton(va_arg(args, char *)); + if(!group->load(name)) { delete group; return NULL; } + return group; + } + + bool loaddefaultparts() + { + skelpart &mdl = addpart(); + mdl.pitchscale = mdl.pitchoffset = mdl.pitchmin = mdl.pitchmax = 0; + adjustments.setsize(0); + const char *fname = name + strlen(name); + do --fname; while(fname >= name && *fname!='/' && *fname!='\\'); + fname++; + defformatstring(meshname, "packages/models/%s/%s.smd", name, fname); + mdl.meshes = sharemeshes(path(meshname), NULL); + if(!mdl.meshes) return false; + mdl.initanimparts(); + mdl.initskins(); + return true; + } +}; + +static inline uint hthash(const smd::smdmeshgroup::smdvertkey &k) +{ + return hthash(k.pos); +} + +static inline bool htcmp(const smd::smdmeshgroup::smdvertkey &k, int index) +{ + if(!k.mesh->verts.inrange(index)) return false; + const smd::vert &v = k.mesh->verts[index]; + return k.pos == v.pos && k.norm == v.norm && k.tc == v.tc && k.blend == v.blend; +} + +skelcommands smdcommands; + diff --git a/src/engine/sound.cpp b/src/engine/sound.cpp new file mode 100644 index 0000000..38ff025 --- /dev/null +++ b/src/engine/sound.cpp @@ -0,0 +1,991 @@ +// sound.cpp: basic positional sound using sdl_mixer + +#include "engine.h" +#include "SDL_mixer.h" + +bool nosound = true; + +struct soundsample +{ + char *name; + Mix_Chunk *chunk; + + soundsample() : name(NULL), chunk(NULL) {} + ~soundsample() { DELETEA(name); } + + void cleanup() { if(chunk) { Mix_FreeChunk(chunk); chunk = NULL; } } + bool load(bool msg = false); +}; + +struct soundslot +{ + soundsample *sample; + int volume; +}; + +struct soundconfig +{ + int slots, numslots; + int maxuses; + + bool hasslot(const soundslot *p, const vector &v) const + { + return p >= v.getbuf() + slots && p < v.getbuf() + slots+numslots && slots+numslots < v.length(); + } + + int chooseslot(int flags) const + { + if(flags&SND_NO_ALT || numslots <= 1) return slots; + if(flags&SND_USE_ALT) return slots + 1 + rnd(numslots - 1); + return slots + rnd(numslots); + } +}; + +struct soundchannel +{ + int id; + bool inuse; + vec loc; + soundslot *slot; + extentity *ent; + int radius, volume, pan, flags; + bool dirty; + + soundchannel(int id) : id(id) { reset(); } + + bool hasloc() const { return loc.x >= -1e15f; } + void clearloc() { loc = vec(-1e16f, -1e16f, -1e16f); } + + void reset() + { + inuse = false; + clearloc(); + slot = NULL; + ent = NULL; + radius = 0; + volume = -1; + pan = -1; + flags = 0; + dirty = false; + } +}; +vector channels; +int maxchannels = 0; + +soundchannel &newchannel(int n, soundslot *slot, const vec *loc = NULL, extentity *ent = NULL, int flags = 0, int radius = 0) +{ + if(ent) + { + loc = &ent->o; + ent->flags |= EF_SOUND; + } + while(!channels.inrange(n)) channels.add(channels.length()); + soundchannel &chan = channels[n]; + chan.reset(); + chan.inuse = true; + if(loc) chan.loc = *loc; + chan.slot = slot; + chan.ent = ent; + chan.flags = 0; + chan.radius = radius; + return chan; +} + +void freechannel(int n) +{ + if(!channels.inrange(n) || !channels[n].inuse) return; + soundchannel &chan = channels[n]; + chan.inuse = false; + if(chan.ent) chan.ent->flags &= ~EF_SOUND; +} + +void syncchannel(soundchannel &chan) +{ + if(!chan.dirty) return; + if(!Mix_FadingChannel(chan.id)) Mix_Volume(chan.id, chan.volume); + Mix_SetPanning(chan.id, 255-chan.pan, chan.pan); + chan.dirty = false; +} + +void stopchannels() +{ + loopv(channels) + { + soundchannel &chan = channels[i]; + if(!chan.inuse) continue; + Mix_HaltChannel(i); + freechannel(i); + } +} + +void setmusicvol(int musicvol); +extern int musicvol; +static int curvol = 0; +VARFP(soundvol, 0, 255, 255, +{ + if(!soundvol) { stopchannels(); setmusicvol(0); } + else if(!curvol) setmusicvol(musicvol); + curvol = soundvol; +}); +VARFP(musicvol, 0, 128, 255, setmusicvol(soundvol ? musicvol : 0)); + +char *musicfile = NULL, *musicdonecmd = NULL; + +Mix_Music *music = NULL; +SDL_RWops *musicrw = NULL; +stream *musicstream = NULL; + +void setmusicvol(int musicvol) +{ + if(nosound) return; + if(music) Mix_VolumeMusic((musicvol*MIX_MAX_VOLUME)/255); +} + +void stopmusic() +{ + if(nosound) return; + DELETEA(musicfile); + DELETEA(musicdonecmd); + if(music) + { + Mix_HaltMusic(); + Mix_FreeMusic(music); + music = NULL; + } + if(musicrw) { SDL_FreeRW(musicrw); musicrw = NULL; } + DELETEP(musicstream); +} + +#ifdef WIN32 +#define AUDIODRIVER "directsound winmm" +#else +#define AUDIODRIVER "" +#endif +bool shouldinitaudio = true; +SVARF(audiodriver, AUDIODRIVER, { shouldinitaudio = true; initwarning("sound configuration", INIT_RESET, CHANGE_SOUND); }); +VARF(usesound, 0, 1, 1, { shouldinitaudio = true; initwarning("sound configuration", INIT_RESET, CHANGE_SOUND); }); +VARF(soundchans, 1, 32, 128, initwarning("sound configuration", INIT_RESET, CHANGE_SOUND)); +VARF(soundfreq, 0, MIX_DEFAULT_FREQUENCY, 48000, initwarning("sound configuration", INIT_RESET, CHANGE_SOUND)); +VARF(soundbufferlen, 128, 1024, 4096, initwarning("sound configuration", INIT_RESET, CHANGE_SOUND)); + +bool initaudio() +{ + static string fallback = ""; + static bool initfallback = true; + static bool restorefallback = false; + if(initfallback) + { + initfallback = false; + if(char *env = SDL_getenv("SDL_AUDIODRIVER")) copystring(fallback, env); + } + if(!fallback[0] && audiodriver[0]) + { + vector drivers; + explodelist(audiodriver, drivers); + loopv(drivers) + { + restorefallback = true; + SDL_setenv("SDL_AUDIODRIVER", drivers[i], 1); + if(SDL_InitSubSystem(SDL_INIT_AUDIO) >= 0) + { + drivers.deletearrays(); + return true; + } + } + drivers.deletearrays(); + } + if(restorefallback) + { + restorefallback = false; + #ifdef WIN32 + SDL_setenv("SDL_AUDIODRIVER", fallback, 1); + #else + unsetenv("SDL_AUDIODRIVER"); + #endif + } + if(SDL_InitSubSystem(SDL_INIT_AUDIO) >= 0) return true; + conoutf(CON_ERROR, "sound init failed: %s", SDL_GetError()); + return false; +} + +void initsound() +{ + SDL_version version; + SDL_GetVersion(&version); + if(version.major == 2 && version.minor == 0 && version.patch == 6) + { + nosound = true; + if(usesound) conoutf(CON_ERROR, "audio is broken in SDL 2.0.6"); + return; + } + + if(shouldinitaudio) + { + shouldinitaudio = false; + if(SDL_WasInit(SDL_INIT_AUDIO)) SDL_QuitSubSystem(SDL_INIT_AUDIO); + if(!usesound || !initaudio()) + { + nosound = true; + return; + } + } + + if(Mix_OpenAudio(soundfreq, MIX_DEFAULT_FORMAT, 2, soundbufferlen)<0) + { + nosound = true; + conoutf(CON_ERROR, "sound init failed (SDL_mixer): %s", Mix_GetError()); + return; + } + Mix_AllocateChannels(soundchans); + maxchannels = soundchans; + nosound = false; +} + +void musicdone() +{ + if(music) { Mix_HaltMusic(); Mix_FreeMusic(music); music = NULL; } + if(musicrw) { SDL_FreeRW(musicrw); musicrw = NULL; } + DELETEP(musicstream); + DELETEA(musicfile); + if(!musicdonecmd) return; + char *cmd = musicdonecmd; + musicdonecmd = NULL; + execute(cmd); + delete[] cmd; +} + +Mix_Music *loadmusic(const char *name) +{ + if(!musicstream) musicstream = openzipfile(name, "rb"); + if(musicstream) + { + if(!musicrw) musicrw = musicstream->rwops(); + if(!musicrw) DELETEP(musicstream); + } + if(musicrw) music = Mix_LoadMUSType_RW(musicrw, MUS_NONE, 0); + else music = Mix_LoadMUS(findfile(name, "rb")); + if(!music) + { + if(musicrw) { SDL_FreeRW(musicrw); musicrw = NULL; } + DELETEP(musicstream); + } + return music; +} + +void startmusic(char *name, char *cmd) +{ + if(nosound) return; + stopmusic(); + if(soundvol && musicvol && *name) + { + defformatstring(file, "packages/%s", name); + path(file); + if(loadmusic(file)) + { + DELETEA(musicfile); + DELETEA(musicdonecmd); + musicfile = newstring(file); + if(cmd[0]) musicdonecmd = newstring(cmd); + Mix_PlayMusic(music, cmd[0] ? 0 : -1); + Mix_VolumeMusic((musicvol*MIX_MAX_VOLUME)/255); + intret(1); + } + else + { + conoutf(CON_ERROR, "could not play music: %s", file); + intret(0); + } + } +} + +COMMANDN(music, startmusic, "ss"); + +static Mix_Chunk *loadwav(const char *name) +{ + Mix_Chunk *c = NULL; + stream *z = openzipfile(name, "rb"); + if(z) + { + SDL_RWops *rw = z->rwops(); + if(rw) + { + c = Mix_LoadWAV_RW(rw, 0); + SDL_FreeRW(rw); + } + delete z; + } + if(!c) c = Mix_LoadWAV(findfile(name, "rb")); + return c; +} + +template static void scalewav(T* dst, T* src, size_t len, int scale) +{ + len /= sizeof(T); + const T* end = src + len; + if(scale==2) for(; src < end; src++, dst += scale) + { + T s = src[0]; + dst[0] = s; + dst[1] = s; + } + else if(scale==4) for(; src < end; src++, dst += scale) + { + T s = src[0]; + dst[0] = s; + dst[1] = s; + dst[2] = s; + dst[3] = s; + } + else for(; src < end; src++) + { + T s = src[0]; + loopi(scale) *dst++ = s; + } +} + +static Mix_Chunk *loadwavscaled(const char *name) +{ + int mixerfreq = 0; + Uint16 mixerformat = 0; + int mixerchannels = 0; + if(!Mix_QuerySpec(&mixerfreq, &mixerformat, &mixerchannels)) return NULL; + + SDL_AudioSpec spec; + Uint8 *audiobuf = NULL; + Uint32 audiolen = 0; + stream *z = openzipfile(name, "rb"); + if(z) + { + SDL_RWops *rw = z->rwops(); + if(rw) + { + SDL_LoadWAV_RW(rw, 0, &spec, &audiobuf, &audiolen); + SDL_FreeRW(rw); + } + delete z; + } + if(!audiobuf) SDL_LoadWAV(findfile(name, "rb"), &spec, &audiobuf, &audiolen); + if(!audiobuf) return NULL; + int samplesize = ((spec.format&0xFF)/8) * spec.channels; + int scale = mixerfreq / spec.freq; + if(scale >= 2) + { + Uint8 *scalebuf = (Uint8*)SDL_malloc(audiolen * scale); + if(scalebuf) + { + switch(samplesize) + { + case 1: scalewav((uchar*)scalebuf, (uchar*)audiobuf, audiolen, scale); break; + case 2: scalewav((ushort*)scalebuf, (ushort*)audiobuf, audiolen, scale); break; + case 4: scalewav((uint*)scalebuf, (uint*)audiobuf, audiolen, scale); break; + case 8: scalewav((ullong*)scalebuf, (ullong*)audiobuf, audiolen, scale); break; + default: SDL_free(scalebuf); scalebuf = NULL; break; + } + if(scalebuf) + { + SDL_free(audiobuf); + audiobuf = scalebuf; + audiolen *= scale; + spec.freq *= scale; + } + } + } + if(spec.freq != mixerfreq || spec.format != mixerformat || spec.channels != mixerchannels) + { + SDL_AudioCVT cvt; + if(SDL_BuildAudioCVT(&cvt, spec.format, spec.channels, spec.freq, mixerformat, mixerchannels, mixerfreq) < 0) + { + SDL_free(audiobuf); + return NULL; + } + if(cvt.filters[0]) + { + cvt.len = audiolen & ~(samplesize-1); + cvt.buf = (Uint8*)SDL_malloc(cvt.len * cvt.len_mult); + if(!cvt.buf) { SDL_free(audiobuf); return NULL; } + SDL_memcpy(cvt.buf, audiobuf, cvt.len); + SDL_free(audiobuf); + if(SDL_ConvertAudio(&cvt) < 0) { SDL_free(cvt.buf); return NULL; } + audiobuf = cvt.buf; + audiolen = cvt.len_cvt; + } + } + Mix_Chunk *c = Mix_QuickLoad_RAW(audiobuf, audiolen); + if(!c) { SDL_free(audiobuf); return NULL; } + c->allocated = 1; + return c; +} + +VARFP(fixwav, 0, 1, 1, initwarning("sound configuration", INIT_LOAD, CHANGE_SOUND)); + +bool soundsample::load(bool msg) +{ + if(chunk) return true; + if(!name[0]) return false; + + static const char * const exts[] = { "", ".wav", ".ogg" }; + string filename; + loopi(sizeof(exts)/sizeof(exts[0])) + { + formatstring(filename, "packages/sounds/%s%s", name, exts[i]); + if(msg && !i) renderprogress(0, filename); + path(filename); + if(fixwav) + { + size_t len = strlen(filename); + if(len >= 4 && !strcasecmp(filename + len - 4, ".wav")) + { + chunk = loadwavscaled(filename); + if(chunk) return true; + } + } + chunk = loadwav(filename); + if(chunk) return true; + } + + conoutf(CON_ERROR, "failed to load sample: packages/sounds/%s", name); + return false; +} + +static hashnameset samples; + +static void cleanupsamples() +{ + enumerate(samples, soundsample, s, s.cleanup()); +} + +static struct soundtype +{ + vector slots; + vector configs; + + int findsound(const char *name, int vol) + { + loopv(configs) + { + soundconfig &s = configs[i]; + loopj(s.numslots) + { + soundslot &c = slots[s.slots+j]; + if(!strcmp(c.sample->name, name) && (!vol || c.volume==vol)) return i; + } + } + return -1; + } + + int addslot(const char *name, int vol) + { + soundsample *s = samples.access(name); + if(!s) + { + char *n = newstring(name); + s = &samples[n]; + s->name = n; + s->chunk = NULL; + } + soundslot *oldslots = slots.getbuf(); + int oldlen = slots.length(); + soundslot &slot = slots.add(); + // soundslots.add() may relocate slot pointers + if(slots.getbuf() != oldslots) loopv(channels) + { + soundchannel &chan = channels[i]; + if(chan.inuse && chan.slot >= oldslots && chan.slot < &oldslots[oldlen]) + chan.slot = &slots[chan.slot - oldslots]; + } + slot.sample = s; + slot.volume = vol ? vol : 100; + return oldlen; + } + + int addsound(const char *name, int vol, int maxuses = 0) + { + soundconfig &s = configs.add(); + s.slots = addslot(name, vol); + s.numslots = 1; + s.maxuses = maxuses; + return configs.length()-1; + } + + void addalt(const char *name, int vol) + { + if(configs.empty()) return; + addslot(name, vol); + configs.last().numslots++; + } + + void clear() + { + slots.setsize(0); + configs.setsize(0); + } + + void reset() + { + loopv(channels) + { + soundchannel &chan = channels[i]; + if(chan.inuse && slots.inbuf(chan.slot)) + { + Mix_HaltChannel(i); + freechannel(i); + } + } + clear(); + } + + void preloadsound(int n) + { + if(nosound || !configs.inrange(n)) return; + soundconfig &config = configs[n]; + loopk(config.numslots) slots[config.slots+k].sample->load(true); + } + + bool playing(const soundchannel &chan, const soundconfig &config) const + { + return chan.inuse && config.hasslot(chan.slot, slots); + } +} gamesounds, mapsounds; + +void registersound(char *name, int *vol) { intret(gamesounds.addsound(name, *vol, 0)); } +COMMAND(registersound, "si"); + +void mapsound(char *name, int *vol, int *maxuses) { intret(mapsounds.addsound(name, *vol, *maxuses < 0 ? 0 : max(1, *maxuses))); } +COMMAND(mapsound, "sii"); + +void altsound(char *name, int *vol) { gamesounds.addalt(name, *vol); } +COMMAND(altsound, "si"); + +void altmapsound(char *name, int *vol) { mapsounds.addalt(name, *vol); } +COMMAND(altmapsound, "si"); + +ICOMMAND(numsounds, "", (), intret(gamesounds.configs.length())); +ICOMMAND(nummapsounds, "", (), intret(mapsounds.configs.length())); + +void soundreset() +{ + gamesounds.reset(); +} +COMMAND(soundreset, ""); + +void mapsoundreset() +{ + mapsounds.reset(); +} +COMMAND(mapsoundreset, ""); + +void resetchannels() +{ + loopv(channels) if(channels[i].inuse) freechannel(i); + channels.shrink(0); +} + +void clear_sound() +{ + closemumble(); + if(nosound) return; + stopmusic(); + + cleanupsamples(); + gamesounds.clear(); + mapsounds.clear(); + samples.clear(); + Mix_CloseAudio(); + resetchannels(); +} + +void stopmapsounds() +{ + loopv(channels) if(channels[i].inuse && channels[i].ent) + { + Mix_HaltChannel(i); + freechannel(i); + } +} + +void clearmapsounds() +{ + stopmapsounds(); + mapsounds.clear(); +} + +void stopmapsound(extentity *e) +{ + loopv(channels) + { + soundchannel &chan = channels[i]; + if(chan.inuse && chan.ent == e) + { + Mix_HaltChannel(i); + freechannel(i); + } + } +} + +void checkmapsounds() +{ + const vector &ents = entities::getents(); + loopv(ents) + { + extentity &e = *ents[i]; + if(e.type!=ET_SOUND) continue; + if(camera1->o.dist(e.o) < e.attr2) + { + if(!(e.flags&EF_SOUND)) playsound(e.attr1, NULL, &e, SND_MAP, -1); + } + else if(e.flags&EF_SOUND) stopmapsound(&e); + } +} + +VAR(stereo, 0, 1, 1); + +bool updatechannel(soundchannel &chan) +{ + if(!chan.slot) return false; + int vol = soundvol, pan = 255/2; + if(chan.hasloc()) + { + vec v; + float dist = chan.loc.dist(camera1->o, v); + int rad = 0; + if(chan.ent) + { + rad = chan.ent->attr2; + if(chan.ent->attr3) + { + rad -= chan.ent->attr3; + dist -= chan.ent->attr3; + } + } + else if(chan.radius > 0) rad = chan.radius; + if(rad > 0) vol -= int(clamp(dist/rad, 0.0f, 1.0f)*soundvol); // simple mono distance attenuation + if(stereo && (v.x != 0 || v.y != 0) && dist>0) + { + v.rotate_around_z(-camera1->yaw*RAD); + pan = int(255.9f*(0.5f - 0.5f*v.x/v.magnitude2())); // range is from 0 (left) to 255 (right) + } + } + vol = (vol*MIX_MAX_VOLUME*chan.slot->volume)/255/255; + vol = min(vol, MIX_MAX_VOLUME); + if(vol == chan.volume && pan == chan.pan) return false; + chan.volume = vol; + chan.pan = pan; + chan.dirty = true; + return true; +} + +void reclaimchannels() +{ + loopv(channels) + { + soundchannel &chan = channels[i]; + if(chan.inuse && !Mix_Playing(i)) freechannel(i); + } +} + +void syncchannels() +{ + loopv(channels) + { + soundchannel &chan = channels[i]; + if(chan.inuse && chan.hasloc() && updatechannel(chan)) syncchannel(chan); + } +} + +VARP(minimizedsounds, 0, 0, 1); + +void updatesounds() +{ + updatemumble(); + if(nosound) return; + if(minimized && !minimizedsounds) stopsounds(); + else + { + reclaimchannels(); + if(mainmenu) stopmapsounds(); + else checkmapsounds(); + syncchannels(); + } + if(music) + { + if(!Mix_PlayingMusic()) musicdone(); + else if(Mix_PausedMusic()) Mix_ResumeMusic(); + } +} + +VARP(maxsoundsatonce, 0, 7, 100); + +VAR(dbgsound, 0, 0, 1); + +void preloadsound(int n) +{ + gamesounds.preloadsound(n); +} + +void preloadmapsound(int n) +{ + mapsounds.preloadsound(n); +} + +void preloadmapsounds() +{ + const vector &ents = entities::getents(); + loopv(ents) + { + extentity &e = *ents[i]; + if(e.type==ET_SOUND) mapsounds.preloadsound(e.attr1); + } +} + +int playsound(int n, const vec *loc, extentity *ent, int flags, int loops, int fade, int chanid, int radius, int expire) +{ + if(nosound || !soundvol || (minimized && !minimizedsounds)) return -1; + + soundtype &sounds = ent || flags&SND_MAP ? mapsounds : gamesounds; + if(!sounds.configs.inrange(n)) { conoutf(CON_WARN, "unregistered sound: %d", n); return -1; } + soundconfig &config = sounds.configs[n]; + + if(loc) + { + // cull sounds that are unlikely to be heard + int maxrad = game::maxsoundradius(n); + if(radius <= 0 || maxrad < radius) radius = maxrad; + if(camera1->o.dist(*loc) > 1.5f*radius) + { + if(channels.inrange(chanid) && sounds.playing(channels[chanid], config)) + { + Mix_HaltChannel(chanid); + freechannel(chanid); + } + return -1; + } + } + + if(chanid < 0) + { + if(config.maxuses) + { + int uses = 0; + loopv(channels) if(sounds.playing(channels[i], config) && ++uses >= config.maxuses) return -1; + } + + // avoid bursts of sounds with heavy packetloss and in sp + static int soundsatonce = 0, lastsoundmillis = 0; + if(totalmillis == lastsoundmillis) soundsatonce++; else soundsatonce = 1; + lastsoundmillis = totalmillis; + if(maxsoundsatonce && soundsatonce > maxsoundsatonce) return -1; + } + + if(channels.inrange(chanid)) + { + soundchannel &chan = channels[chanid]; + if(sounds.playing(chan, config)) + { + if(loc) chan.loc = *loc; + else if(chan.hasloc()) chan.clearloc(); + return chanid; + } + } + if(fade < 0) return -1; + + soundslot &slot = sounds.slots[config.chooseslot(flags)]; + if(!slot.sample->chunk && !slot.sample->load()) return -1; + + if(dbgsound) conoutf(CON_DEBUG, "sound: %s", slot.sample->name); + + chanid = -1; + loopv(channels) if(!channels[i].inuse) { chanid = i; break; } + if(chanid < 0 && channels.length() < maxchannels) chanid = channels.length(); + if(chanid < 0) loopv(channels) if(!channels[i].volume) { chanid = i; break; } + if(chanid < 0) return -1; + + soundchannel &chan = newchannel(chanid, &slot, loc, ent, flags, radius); + updatechannel(chan); + int playing = -1; + if(fade) + { + Mix_Volume(chanid, chan.volume); + playing = expire >= 0 ? Mix_FadeInChannelTimed(chanid, slot.sample->chunk, loops, fade, expire) : Mix_FadeInChannel(chanid, slot.sample->chunk, loops, fade); + } + else playing = expire >= 0 ? Mix_PlayChannelTimed(chanid, slot.sample->chunk, loops, expire) : Mix_PlayChannel(chanid, slot.sample->chunk, loops); + if(playing >= 0) syncchannel(chan); + else freechannel(chanid); + return playing; +} + +void stopsounds() +{ + loopv(channels) if(channels[i].inuse) + { + Mix_HaltChannel(i); + freechannel(i); + } +} + +bool stopsound(int n, int chanid, int fade) +{ + if(!gamesounds.configs.inrange(n) || !channels.inrange(chanid) || !channels[chanid].inuse || !gamesounds.playing(channels[chanid], gamesounds.configs[n])) return false; + if(dbgsound) conoutf(CON_DEBUG, "stopsound: %s", channels[chanid].slot->sample->name); + if(!fade || !Mix_FadeOutChannel(chanid, fade)) + { + Mix_HaltChannel(chanid); + freechannel(chanid); + } + return true; +} + +int playsoundname(const char *s, const vec *loc, int vol, int flags, int loops, int fade, int chanid, int radius, int expire) +{ + if(!vol) vol = 100; + int id = gamesounds.findsound(s, vol); + if(id < 0) id = gamesounds.addsound(s, vol); + return playsound(id, loc, NULL, flags, loops, fade, chanid, radius, expire); +} + +ICOMMAND(sound, "i", (int *n), playsound(*n)); + +void resetsound() +{ + clearchanges(CHANGE_SOUND); + if(!nosound) + { + cleanupsamples(); + if(music) + { + Mix_HaltMusic(); + Mix_FreeMusic(music); + } + if(musicstream) musicstream->seek(0, SEEK_SET); + Mix_CloseAudio(); + } + initsound(); + resetchannels(); + if(nosound) + { + DELETEA(musicfile); + DELETEA(musicdonecmd); + music = NULL; + cleanupsamples(); + return; + } + if(music && loadmusic(musicfile)) + { + Mix_PlayMusic(music, musicdonecmd ? 0 : -1); + Mix_VolumeMusic((musicvol*MIX_MAX_VOLUME)/255); + } + else + { + DELETEA(musicfile); + DELETEA(musicdonecmd); + } +} + +COMMAND(resetsound, ""); + +#ifdef WIN32 + +#include + +#else + +#include + +#ifdef _POSIX_SHARED_MEMORY_OBJECTS +#include +#include +#include +#include +#include +#endif + +#endif + +#if defined(WIN32) || defined(_POSIX_SHARED_MEMORY_OBJECTS) +struct MumbleInfo +{ + int version, timestamp; + vec pos, front, top; + wchar_t name[256]; +}; +#endif + +#ifdef WIN32 +static HANDLE mumblelink = NULL; +static MumbleInfo *mumbleinfo = NULL; +#define VALID_MUMBLELINK (mumblelink && mumbleinfo) +#elif defined(_POSIX_SHARED_MEMORY_OBJECTS) +static int mumblelink = -1; +static MumbleInfo *mumbleinfo = (MumbleInfo *)-1; +#define VALID_MUMBLELINK (mumblelink >= 0 && mumbleinfo != (MumbleInfo *)-1) +#endif + +#ifdef VALID_MUMBLELINK +VARFP(mumble, 0, 1, 1, { if(mumble) initmumble(); else closemumble(); }); +#else +VARFP(mumble, 0, 0, 1, { if(mumble) initmumble(); else closemumble(); }); +#endif + +void initmumble() +{ + if(!mumble) return; +#ifdef VALID_MUMBLELINK + if(VALID_MUMBLELINK) return; + + #ifdef WIN32 + mumblelink = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, "MumbleLink"); + if(mumblelink) + { + mumbleinfo = (MumbleInfo *)MapViewOfFile(mumblelink, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(MumbleInfo)); + if(mumbleinfo) wcsncpy(mumbleinfo->name, L"Sauerbraten", 256); + } + #elif defined(_POSIX_SHARED_MEMORY_OBJECTS) + defformatstring(shmname, "/MumbleLink.%d", getuid()); + mumblelink = shm_open(shmname, O_RDWR, 0); + if(mumblelink >= 0) + { + mumbleinfo = (MumbleInfo *)mmap(NULL, sizeof(MumbleInfo), PROT_READ|PROT_WRITE, MAP_SHARED, mumblelink, 0); + if(mumbleinfo != (MumbleInfo *)-1) wcsncpy(mumbleinfo->name, L"Sauerbraten", 256); + } + #endif + if(!VALID_MUMBLELINK) closemumble(); +#else + conoutf(CON_ERROR, "Mumble positional audio is not available on this platform."); +#endif +} + +void closemumble() +{ +#ifdef WIN32 + if(mumbleinfo) { UnmapViewOfFile(mumbleinfo); mumbleinfo = NULL; } + if(mumblelink) { CloseHandle(mumblelink); mumblelink = NULL; } +#elif defined(_POSIX_SHARED_MEMORY_OBJECTS) + if(mumbleinfo != (MumbleInfo *)-1) { munmap(mumbleinfo, sizeof(MumbleInfo)); mumbleinfo = (MumbleInfo *)-1; } + if(mumblelink >= 0) { close(mumblelink); mumblelink = -1; } +#endif +} + +static inline vec mumblevec(const vec &v, bool pos = false) +{ + // change from X left, Z up, Y forward to X right, Y up, Z forward + // 8 cube units = 1 meter + vec m(-v.x, v.z, v.y); + if(pos) m.div(8); + return m; +} + +void updatemumble() +{ +#ifdef VALID_MUMBLELINK + if(!VALID_MUMBLELINK) return; + + static int timestamp = 0; + + mumbleinfo->version = 1; + mumbleinfo->timestamp = ++timestamp; + + mumbleinfo->pos = mumblevec(player->o, true); + mumbleinfo->front = mumblevec(vec(RAD*player->yaw, RAD*player->pitch)); + mumbleinfo->top = mumblevec(vec(RAD*player->yaw, RAD*(player->pitch+90))); +#endif +} + diff --git a/src/engine/textedit.h b/src/engine/textedit.h new file mode 100644 index 0000000..c273661 --- /dev/null +++ b/src/engine/textedit.h @@ -0,0 +1,770 @@ + +struct editline +{ + enum { CHUNKSIZE = 256 }; + + char *text; + int len, maxlen; + + editline() : text(NULL), len(0), maxlen(0) {} + editline(const char *init) : text(NULL), len(0), maxlen(0) + { + set(init); + } + + bool empty() { return len <= 0; } + + void clear() + { + DELETEA(text); + len = maxlen = 0; + } + + bool grow(int total, const char *fmt = "", ...) + { + if(total + 1 <= maxlen) return false; + maxlen = (total + CHUNKSIZE) - total%CHUNKSIZE; + char *newtext = new char[maxlen]; + if(fmt) + { + va_list args; + va_start(args, fmt); + vformatstring(newtext, fmt, args, maxlen); + va_end(args); + } + else newtext[0] = '\0'; + DELETEA(text); + text = newtext; + return true; + } + + void set(const char *str, int slen = -1) + { + if(slen < 0) + { + slen = strlen(str); + if(!grow(slen, "%s", str)) memcpy(text, str, slen + 1); + } + else + { + grow(slen); + memcpy(text, str, slen); + text[slen] = '\0'; + } + len = slen; + } + + void prepend(const char *str) + { + int slen = strlen(str); + if(!grow(slen + len, "%s%s", str, text ? text : "")) + { + memmove(&text[slen], text, len + 1); + memcpy(text, str, slen + 1); + } + len += slen; + } + + void append(const char *str) + { + int slen = strlen(str); + if(!grow(len + slen, "%s%s", text ? text : "", str)) memcpy(&text[len], str, slen + 1); + len += slen; + } + + bool read(stream *f, int chop = -1) + { + if(chop < 0) chop = INT_MAX; else chop++; + set(""); + while(len + 1 < chop && f->getline(&text[len], min(maxlen, chop) - len)) + { + len += strlen(&text[len]); + if(len > 0 && text[len-1] == '\n') + { + text[--len] = '\0'; + return true; + } + if(len + 1 >= maxlen && len + 1 < chop) grow(len + CHUNKSIZE, "%s", text); + } + if(len + 1 >= chop) + { + char buf[CHUNKSIZE]; + while(f->getline(buf, sizeof(buf))) + { + int blen = strlen(buf); + if(blen > 0 && buf[blen-1] == '\n') return true; + } + } + return len > 0; + } + + void del(int start, int count) + { + if(!text) return; + if(start < 0) { count += start; start = 0; } + if(count <= 0 || start >= len) return; + if(start + count > len) count = len - start - 1; + memmove(&text[start], &text[start+count], len + 1 - (start + count)); + len -= count; + } + + void chop(int newlen) + { + if(!text) return; + len = clamp(newlen, 0, len); + text[len] = '\0'; + } + + void insert(char *str, int start, int count = 0) + { + if(count <= 0) count = strlen(str); + start = clamp(start, 0, len); + grow(len + count, "%s", text ? text : ""); + memmove(&text[start + count], &text[start], len - start + 1); + memcpy(&text[start], str, count); + len += count; + } + + void combinelines(vector &src) + { + if(src.empty()) set(""); + else loopv(src) + { + if(i) append("\n"); + if(!i) set(src[i].text, src[i].len); + else insert(src[i].text, len, src[i].len); + } + } +}; + +struct editor +{ + int mode; //editor mode - 1= keep while focused, 2= keep while used in gui, 3= keep forever (i.e. until mode changes) + bool active, rendered; + const char *name; + const char *filename; + + int cx, cy; // cursor position - ensured to be valid after a region() or currentline() + int mx, my; // selection mark, mx=-1 if following cursor - avoid direct access, instead use region() + int maxx, maxy; // maxy=-1 if unlimited lines, 1 if single line editor + + int scrolly; // vertical scroll offset + + bool linewrap; + int pixelwidth; // required for up/down/hit/draw/bounds + int pixelheight; // -1 for variable sized, i.e. from bounds() + + vector lines; // MUST always contain at least one line! + + editor(const char *name, int mode, const char *initval) : + mode(mode), active(true), rendered(false), name(newstring(name)), filename(NULL), + cx(0), cy(0), mx(-1), maxx(-1), maxy(-1), scrolly(0), linewrap(false), pixelwidth(-1), pixelheight(-1) + { + //printf("editor %08x '%s'\n", this, name); + lines.add().set(initval ? initval : ""); + } + + ~editor() + { + //printf("~editor %08x '%s'\n", this, name); + DELETEA(name); + DELETEA(filename); + clear(NULL); + } + + void clear(const char *init = "") + { + cx = cy = 0; + mark(false); + loopv(lines) lines[i].clear(); + lines.shrink(0); + if(init) lines.add().set(init); + } + + void setfile(const char *fname) + { + DELETEA(filename); + if(fname) filename = newstring(fname); + } + + void load() + { + if(!filename) return; + clear(NULL); + stream *file = openutf8file(filename, "r"); + if(file) + { + while(lines.add().read(file, maxx) && (maxy < 0 || lines.length() <= maxy)); + lines.pop().clear(); + delete file; + } + if(lines.empty()) lines.add().set(""); + } + + void save() + { + if(!filename) return; + stream *file = openutf8file(filename, "w"); + if(!file) return; + loopv(lines) file->putline(lines[i].text); + delete file; + } + + void mark(bool enable) + { + mx = (enable) ? cx : -1; + my = cy; + } + + void selectall() + { + mx = my = INT_MAX; + cx = cy = 0; + } + + // constrain results to within buffer - s=start, e=end, return true if a selection range + // also ensures that cy is always within lines[] and cx is valid + bool region(int &sx, int &sy, int &ex, int &ey) + { + int n = lines.length(); + ASSERT(n != 0); + if(cy < 0) cy = 0; else if(cy >= n) cy = n-1; + int len = lines[cy].len; + if(cx < 0) cx = 0; else if(cx > len) cx = len; + if(mx >= 0) + { + if(my < 0) my = 0; else if(my >= n) my = n-1; + len = lines[my].len; + if(mx > len) mx = len; + } + sx = (mx >= 0) ? mx : cx; + sy = (mx >= 0) ? my : cy; + ex = cx; + ey = cy; + if(sy > ey) { swap(sy, ey); swap(sx, ex); } + else if(sy==ey && sx > ex) swap(sx, ex); + return (sx != ex) || (sy != ey); + } + + bool region() { int sx, sy, ex, ey; return region(sx, sy, ex, ey); } + + // also ensures that cy is always within lines[] and cx is valid + editline ¤tline() + { + int n = lines.length(); + ASSERT(n != 0); + if(cy < 0) cy = 0; else if(cy >= n) cy = n-1; + if(cx < 0) cx = 0; else if(cx > lines[cy].len) cx = lines[cy].len; + return lines[cy]; + } + + void copyselectionto(editor *b) + { + if(b==this) return; + + b->clear(NULL); + int sx, sy, ex, ey; + region(sx, sy, ex, ey); + loopi(1+ey-sy) + { + if(b->maxy != -1 && b->lines.length() >= b->maxy) break; + int y = sy+i; + char *line = lines[y].text; + int len = lines[y].len; + if(y == sy && y == ey) + { + line += sx; + len = ex - sx; + } + else if(y == sy) line += sx; + else if(y == ey) len = ex; + b->lines.add().set(line, len); + } + if(b->lines.empty()) b->lines.add().set(""); + } + + char *tostring() + { + int len = 0; + loopv(lines) len += lines[i].len + 1; + char *str = newstring(len); + int offset = 0; + loopv(lines) + { + editline &l = lines[i]; + memcpy(&str[offset], l.text, l.len); + offset += l.len; + str[offset++] = '\n'; + } + str[offset] = '\0'; + return str; + } + + char *selectiontostring() + { + vector buf; + int sx, sy, ex, ey; + region(sx, sy, ex, ey); + loopi(1+ey-sy) + { + int y = sy+i; + char *line = lines[y].text; + int len = lines[y].len; + if(y == sy && y == ey) + { + line += sx; + len = ex - sx; + } + else if(y == sy) line += sx; + else if(y == ey) len = ex; + buf.put(line, len); + buf.add('\n'); + } + buf.add('\0'); + return newstring(buf.getbuf(), buf.length()-1); + } + + void removelines(int start, int count) + { + loopi(count) lines[start+i].clear(); + lines.remove(start, count); + } + + bool del() // removes the current selection (if any) + { + int sx, sy, ex, ey; + if(!region(sx, sy, ex, ey)) + { + mark(false); + return false; + } + if(sy == ey) + { + if(sx == 0 && ex == lines[ey].len) removelines(sy, 1); + else lines[sy].del(sx, ex - sx); + } + else + { + if(ey > sy+1) { removelines(sy+1, ey-(sy+1)); ey = sy+1; } + if(ex == lines[ey].len) removelines(ey, 1); else lines[ey].del(0, ex); + if(sx == 0) removelines(sy, 1); else lines[sy].del(sx, lines[sy].len - sx); + } + if(lines.empty()) lines.add().set(""); + mark(false); + cx = sx; + cy = sy; + editline ¤t = currentline(); + if(cx >= current.len && cy < lines.length() - 1) + { + current.append(lines[cy+1].text); + removelines(cy + 1, 1); + } + return true; + } + + void insert(char ch) + { + del(); + editline ¤t = currentline(); + if(ch == '\n') + { + if(maxy == -1 || cy < maxy-1) + { + editline newline(¤t.text[cx]); + current.chop(cx); + cy = min(lines.length(), cy+1); + lines.insert(cy, newline); + } + else current.chop(cx); + cx = 0; + } + else + { + int len = current.len; + if(maxx >= 0 && len > maxx-1) len = maxx-1; + if(cx <= len) current.insert(&ch, cx++, 1); + } + } + + void insert(const char *s) + { + while(*s) insert(*s++); + } + + void insertallfrom(editor *b) + { + if(b==this) return; + + del(); + + if(b->lines.length() == 1 || maxy == 1) + { + editline ¤t = currentline(); + char *str = b->lines[0].text; + int slen = b->lines[0].len; + if(maxx >= 0 && b->lines[0].len + cx > maxx) slen = maxx-cx; + if(slen > 0) + { + int len = current.len; + if(maxx >= 0 && slen + cx + len > maxx) len = max(0, maxx-(cx+slen)); + current.insert(str, cx, slen); + cx += slen; + } + } + else + { + loopv(b->lines) + { + if(!i) + { + lines[cy++].append(b->lines[i].text); + } + else if(i >= b->lines.length()) + { + cx = b->lines[i].len; + lines[cy].prepend(b->lines[i].text); + } + else if(maxy < 0 || lines.length() < maxy) lines.insert(cy++, editline(b->lines[i].text)); + } + } + } + + void key(int code) + { + switch(code) + { + case SDLK_UP: + if(linewrap) + { + int x, y; + char *str = currentline().text; + text_pos(str, cx+1, x, y, pixelwidth); + if(y > 0) { cx = text_visible(str, x, y-FONTH, pixelwidth); break; } + } + cy--; + break; + case SDLK_DOWN: + if(linewrap) + { + int x, y, width, height; + char *str = currentline().text; + text_pos(str, cx, x, y, pixelwidth); + text_bounds(str, width, height, pixelwidth); + y += FONTH; + if(y < height) { cx = text_visible(str, x, y, pixelwidth); break; } + } + cy++; + break; + case -4: + cy--; + break; + case -5: + cy++; + break; + case SDLK_PAGEUP: + cy-=pixelheight/FONTH; + break; + case SDLK_PAGEDOWN: + cy+=pixelheight/FONTH; + break; + case SDLK_HOME: + cx = cy = 0; + break; + case SDLK_END: + cx = cy = INT_MAX; + break; + case SDLK_LEFT: + cx--; + break; + case SDLK_RIGHT: + cx++; + break; + case SDLK_DELETE: + if(!del()) + { + editline ¤t = currentline(); + if(cx < current.len) current.del(cx, 1); + else if(cy < lines.length()-1) + { //combine with next line + current.append(lines[cy+1].text); + removelines(cy+1, 1); + } + } + break; + case SDLK_BACKSPACE: + if(!del()) + { + editline ¤t = currentline(); + if(cx > 0) current.del(--cx, 1); + else if(cy > 0) + { //combine with previous line + cx = lines[cy-1].len; + lines[cy-1].append(current.text); + removelines(cy--, 1); + } + } + break; + case SDLK_LSHIFT: + case SDLK_RSHIFT: + break; + case SDLK_RETURN: + insert('\n'); + break; + case SDLK_TAB: + insert('\t'); + break; + } + } + + void input(const char *str, int len) + { + loopi(len) insert(str[i]); + } + + void hit(int hitx, int hity, bool dragged) + { + int maxwidth = linewrap?pixelwidth:-1; + int h = 0; + for(int i = scrolly; i < lines.length(); i++) + { + int width, height; + text_bounds(lines[i].text, width, height, maxwidth); + if(h + height > pixelheight) break; + + if(hity >= h && hity <= h+height) + { + int x = text_visible(lines[i].text, hitx, hity-h, maxwidth); + if(dragged) { mx = x; my = i; } else { cx = x; cy = i; }; + break; + } + h+=height; + } + } + + int limitscrolly() + { + int maxwidth = linewrap?pixelwidth:-1; + int slines = lines.length(); + for(int ph = pixelheight; slines > 0 && ph > 0;) + { + int width, height; + text_bounds(lines[slines-1].text, width, height, maxwidth); + if(height > ph) break; + ph -= height; + slines--; + } + return slines; + } + + void draw(int x, int y, int color, bool hit) + { + int maxwidth = linewrap?pixelwidth:-1; + + int sx, sy, ex, ey; + bool selection = region(sx, sy, ex, ey); + + // fix scrolly so that is always on screen + if(cy < scrolly) scrolly = cy; + else + { + if(scrolly < 0) scrolly = 0; + int h = 0; + for(int i = cy; i >= scrolly; i--) + { + int width, height; + text_bounds(lines[i].text, width, height, maxwidth); + if(h + height > pixelheight) { scrolly = i+1; break; } + h += height; + } + } + + if(selection) + { + // convert from cursor coords into pixel coords + int psx, psy, pex, pey; + text_pos(lines[sy].text, sx, psx, psy, maxwidth); + text_pos(lines[ey].text, ex, pex, pey, maxwidth); + int maxy = lines.length(); + int h = 0; + for(int i = scrolly; i < maxy; i++) + { + int width, height; + text_bounds(lines[i].text, width, height, maxwidth); + if(h + height > pixelheight) { maxy = i; break; } + if(i == sy) psy += h; + if(i == ey) { pey += h; break; } + h += height; + } + maxy--; + + if(ey >= scrolly && sy <= maxy) + { + // crop top/bottom within window + if(sy < scrolly) { sy = scrolly; psy = 0; psx = 0; } + if(ey > maxy) { ey = maxy; pey = pixelheight - FONTH; pex = pixelwidth; } + + hudnotextureshader->set(); + gle::colorub(0xA0, 0x80, 0x80); + gle::defvertex(2); + gle::begin(GL_QUADS); + if(psy == pey) + { + gle::attribf(x+psx, y+psy); + gle::attribf(x+pex, y+psy); + gle::attribf(x+pex, y+pey+FONTH); + gle::attribf(x+psx, y+pey+FONTH); + } + else + { gle::attribf(x+psx, y+psy); + gle::attribf(x+psx, y+psy+FONTH); + gle::attribf(x+pixelwidth, y+psy+FONTH); + gle::attribf(x+pixelwidth, y+psy); + if(pey-psy > FONTH) + { + gle::attribf(x, y+psy+FONTH); + gle::attribf(x+pixelwidth, y+psy+FONTH); + gle::attribf(x+pixelwidth, y+pey); + gle::attribf(x, y+pey); + } + gle::attribf(x, y+pey); + gle::attribf(x, y+pey+FONTH); + gle::attribf(x+pex, y+pey+FONTH); + gle::attribf(x+pex, y+pey); + } + gle::end(); + hudshader->set(); + } + } + + int h = 0; + for(int i = scrolly; i < lines.length(); i++) + { + int width, height; + text_bounds(lines[i].text, width, height, maxwidth); + if(h + height > pixelheight) break; + + draw_text(lines[i].text, x, y+h, color>>16, (color>>8)&0xFF, color&0xFF, 0xFF, hit&&(cy==i)?cx:-1, maxwidth); + if(linewrap && height > FONTH) // line wrap indicator + { + hudnotextureshader->set(); + gle::colorub(0x80, 0xA0, 0x80); + gle::defvertex(2); + gle::begin(GL_TRIANGLE_STRIP); + gle::attribf(x, y+h+FONTH); + gle::attribf(x, y+h+height); + gle::attribf(x-FONTW/2, y+h+FONTH); + gle::attribf(x-FONTW/2, y+h+height); + gle::end(); + hudshader->set(); + } + h+=height; + } + } +}; + +// a 'stack' where the last is the current focused editor +static vector editors; + +static editor *currentfocus() { return editors.length() ? editors.last() : NULL; } + +static void readyeditors() +{ + loopv(editors) editors[i]->active = (editors[i]->mode==EDITORFOREVER); +} + +static void flusheditors() +{ + loopvrev(editors) if(!editors[i]->active) + { + editor *e = editors.remove(i); + DELETEP(e); + } +} + +static editor *useeditor(const char *name, int mode, bool focus, const char *initval = NULL) +{ + loopv(editors) if(strcmp(editors[i]->name, name) == 0) + { + editor *e = editors[i]; + if(focus) { editors.add(e); editors.remove(i); } // re-position as last + e->active = true; + return e; + } + editor *e = new editor(name, mode, initval); + if(focus) editors.add(e); else editors.insert(0, e); + return e; +} + + +#define TEXTCOMMAND(f, s, d, body) ICOMMAND(f, s, d,\ + editor *top = currentfocus();\ + if(!top || identflags&IDF_OVERRIDDEN) return;\ + body\ +) + +ICOMMAND(textlist, "", (), // @DEBUG return list of all the editors + vector s; + loopv(editors) + { + if(i > 0) s.put(", ", 2); + s.put(editors[i]->name, strlen(editors[i]->name)); + } + s.add('\0'); + result(s.getbuf()); +); +TEXTCOMMAND(textshow, "", (), // @DEBUG return the start of the buffer + editline line; + line.combinelines(top->lines); + result(line.text); + line.clear(); +); +ICOMMAND(textfocus, "si", (char *name, int *mode), // focus on a (or create a persistent) specific editor, else returns current name + if(*name) useeditor(name, *mode<=0 ? EDITORFOREVER : *mode, true); + else if(editors.length() > 0) result(editors.last()->name); +); +TEXTCOMMAND(textprev, "", (), editors.insert(0, top); editors.pop();); // return to the previous editor +TEXTCOMMAND(textmode, "i", (int *m), // (1= keep while focused, 2= keep while used in gui, 3= keep forever (i.e. until mode changes)) topmost editor, return current setting if no args + if(*m) top->mode = *m; + else intret(top->mode); +); +TEXTCOMMAND(textsave, "s", (char *file), // saves the topmost (filename is optional) + if(*file) top->setfile(path(file, true)); + top->save(); +); +TEXTCOMMAND(textload, "s", (char *file), // loads into the topmost editor, returns filename if no args + if(*file) + { + top->setfile(path(file, true)); + top->load(); + } + else if(top->filename) result(top->filename); +); +TEXTCOMMAND(textinit, "sss", (char *name, char *file, char *initval), // loads into named editor if no file assigned and editor has been rendered +{ + editor *e = NULL; + loopv(editors) if(!strcmp(editors[i]->name, name)) { e = editors[i]; break; } + if(e && e->rendered && !e->filename && *file && (e->lines.empty() || (e->lines.length() == 1 && !strcmp(e->lines[0].text, initval)))) + { + e->setfile(path(file, true)); + e->load(); + } +}); + +#define PASTEBUFFER "#pastebuffer" + +TEXTCOMMAND(textcopy, "", (), editor *b = useeditor(PASTEBUFFER, EDITORFOREVER, false); top->copyselectionto(b);); +TEXTCOMMAND(textpaste, "", (), editor *b = useeditor(PASTEBUFFER, EDITORFOREVER, false); top->insertallfrom(b);); +TEXTCOMMAND(textmark, "i", (int *m), // (1=mark, 2=unmark), return current mark setting if no args + if(*m) top->mark(*m==1); + else intret(top->region() ? 1 : 2); +); +TEXTCOMMAND(textselectall, "", (), top->selectall();); +TEXTCOMMAND(textclear, "", (), top->clear();); +TEXTCOMMAND(textcurrentline, "", (), result(top->currentline().text);); + +TEXTCOMMAND(textexec, "i", (int *selected), // execute script commands from the buffer (0=all, 1=selected region only) + char *script = *selected ? top->selectiontostring() : top->tostring(); + execute(script); + delete[] script; +); + diff --git a/src/engine/texture.cpp b/src/engine/texture.cpp new file mode 100644 index 0000000..964d39d --- /dev/null +++ b/src/engine/texture.cpp @@ -0,0 +1,3644 @@ +// texture.cpp: texture slot management + +#include "engine.h" +#include "SDL_image.h" + +#ifndef SDL_IMAGE_VERSION_ATLEAST +#define SDL_IMAGE_VERSION_ATLEAST(X, Y, Z) \ + (SDL_VERSIONNUM(SDL_IMAGE_MAJOR_VERSION, SDL_IMAGE_MINOR_VERSION, SDL_IMAGE_PATCHLEVEL) >= SDL_VERSIONNUM(X, Y, Z)) +#endif + +template static void halvetexture(uchar * RESTRICT src, uint sw, uint sh, uint stride, uchar * RESTRICT dst) +{ + for(uchar *yend = &src[sh*stride]; src < yend;) + { + for(uchar *xend = &src[sw*BPP], *xsrc = src; xsrc < xend; xsrc += 2*BPP, dst += BPP) + { + loopi(BPP) dst[i] = (uint(xsrc[i]) + uint(xsrc[i+BPP]) + uint(xsrc[stride+i]) + uint(xsrc[stride+i+BPP]))>>2; + } + src += 2*stride; + } +} + +template static void shifttexture(uchar * RESTRICT src, uint sw, uint sh, uint stride, uchar * RESTRICT dst, uint dw, uint dh) +{ + uint wfrac = sw/dw, hfrac = sh/dh, wshift = 0, hshift = 0; + while(dw<> tshift; + } + src += hfrac*stride; + } +} + +template static void scaletexture(uchar * RESTRICT src, uint sw, uint sh, uint stride, uchar * RESTRICT dst, uint dw, uint dh) +{ + uint wfrac = (sw<<12)/dw, hfrac = (sh<<12)/dh, darea = dw*dh, sarea = sw*sh; + int over, under; + for(over = 0; (darea>>over) > sarea; over++); + for(under = 0; (darea<>12, h = (yn>>12) - yi, ylow = ((yn|(-int(h)>>24))&0xFFFU) + 1 - (y&0xFFFU), yhigh = (yn&0xFFFU) + 1; + const uchar *ysrc = &src[yi*stride]; + for(uint x = 0; x < dw; x += wfrac, dst += BPP) + { + const uint xn = x + wfrac - 1, xi = x>>12, w = (xn>>12) - xi, xlow = ((w+0xFFFU)&0x1000U) - (x&0xFFFU), xhigh = (xn&0xFFFU) + 1; + const uchar *xsrc = &ysrc[xi*BPP], *xend = &xsrc[w*BPP]; + uint t[BPP] = {0}; + for(const uchar *xcur = &xsrc[BPP]; xcur < xend; xcur += BPP) + loopi(BPP) t[i] += xcur[i]; + loopi(BPP) t[i] = (ylow*(t[i] + ((xsrc[i]*xlow + xend[i]*xhigh)>>12)))>>cscale; + if(h) + { + xsrc += stride; + xend += stride; + for(uint hcur = h; --hcur; xsrc += stride, xend += stride) + { + uint c[BPP] = {0}; + for(const uchar *xcur = &xsrc[BPP]; xcur < xend; xcur += BPP) + loopi(BPP) c[i] += xcur[i]; + loopi(BPP) t[i] += ((c[i]<<12) + xsrc[i]*xlow + xend[i]*xhigh)>>cscale; + } + uint c[BPP] = {0}; + for(const uchar *xcur = &xsrc[BPP]; xcur < xend; xcur += BPP) + loopi(BPP) c[i] += xcur[i]; + loopi(BPP) t[i] += (yhigh*(c[i] + ((xsrc[i]*xlow + xend[i]*xhigh)>>12)))>>cscale; + } + loopi(BPP) dst[i] = (t[i] * area)>>dscale; + } + } +} + +static void scaletexture(uchar * RESTRICT src, uint sw, uint sh, uint bpp, uint pitch, uchar * RESTRICT dst, uint dw, uint dh) +{ + if(sw == dw*2 && sh == dh*2) + { + switch(bpp) + { + case 1: return halvetexture<1>(src, sw, sh, pitch, dst); + case 2: return halvetexture<2>(src, sw, sh, pitch, dst); + case 3: return halvetexture<3>(src, sw, sh, pitch, dst); + case 4: return halvetexture<4>(src, sw, sh, pitch, dst); + } + } + else if(sw < dw || sh < dh || sw&(sw-1) || sh&(sh-1) || dw&(dw-1) || dh&(dh-1)) + { + switch(bpp) + { + case 1: return scaletexture<1>(src, sw, sh, pitch, dst, dw, dh); + case 2: return scaletexture<2>(src, sw, sh, pitch, dst, dw, dh); + case 3: return scaletexture<3>(src, sw, sh, pitch, dst, dw, dh); + case 4: return scaletexture<4>(src, sw, sh, pitch, dst, dw, dh); + } + } + else + { + switch(bpp) + { + case 1: return shifttexture<1>(src, sw, sh, pitch, dst, dw, dh); + case 2: return shifttexture<2>(src, sw, sh, pitch, dst, dw, dh); + case 3: return shifttexture<3>(src, sw, sh, pitch, dst, dw, dh); + case 4: return shifttexture<4>(src, sw, sh, pitch, dst, dw, dh); + } + } +} + +static void reorientnormals(uchar * RESTRICT src, int sw, int sh, int bpp, int stride, uchar * RESTRICT dst, bool flipx, bool flipy, bool swapxy) +{ + int stridex = bpp, stridey = bpp; + if(swapxy) stridex *= sh; else stridey *= sw; + if(flipx) { dst += (sw-1)*stridex; stridex = -stridex; } + if(flipy) { dst += (sh-1)*stridey; stridey = -stridey; } + uchar *srcrow = src; + loopi(sh) + { + for(uchar *curdst = dst, *src = srcrow, *end = &srcrow[sw*bpp]; src < end;) + { + uchar nx = *src++, ny = *src++; + if(flipx) nx = 255-nx; + if(flipy) ny = 255-ny; + if(swapxy) swap(nx, ny); + curdst[0] = nx; + curdst[1] = ny; + curdst[2] = *src++; + if(bpp > 3) curdst[3] = *src++; + curdst += stridex; + } + srcrow += stride; + dst += stridey; + } +} + +template +static inline void reorienttexture(uchar * RESTRICT src, int sw, int sh, int stride, uchar * RESTRICT dst, bool flipx, bool flipy, bool swapxy) +{ + int stridex = BPP, stridey = BPP; + if(swapxy) stridex *= sh; else stridey *= sw; + if(flipx) { dst += (sw-1)*stridex; stridex = -stridex; } + if(flipy) { dst += (sh-1)*stridey; stridey = -stridey; } + uchar *srcrow = src; + loopi(sh) + { + for(uchar *curdst = dst, *src = srcrow, *end = &srcrow[sw*BPP]; src < end;) + { + loopk(BPP) curdst[k] = *src++; + curdst += stridex; + } + srcrow += stride; + dst += stridey; + } +} + +static void reorienttexture(uchar * RESTRICT src, int sw, int sh, int bpp, int stride, uchar * RESTRICT dst, bool flipx, bool flipy, bool swapxy) +{ + switch(bpp) + { + case 1: return reorienttexture<1>(src, sw, sh, stride, dst, flipx, flipy, swapxy); + case 2: return reorienttexture<2>(src, sw, sh, stride, dst, flipx, flipy, swapxy); + case 3: return reorienttexture<3>(src, sw, sh, stride, dst, flipx, flipy, swapxy); + case 4: return reorienttexture<4>(src, sw, sh, stride, dst, flipx, flipy, swapxy); + } +} + +static void reorients3tc(GLenum format, int blocksize, int w, int h, uchar *src, uchar *dst, bool flipx, bool flipy, bool swapxy, bool normals = false) +{ + int bx1 = 0, by1 = 0, bx2 = min(w, 4), by2 = min(h, 4), bw = (w+3)/4, bh = (h+3)/4, stridex = blocksize, stridey = blocksize; + if(swapxy) stridex *= bw; else stridey *= bh; + if(flipx) { dst += (bw-1)*stridex; stridex = -stridex; bx1 += 4-bx2; bx2 = 4; } + if(flipy) { dst += (bh-1)*stridey; stridey = -stridey; by1 += 4-by2; by2 = 4; } + loopi(bh) + { + for(uchar *curdst = dst, *end = &src[bw*blocksize]; src < end; src += blocksize, curdst += stridex) + { + if(format == GL_COMPRESSED_RGBA_S3TC_DXT3_EXT) + { + ullong salpha = lilswap(*(const ullong *)src), dalpha = 0; + uint xmask = flipx ? 15 : 0, ymask = flipy ? 15 : 0, xshift = 2, yshift = 4; + if(swapxy) swap(xshift, yshift); + for(int y = by1; y < by2; y++) for(int x = bx1; x < bx2; x++) + { + dalpha |= ((salpha&15) << (((xmask^x)<>= 4; + } + *(ullong *)curdst = lilswap(dalpha); + src += 8; + curdst += 8; + } + else if(format == GL_COMPRESSED_RGBA_S3TC_DXT5_EXT) + { + uchar alpha1 = src[0], alpha2 = src[1]; + ullong salpha = lilswap(*(const ushort *)&src[2]) + ((ullong)lilswap(*(const uint *)&src[4]) << 16), dalpha = 0; + uint xmask = flipx ? 7 : 0, ymask = flipy ? 7 : 0, xshift = 0, yshift = 2; + if(swapxy) swap(xshift, yshift); + for(int y = by1; y < by2; y++) for(int x = bx1; x < bx2; x++) + { + dalpha |= ((salpha&7) << (3*((xmask^x)<>= 3; + } + curdst[0] = alpha1; + curdst[1] = alpha2; + *(ushort *)&curdst[2] = lilswap(ushort(dalpha)); + *(ushort *)&curdst[4] = lilswap(ushort(dalpha>>16)); + *(ushort *)&curdst[6] = lilswap(ushort(dalpha>>32)); + src += 8; + curdst += 8; + } + + ushort color1 = lilswap(*(const ushort *)src), color2 = lilswap(*(const ushort *)&src[2]); + uint sbits = lilswap(*(const uint *)&src[4]); + if(normals) + { + ushort ncolor1 = color1, ncolor2 = color2; + if(flipx) + { + ncolor1 = (ncolor1 & ~0xF800) | (0xF800 - (ncolor1 & 0xF800)); + ncolor2 = (ncolor2 & ~0xF800) | (0xF800 - (ncolor2 & 0xF800)); + } + if(flipy) + { + ncolor1 = (ncolor1 & ~0x7E0) | (0x7E0 - (ncolor1 & 0x7E0)); + ncolor2 = (ncolor2 & ~0x7E0) | (0x7E0 - (ncolor2 & 0x7E0)); + } + if(swapxy) + { + ncolor1 = (ncolor1 & 0x1F) | (((((ncolor1 >> 11) & 0x1F) * 0x3F) / 0x1F) << 5) | (((((ncolor1 >> 5) & 0x3F) * 0x1F) / 0x3F) << 11); + ncolor2 = (ncolor2 & 0x1F) | (((((ncolor2 >> 11) & 0x1F) * 0x3F) / 0x1F) << 5) | (((((ncolor2 >> 5) & 0x3F) * 0x1F) / 0x3F) << 11); + } + if(color1 <= color2 && ncolor1 > ncolor2) { color1 = ncolor2; color2 = ncolor1; } + else { color1 = ncolor1; color2 = ncolor2; } + } + uint dbits = 0, xmask = flipx ? 3 : 0, ymask = flipy ? 3 : 0, xshift = 1, yshift = 3; + if(swapxy) swap(xshift, yshift); + for(int y = by1; y < by2; y++) for(int x = bx1; x < bx2; x++) + { + dbits |= ((sbits&3) << (((xmask^x)<>= 2; + } + *(ushort *)curdst = lilswap(color1); + *(ushort *)&curdst[2] = lilswap(color2); + *(uint *)&curdst[4] = lilswap(dbits); + + if(blocksize > 8) { src -= 8; curdst -= 8; } + } + dst += stridey; + } +} + +static void reorientrgtc(GLenum format, int blocksize, int w, int h, uchar *src, uchar *dst, bool flipx, bool flipy, bool swapxy) +{ + int bx1 = 0, by1 = 0, bx2 = min(w, 4), by2 = min(h, 4), bw = (w+3)/4, bh = (h+3)/4, stridex = blocksize, stridey = blocksize; + if(swapxy) stridex *= bw; else stridey *= bh; + if(flipx) { dst += (bw-1)*stridex; stridex = -stridex; bx1 += 4-bx2; bx2 = 4; } + if(flipy) { dst += (bh-1)*stridey; stridey = -stridey; by1 += 4-by2; by2 = 4; } + stridex -= blocksize; + loopi(bh) + { + for(uchar *curdst = dst, *end = &src[bw*blocksize]; src < end; curdst += stridex) + { + loopj(blocksize/8) + { + uchar val1 = src[0], val2 = src[1]; + ullong sval = lilswap(*(const ushort *)&src[2]) + ((ullong)lilswap(*(const uint *)&src[4] )<< 16), dval = 0; + uint xmask = flipx ? 7 : 0, ymask = flipy ? 7 : 0, xshift = 0, yshift = 2; + if(swapxy) swap(xshift, yshift); + for(int y = by1; y < by2; y++) for(int x = bx1; x < bx2; x++) + { + dval |= ((sval&7) << (3*((xmask^x)<>= 3; + } + curdst[0] = val1; + curdst[1] = val2; + *(ushort *)&curdst[2] = lilswap(ushort(dval)); + *(ushort *)&curdst[4] = lilswap(ushort(dval>>16)); + *(ushort *)&curdst[6] = lilswap(ushort(dval>>32)); + src += 8; + curdst += 8; + } + } + dst += stridey; + } +} + +#define writetex(t, body) do \ + { \ + uchar *dstrow = t.data; \ + loop(y, t.h) \ + { \ + for(uchar *dst = dstrow, *end = &dstrow[t.w*t.bpp]; dst < end; dst += t.bpp) \ + { \ + body; \ + } \ + dstrow += t.pitch; \ + } \ + } while(0) + +#define readwritetex(t, s, body) do \ + { \ + uchar *dstrow = t.data, *srcrow = s.data; \ + loop(y, t.h) \ + { \ + for(uchar *dst = dstrow, *src = srcrow, *end = &srcrow[s.w*s.bpp]; src < end; dst += t.bpp, src += s.bpp) \ + { \ + body; \ + } \ + dstrow += t.pitch; \ + srcrow += s.pitch; \ + } \ + } while(0) + +#define read2writetex(t, s1, src1, s2, src2, body) do \ + { \ + uchar *dstrow = t.data, *src1row = s1.data, *src2row = s2.data; \ + loop(y, t.h) \ + { \ + for(uchar *dst = dstrow, *end = &dstrow[t.w*t.bpp], *src1 = src1row, *src2 = src2row; dst < end; dst += t.bpp, src1 += s1.bpp, src2 += s2.bpp) \ + { \ + body; \ + } \ + dstrow += t.pitch; \ + src1row += s1.pitch; \ + src2row += s2.pitch; \ + } \ + } while(0) + +#define readwritergbtex(t, s, body) \ + { \ + if(t.bpp >= 3) readwritetex(t, s, body); \ + else \ + { \ + ImageData rgb(t.w, t.h, 3); \ + read2writetex(rgb, t, orig, s, src, { dst[0] = dst[1] = dst[2] = orig[0]; body; }); \ + t.replace(rgb); \ + } \ + } + +void forcergbimage(ImageData &s) +{ + if(s.bpp >= 3) return; + ImageData d(s.w, s.h, 3); + readwritetex(d, s, { dst[0] = dst[1] = dst[2] = src[0]; }); + s.replace(d); +} + +#define readwritergbatex(t, s, body) \ + { \ + if(t.bpp >= 4) { readwritetex(t, s, body); } \ + else \ + { \ + ImageData rgba(t.w, t.h, 4); \ + if(t.bpp==3) read2writetex(rgba, t, orig, s, src, { dst[0] = orig[0]; dst[1] = orig[1]; dst[2] = orig[2]; body; }); \ + else read2writetex(rgba, t, orig, s, src, { dst[0] = dst[1] = dst[2] = orig[0]; body; }); \ + t.replace(rgba); \ + } \ + } + +void forcergbaimage(ImageData &s) +{ + if(s.bpp >= 4) return; + ImageData d(s.w, s.h, 4); + if(s.bpp==3) readwritetex(d, s, { dst[0] = src[0]; dst[1] = src[1]; dst[2] = src[2]; }); + else readwritetex(d, s, { dst[0] = dst[1] = dst[2] = src[0]; }); + s.replace(d); +} + +void swizzleimage(ImageData &s) +{ + if(s.bpp==2) + { + ImageData d(s.w, s.h, 4); + readwritetex(d, s, { dst[0] = dst[1] = dst[2] = src[0]; dst[3] = src[1]; }); + s.replace(d); + } + else if(s.bpp==1) + { + ImageData d(s.w, s.h, 3); + readwritetex(d, s, { dst[0] = dst[1] = dst[2] = src[0]; }); + s.replace(d); + } +} + +void texreorient(ImageData &s, bool flipx, bool flipy, bool swapxy, int type = TEX_DIFFUSE) +{ + ImageData d(swapxy ? s.h : s.w, swapxy ? s.w : s.h, s.bpp, s.levels, s.align, s.compressed); + switch(s.compressed) + { + case GL_COMPRESSED_RGB_S3TC_DXT1_EXT: + case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT: + case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT: + case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT: + { + uchar *dst = d.data, *src = s.data; + loopi(s.levels) + { + reorients3tc(s.compressed, s.bpp, max(s.w>>i, 1), max(s.h>>i, 1), src, dst, flipx, flipy, swapxy, type==TEX_NORMAL); + src += s.calclevelsize(i); + dst += d.calclevelsize(i); + } + break; + } + case GL_COMPRESSED_RED_RGTC1: + case GL_COMPRESSED_RG_RGTC2: + case GL_COMPRESSED_LUMINANCE_LATC1_EXT: + case GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT: + { + uchar *dst = d.data, *src = s.data; + loopi(s.levels) + { + reorientrgtc(s.compressed, s.bpp, max(s.w>>i, 1), max(s.h>>i, 1), src, dst, flipx, flipy, swapxy); + src += s.calclevelsize(i); + dst += d.calclevelsize(i); + } + break; + } + default: + if(type==TEX_NORMAL && s.bpp >= 3) reorientnormals(s.data, s.w, s.h, s.bpp, s.pitch, d.data, flipx, flipy, swapxy); + else reorienttexture(s.data, s.w, s.h, s.bpp, s.pitch, d.data, flipx, flipy, swapxy); + break; + } + s.replace(d); +} + +extern const texrotation texrotations[8] = +{ + { false, false, false }, // 0: default + { false, true, true }, // 1: 90 degrees + { true, true, false }, // 2: 180 degrees + { true, false, true }, // 3: 270 degrees + { true, false, false }, // 4: flip X + { false, true, false }, // 5: flip Y + { false, false, true }, // 6: transpose + { true, true, true }, // 7: flipped transpose +}; + +void texrotate(ImageData &s, int numrots, int type = TEX_DIFFUSE) +{ + if(numrots>=1 && numrots<=7) + { + const texrotation &r = texrotations[numrots]; + texreorient(s, r.flipx, r.flipy, r.swapxy, type); + } +} + +void texoffset(ImageData &s, int xoffset, int yoffset) +{ + xoffset = max(xoffset, 0); + xoffset %= s.w; + yoffset = max(yoffset, 0); + yoffset %= s.h; + if(!xoffset && !yoffset) return; + ImageData d(s.w, s.h, s.bpp); + uchar *src = s.data; + loop(y, s.h) + { + uchar *dst = (uchar *)d.data+((y+yoffset)%d.h)*d.pitch; + memcpy(dst+xoffset*s.bpp, src, (s.w-xoffset)*s.bpp); + memcpy(dst, src+(s.w-xoffset)*s.bpp, xoffset*s.bpp); + src += s.pitch; + } + s.replace(d); +} + +void texmad(ImageData &s, const vec &mul, const vec &add) +{ + if(s.bpp < 3 && (mul.x != mul.y || mul.y != mul.z || add.x != add.y || add.y != add.z)) + swizzleimage(s); + int maxk = min(int(s.bpp), 3); + writetex(s, + loopk(maxk) dst[k] = uchar(clamp(dst[k]*mul[k] + 255*add[k], 0.0f, 255.0f)); + ); +} + +void texcolorify(ImageData &s, const vec &color, vec weights) +{ + if(s.bpp < 3) return; + if(weights.iszero()) weights = vec(0.21f, 0.72f, 0.07f); + writetex(s, + float lum = dst[0]*weights.x + dst[1]*weights.y + dst[2]*weights.z; + loopk(3) dst[k] = uchar(clamp(lum*color[k], 0.0f, 255.0f)); + ); +} + +void texcolormask(ImageData &s, const vec &color1, const vec &color2) +{ + if(s.bpp < 4) return; + ImageData d(s.w, s.h, 3); + readwritetex(d, s, + vec color; + color.lerp(color2, color1, src[3]/255.0f); + loopk(3) dst[k] = uchar(clamp(color[k]*src[k], 0.0f, 255.0f)); + ); + s.replace(d); +} + +void texdup(ImageData &s, int srcchan, int dstchan) +{ + if(srcchan==dstchan || max(srcchan, dstchan) >= s.bpp) return; + writetex(s, dst[dstchan] = dst[srcchan]); +} + +void texmix(ImageData &s, int c1, int c2, int c3, int c4) +{ + int numchans = c1 < 0 ? 0 : (c2 < 0 ? 1 : (c3 < 0 ? 2 : (c4 < 0 ? 3 : 4))); + if(numchans <= 0) return; + ImageData d(s.w, s.h, numchans); + readwritetex(d, s, + switch(numchans) + { + case 4: dst[3] = src[c4]; + case 3: dst[2] = src[c3]; + case 2: dst[1] = src[c2]; + case 1: dst[0] = src[c1]; + } + ); + s.replace(d); +} + +void texgrey(ImageData &s) +{ + if(s.bpp <= 2) return; + ImageData d(s.w, s.h, s.bpp >= 4 ? 2 : 1); + if(s.bpp >= 4) + { + readwritetex(d, s, + dst[0] = src[0]; + dst[1] = src[3]; + ); + } + else + { + readwritetex(d, s, dst[0] = src[0]); + } + s.replace(d); +} + +void texpremul(ImageData &s) +{ + switch(s.bpp) + { + case 2: + writetex(s, + dst[0] = uchar((uint(dst[0])*uint(dst[1]))/255); + ); + break; + case 4: + writetex(s, + uint alpha = dst[3]; + dst[0] = uchar((uint(dst[0])*alpha)/255); + dst[1] = uchar((uint(dst[1])*alpha)/255); + dst[2] = uchar((uint(dst[2])*alpha)/255); + ); + break; + } +} + +void texagrad(ImageData &s, float x2, float y2, float x1, float y1) +{ + if(s.bpp != 2 && s.bpp != 4) return; + y1 = 1 - y1; + y2 = 1 - y2; + float minx = 1, miny = 1, maxx = 1, maxy = 1; + if(x1 != x2) + { + minx = (0 - x1) / (x2 - x1); + maxx = (1 - x1) / (x2 - x1); + } + if(y1 != y2) + { + miny = (0 - y1) / (y2 - y1); + maxy = (1 - y1) / (y2 - y1); + } + float dx = (maxx - minx)/max(s.w-1, 1), + dy = (maxy - miny)/max(s.h-1, 1), + cury = miny; + for(uchar *dstrow = s.data + s.bpp - 1, *endrow = dstrow + s.h*s.pitch; dstrow < endrow; dstrow += s.pitch) + { + float curx = minx; + for(uchar *dst = dstrow, *end = &dstrow[s.w*s.bpp]; dst < end; dst += s.bpp) + { + dst[0] = uchar(dst[0]*clamp(curx, 0.0f, 1.0f)*clamp(cury, 0.0f, 1.0f)); + curx += dx; + } + cury += dy; + } +} + +VAR(hwtexsize, 1, 0, 0); +VAR(hwcubetexsize, 1, 0, 0); +VAR(hwmaxaniso, 1, 0, 0); +VARFP(maxtexsize, 0, 0, 1<<12, initwarning("texture quality", INIT_LOAD)); +VARFP(reducefilter, 0, 1, 1, initwarning("texture quality", INIT_LOAD)); +VARFP(texreduce, 0, 0, 12, initwarning("texture quality", INIT_LOAD)); +VARFP(texcompress, 0, 1<<10, 1<<12, initwarning("texture quality", INIT_LOAD)); +VARFP(texcompressquality, -1, -1, 1, setuptexcompress()); +VARFP(trilinear, 0, 1, 1, initwarning("texture filtering", INIT_LOAD)); +VARFP(bilinear, 0, 1, 1, initwarning("texture filtering", INIT_LOAD)); +VARFP(aniso, 0, 0, 16, initwarning("texture filtering", INIT_LOAD)); + +extern int usetexcompress; + +void setuptexcompress() +{ + if(!usetexcompress) return; + + GLenum hint = GL_DONT_CARE; + switch(texcompressquality) + { + case 1: hint = GL_NICEST; break; + case 0: hint = GL_FASTEST; break; + } + glHint(GL_TEXTURE_COMPRESSION_HINT, hint); +} + +GLenum compressedformat(GLenum format, int w, int h, int force = 0) +{ + if(usetexcompress && texcompress && force >= 0 && (force || max(w, h) >= texcompress)) switch(format) + { + case GL_RGB5: + case GL_RGB8: + case GL_RGB: return usetexcompress > 1 ? GL_COMPRESSED_RGB_S3TC_DXT1_EXT : GL_COMPRESSED_RGB; + case GL_RGB5_A1: return usetexcompress > 1 ? GL_COMPRESSED_RGBA_S3TC_DXT1_EXT : GL_COMPRESSED_RGBA; + case GL_RGBA: return usetexcompress > 1 ? GL_COMPRESSED_RGBA_S3TC_DXT5_EXT : GL_COMPRESSED_RGBA; + case GL_RED: + case GL_R8: return hasRGTC ? (usetexcompress > 1 ? GL_COMPRESSED_RED_RGTC1 : GL_COMPRESSED_RED) : (usetexcompress > 1 ? GL_COMPRESSED_RGB_S3TC_DXT1_EXT : GL_COMPRESSED_RGB); + case GL_RG: + case GL_RG8: return hasRGTC ? (usetexcompress > 1 ? GL_COMPRESSED_RG_RGTC2 : GL_COMPRESSED_RG) : (usetexcompress > 1 ? GL_COMPRESSED_RGBA_S3TC_DXT5_EXT : GL_COMPRESSED_RGBA); + case GL_LUMINANCE: + case GL_LUMINANCE8: return hasLATC ? (usetexcompress > 1 ? GL_COMPRESSED_LUMINANCE_LATC1_EXT : GL_COMPRESSED_LUMINANCE) : (usetexcompress > 1 ? GL_COMPRESSED_RGB_S3TC_DXT1_EXT : GL_COMPRESSED_RGB); + case GL_LUMINANCE_ALPHA: + case GL_LUMINANCE8_ALPHA8: return hasLATC ? (usetexcompress > 1 ? GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT : GL_COMPRESSED_LUMINANCE_ALPHA) : (usetexcompress > 1 ? GL_COMPRESSED_RGBA_S3TC_DXT5_EXT : GL_COMPRESSED_RGBA); + } + return format; +} + +GLenum uncompressedformat(GLenum format) +{ + switch(format) + { + case GL_COMPRESSED_ALPHA: + return GL_ALPHA; + case GL_COMPRESSED_LUMINANCE: + case GL_COMPRESSED_LUMINANCE_LATC1_EXT: + return GL_LUMINANCE; + case GL_COMPRESSED_LUMINANCE_ALPHA: + case GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT: + return GL_LUMINANCE_ALPHA; + case GL_COMPRESSED_RED: + case GL_COMPRESSED_RED_RGTC1: + return GL_RED; + case GL_COMPRESSED_RG: + case GL_COMPRESSED_RG_RGTC2: + return GL_RG; + case GL_COMPRESSED_RGB: + case GL_COMPRESSED_RGB_S3TC_DXT1_EXT: + return GL_RGB; + case GL_COMPRESSED_RGBA: + case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT: + case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT: + case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT: + return GL_RGBA; + } + return GL_FALSE; +} + +int formatsize(GLenum format) +{ + switch(format) + { + case GL_RED: + case GL_LUMINANCE: + case GL_ALPHA: return 1; + case GL_RG: + case GL_LUMINANCE_ALPHA: return 2; + case GL_RGB: return 3; + case GL_RGBA: return 4; + default: return 4; + } +} + +VARFP(usenp2, 0, 0, 1, initwarning("texture quality", INIT_LOAD)); + +void resizetexture(int w, int h, bool mipmap, bool canreduce, GLenum target, int compress, int &tw, int &th) +{ + int hwlimit = target==GL_TEXTURE_CUBE_MAP ? hwcubetexsize : hwtexsize, + sizelimit = mipmap && maxtexsize ? min(maxtexsize, hwlimit) : hwlimit; + if(compress > 0 && !usetexcompress) + { + w = max(w/compress, 1); + h = max(h/compress, 1); + } + if(canreduce && texreduce) + { + w = max(w>>texreduce, 1); + h = max(h>>texreduce, 1); + } + w = min(w, sizelimit); + h = min(h, sizelimit); + if(!usenp2 && (w&(w-1) || h&(h-1))) + { + tw = th = 1; + while(tw < w) tw *= 2; + while(th < h) th *= 2; + if(w < tw - tw/4) tw /= 2; + if(h < th - th/4) th /= 2; + } + else + { + tw = w; + th = h; + } +} + +static GLuint mipmapfbo[2] = { 0, 0 }; + +void cleanupmipmaps() +{ + if(mipmapfbo[0]) { glDeleteFramebuffers_(2, mipmapfbo); memset(mipmapfbo, 0, sizeof(mipmapfbo)); } +} + +VARFP(gpumipmap, 0, 0, 1, cleanupmipmaps()); + +void uploadtexture(int tnum, GLenum target, GLenum internal, int tw, int th, GLenum format, GLenum type, void *pixels, int pw, int ph, int pitch, bool mipmap) +{ + int bpp = formatsize(format), row = 0, rowalign = 0; + if(!pitch) pitch = pw*bpp; + uchar *buf = NULL; + if(pw!=tw || ph!=th) + { + buf = new uchar[tw*th*bpp]; + scaletexture((uchar *)pixels, pw, ph, bpp, pitch, buf, tw, th); + } + else if(tw*bpp != pitch) + { + row = pitch/bpp; + rowalign = texalign(pixels, pitch, 1); + while(rowalign > 0 && ((row*bpp + rowalign - 1)/rowalign)*rowalign != pitch) rowalign >>= 1; + if(!rowalign) + { + row = 0; + buf = new uchar[tw*th*bpp]; + loopi(th) memcpy(&buf[i*tw*bpp], &((uchar *)pixels)[i*pitch], tw*bpp); + } + } + bool shouldgpumipmap = pixels && mipmap && max(tw, th) > 1 && gpumipmap && hasFBB && !uncompressedformat(internal); + for(int level = 0, align = 0, mw = tw, mh = th;; level++) + { + uchar *src = buf ? buf : (uchar *)pixels; + if(buf) pitch = mw*bpp; + int srcalign = row > 0 ? rowalign : texalign(src, pitch, 1); + if(align != srcalign) glPixelStorei(GL_UNPACK_ALIGNMENT, align = srcalign); + if(row > 0) glPixelStorei(GL_UNPACK_ROW_LENGTH, row); + glTexImage2D(target, level, internal, mw, mh, 0, format, type, src); + if(row > 0) glPixelStorei(GL_UNPACK_ROW_LENGTH, row = 0); + if(!mipmap || shouldgpumipmap || max(mw, mh) <= 1) break; + int srcw = mw, srch = mh; + if(mw > 1) mw /= 2; + if(mh > 1) mh /= 2; + if(src) + { + if(!buf) buf = new uchar[mw*mh*bpp]; + scaletexture(src, srcw, srch, bpp, pitch, buf, mw, mh); + } + } + if(buf) delete[] buf; + if(shouldgpumipmap) + { + GLint fbo = 0; + if(!inbetweenframes || drawtex) glGetIntegerv(GL_FRAMEBUFFER_BINDING, &fbo); + for(int level = 1, mw = tw, mh = th; max(mw, mh) > 1; level++) + { + if(mw > 1) mw /= 2; + if(mh > 1) mh /= 2; + glTexImage2D(target, level, internal, mw, mh, 0, format, type, NULL); + } + if(!mipmapfbo[0]) glGenFramebuffers_(2, mipmapfbo); + glBindFramebuffer_(GL_READ_FRAMEBUFFER, mipmapfbo[0]); + glBindFramebuffer_(GL_DRAW_FRAMEBUFFER, mipmapfbo[1]); + for(int level = 1, mw = tw, mh = th; max(mw, mh) > 1; level++) + { + int srcw = mw, srch = mh; + if(mw > 1) mw /= 2; + if(mh > 1) mh /= 2; + glFramebufferTexture2D_(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, tnum, level - 1); + glFramebufferTexture2D_(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, tnum, level); + glBlitFramebuffer_(0, 0, srcw, srch, 0, 0, mw, mh, GL_COLOR_BUFFER_BIT, GL_LINEAR); + } + glFramebufferTexture2D_(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, 0, 0); + glFramebufferTexture2D_(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, 0, 0); + glBindFramebuffer_(GL_FRAMEBUFFER, fbo); + } +} + +void uploadcompressedtexture(GLenum target, GLenum subtarget, GLenum format, int w, int h, uchar *data, int align, int blocksize, int levels, bool mipmap) +{ + int hwlimit = target==GL_TEXTURE_CUBE_MAP ? hwcubetexsize : hwtexsize, + sizelimit = levels > 1 && maxtexsize ? min(maxtexsize, hwlimit) : hwlimit; + int level = 0; + loopi(levels) + { + int size = ((w + align-1)/align) * ((h + align-1)/align) * blocksize; + if(w <= sizelimit && h <= sizelimit) + { + glCompressedTexImage2D_(subtarget, level, format, w, h, 0, size, data); + level++; + if(!mipmap) break; + } + if(max(w, h) <= 1) break; + if(w > 1) w /= 2; + if(h > 1) h /= 2; + data += size; + } +} + +GLenum textarget(GLenum subtarget) +{ + switch(subtarget) + { + case GL_TEXTURE_CUBE_MAP_POSITIVE_X: + case GL_TEXTURE_CUBE_MAP_NEGATIVE_X: + case GL_TEXTURE_CUBE_MAP_POSITIVE_Y: + case GL_TEXTURE_CUBE_MAP_NEGATIVE_Y: + case GL_TEXTURE_CUBE_MAP_POSITIVE_Z: + case GL_TEXTURE_CUBE_MAP_NEGATIVE_Z: + return GL_TEXTURE_CUBE_MAP; + } + return subtarget; +} + +const GLint *swizzlemask(GLenum format) +{ + static const GLint luminance[4] = { GL_RED, GL_RED, GL_RED, GL_ONE }; + static const GLint luminancealpha[4] = { GL_RED, GL_RED, GL_RED, GL_GREEN }; + switch(format) + { + case GL_RED: return luminance; + case GL_RG: return luminancealpha; + } + return NULL; +} + +void setuptexparameters(int tnum, void *pixels, int clamp, int filter, GLenum format, GLenum target, bool swizzle) +{ + glBindTexture(target, tnum); + glTexParameteri(target, GL_TEXTURE_WRAP_S, clamp&1 ? GL_CLAMP_TO_EDGE : (clamp&0x100 ? GL_MIRRORED_REPEAT : GL_REPEAT)); + glTexParameteri(target, GL_TEXTURE_WRAP_T, clamp&2 ? GL_CLAMP_TO_EDGE : (clamp&0x200 ? GL_MIRRORED_REPEAT : GL_REPEAT)); + if(target==GL_TEXTURE_2D && hasAF && min(aniso, hwmaxaniso) > 0 && filter > 1) glTexParameteri(target, GL_TEXTURE_MAX_ANISOTROPY_EXT, min(aniso, hwmaxaniso)); + glTexParameteri(target, GL_TEXTURE_MAG_FILTER, filter && bilinear ? GL_LINEAR : GL_NEAREST); + glTexParameteri(target, GL_TEXTURE_MIN_FILTER, + filter > 1 ? + (trilinear ? + (bilinear ? GL_LINEAR_MIPMAP_LINEAR : GL_NEAREST_MIPMAP_LINEAR) : + (bilinear ? GL_LINEAR_MIPMAP_NEAREST : GL_NEAREST_MIPMAP_NEAREST)) : + (filter && bilinear ? GL_LINEAR : GL_NEAREST)); + if(swizzle && hasTRG && hasTSW) + { + const GLint *mask = swizzlemask(format); + if(mask) glTexParameteriv(target, GL_TEXTURE_SWIZZLE_RGBA, mask); + } +} + +static GLenum textype(GLenum &component, GLenum &format) +{ + GLenum type = GL_UNSIGNED_BYTE; + switch(component) + { + case GL_R16F: + case GL_R32F: + if(!format) format = GL_RED; + type = GL_FLOAT; + break; + + case GL_RG16F: + case GL_RG32F: + if(!format) format = GL_RG; + type = GL_FLOAT; + break; + + case GL_RGB16F: + case GL_RGB32F: + if(!format) format = GL_RGB; + type = GL_FLOAT; + break; + + case GL_RGBA16F: + case GL_RGBA32F: + if(!format) format = GL_RGBA; + type = GL_FLOAT; + break; + + case GL_DEPTH_COMPONENT16: + case GL_DEPTH_COMPONENT24: + case GL_DEPTH_COMPONENT32: + if(!format) format = GL_DEPTH_COMPONENT; + break; + + case GL_RGB5: + case GL_RGB8: + case GL_RGB10: + case GL_RGB16: + case GL_COMPRESSED_RGB: + case GL_COMPRESSED_RGB_S3TC_DXT1_EXT: + if(!format) format = GL_RGB; + break; + + case GL_RGB5_A1: + case GL_RGBA8: + case GL_RGB10_A2: + case GL_RGBA16: + case GL_COMPRESSED_RGBA: + case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT: + case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT: + case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT: + if(!format) format = GL_RGBA; + break; + + case GL_DEPTH_STENCIL: + case GL_DEPTH24_STENCIL8: + if(!format) format = GL_DEPTH_STENCIL; + type = GL_UNSIGNED_INT_24_8; + break; + + case GL_R8: + case GL_R16: + case GL_COMPRESSED_RED: + case GL_COMPRESSED_RED_RGTC1: + if(!format) format = GL_RED; + break; + + case GL_RG8: + case GL_RG16: + case GL_COMPRESSED_RG: + case GL_COMPRESSED_RG_RGTC2: + if(!format) format = GL_RG; + break; + + case GL_LUMINANCE8: + case GL_LUMINANCE16: + case GL_COMPRESSED_LUMINANCE: + case GL_COMPRESSED_LUMINANCE_LATC1_EXT: + if(!format) format = GL_LUMINANCE; + break; + + case GL_LUMINANCE8_ALPHA8: + case GL_LUMINANCE16_ALPHA16: + case GL_COMPRESSED_LUMINANCE_ALPHA: + case GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT: + if(!format) format = GL_LUMINANCE_ALPHA; + break; + + case GL_ALPHA8: + case GL_ALPHA16: + case GL_COMPRESSED_ALPHA: + if(!format) format = GL_ALPHA; + break; + } + if(!format) format = component; + return type; +} + +void createtexture(int tnum, int w, int h, void *pixels, int clamp, int filter, GLenum component, GLenum subtarget, int pw, int ph, int pitch, bool resize, GLenum format, bool swizzle) +{ + GLenum target = textarget(subtarget), type = textype(component, format); + if(filter >= 0 && clamp >= 0) setuptexparameters(tnum, pixels, clamp, filter, format, target, swizzle); + if(!pw) pw = w; + if(!ph) ph = h; + int tw = w, th = h; + bool mipmap = filter > 1 && pixels; + if(resize && pixels) + { + resizetexture(w, h, mipmap, false, target, 0, tw, th); + if(mipmap) component = compressedformat(component, tw, th); + } + uploadtexture(tnum, subtarget, component, tw, th, format, type, pixels, pw, ph, pitch, mipmap); +} + +void createcompressedtexture(int tnum, int w, int h, uchar *data, int align, int blocksize, int levels, int clamp, int filter, GLenum format, GLenum subtarget, bool swizzle = false) +{ + GLenum target = textarget(subtarget); + if(filter >= 0 && clamp >= 0) setuptexparameters(tnum, data, clamp, filter, format, target); + uploadcompressedtexture(target, subtarget, format, w, h, data, align, blocksize, levels, filter > 1); +} + +hashnameset textures; + +Texture *notexture = NULL; // used as default, ensured to be loaded + +static GLenum texformat(int bpp, bool swizzle = false) +{ + switch(bpp) + { + case 1: return hasTRG && (hasTSW || !glcompat || !swizzle) ? GL_RED : GL_LUMINANCE; + case 2: return hasTRG && (hasTSW || !glcompat || !swizzle) ? GL_RG : GL_LUMINANCE_ALPHA; + case 3: return GL_RGB; + case 4: return GL_RGBA; + default: return 0; + } +} + +static bool alphaformat(GLenum format) +{ + switch(format) + { + case GL_ALPHA: + case GL_LUMINANCE_ALPHA: + case GL_RG: + case GL_RGBA: + return true; + default: + return false; + } +} + +int texalign(const void *data, int w, int bpp) +{ + int stride = w*bpp; + if(stride&1) return 1; + if(stride&2) return 2; + return 4; +} + +static Texture *newtexture(Texture *t, const char *rname, ImageData &s, int clamp = 0, bool mipit = true, bool canreduce = false, bool transient = false, int compress = 0) +{ + if(!t) + { + char *key = newstring(rname); + t = &textures[key]; + t->name = key; + } + + t->clamp = clamp; + t->mipmap = mipit; + t->type = Texture::IMAGE; + if(transient) t->type |= Texture::TRANSIENT; + if(clamp&0x300) t->type |= Texture::MIRROR; + if(!s.data) + { + t->type |= Texture::STUB; + t->w = t->h = t->xs = t->ys = t->bpp = 0; + return t; + } + + bool swizzle = !(clamp&0x10000); + GLenum format; + if(s.compressed) + { + format = uncompressedformat(s.compressed); + t->bpp = formatsize(format); + t->type |= Texture::COMPRESSED; + } + else + { + format = texformat(s.bpp, swizzle); + t->bpp = s.bpp; + if(swizzle && hasTRG && !hasTSW && swizzlemask(format)) + { + swizzleimage(s); + format = texformat(s.bpp, swizzle); + t->bpp = s.bpp; + } + } + if(alphaformat(format)) t->type |= Texture::ALPHA; + t->w = t->xs = s.w; + t->h = t->ys = s.h; + + int filter = !canreduce || reducefilter ? (mipit ? 2 : 1) : 0; + glGenTextures(1, &t->id); + if(s.compressed) + { + uchar *data = s.data; + int levels = s.levels, level = 0; + if(canreduce && texreduce) loopi(min(texreduce, s.levels-1)) + { + data += s.calclevelsize(level++); + levels--; + if(t->w > 1) t->w /= 2; + if(t->h > 1) t->h /= 2; + } + int sizelimit = mipit && maxtexsize ? min(maxtexsize, hwtexsize) : hwtexsize; + while(t->w > sizelimit || t->h > sizelimit) + { + data += s.calclevelsize(level++); + levels--; + if(t->w > 1) t->w /= 2; + if(t->h > 1) t->h /= 2; + } + createcompressedtexture(t->id, t->w, t->h, data, s.align, s.bpp, levels, clamp, filter, s.compressed, GL_TEXTURE_2D, swizzle); + } + else + { + resizetexture(t->w, t->h, mipit, canreduce, GL_TEXTURE_2D, compress, t->w, t->h); + GLenum component = compressedformat(format, t->w, t->h, compress); + createtexture(t->id, t->w, t->h, s.data, clamp, filter, component, GL_TEXTURE_2D, t->xs, t->ys, s.pitch, false, format, swizzle); + } + return t; +} + +#if SDL_BYTEORDER == SDL_BIG_ENDIAN +#define RGBAMASKS 0xff000000, 0x00ff0000, 0x0000ff00, 0x000000ff +#define RGBMASKS 0xff0000, 0x00ff00, 0x0000ff, 0 +#else +#define RGBAMASKS 0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000 +#define RGBMASKS 0x0000ff, 0x00ff00, 0xff0000, 0 +#endif + +SDL_Surface *wrapsurface(void *data, int width, int height, int bpp) +{ + switch(bpp) + { + case 3: return SDL_CreateRGBSurfaceFrom(data, width, height, 8*bpp, bpp*width, RGBMASKS); + case 4: return SDL_CreateRGBSurfaceFrom(data, width, height, 8*bpp, bpp*width, RGBAMASKS); + } + return NULL; +} + +SDL_Surface *creatergbsurface(SDL_Surface *os) +{ + SDL_Surface *ns = SDL_CreateRGBSurface(SDL_SWSURFACE, os->w, os->h, 24, RGBMASKS); + if(ns) SDL_BlitSurface(os, NULL, ns, NULL); + SDL_FreeSurface(os); + return ns; +} + +SDL_Surface *creatergbasurface(SDL_Surface *os) +{ + SDL_Surface *ns = SDL_CreateRGBSurface(SDL_SWSURFACE, os->w, os->h, 32, RGBAMASKS); + if(ns) + { + SDL_SetSurfaceBlendMode(os, SDL_BLENDMODE_NONE); + SDL_BlitSurface(os, NULL, ns, NULL); + } + SDL_FreeSurface(os); + return ns; +} + +bool checkgrayscale(SDL_Surface *s) +{ + // gray scale images have 256 levels, no colorkey, and the palette is a ramp + if(s->format->palette) + { + if(s->format->palette->ncolors != 256 || SDL_GetColorKey(s, NULL) >= 0) return false; + const SDL_Color *colors = s->format->palette->colors; + loopi(256) if(colors[i].r != i || colors[i].g != i || colors[i].b != i) return false; + } + return true; +} + +SDL_Surface *fixsurfaceformat(SDL_Surface *s) +{ + if(!s) return NULL; + if(!s->pixels || min(s->w, s->h) <= 0 || s->format->BytesPerPixel <= 0) + { + SDL_FreeSurface(s); + return NULL; + } + static const uint rgbmasks[] = { RGBMASKS }, rgbamasks[] = { RGBAMASKS }; + switch(s->format->BytesPerPixel) + { + case 1: + if(!checkgrayscale(s)) return SDL_GetColorKey(s, NULL) >= 0 ? creatergbasurface(s) : creatergbsurface(s); + break; + case 3: + if(s->format->Rmask != rgbmasks[0] || s->format->Gmask != rgbmasks[1] || s->format->Bmask != rgbmasks[2]) + return creatergbsurface(s); + break; + case 4: + if(s->format->Rmask != rgbamasks[0] || s->format->Gmask != rgbamasks[1] || s->format->Bmask != rgbamasks[2] || s->format->Amask != rgbamasks[3]) + return s->format->Amask ? creatergbasurface(s) : creatergbsurface(s); + break; + } + return s; +} + +void texflip(ImageData &s) +{ + ImageData d(s.w, s.h, s.bpp); + uchar *dst = d.data, *src = &s.data[s.pitch*s.h]; + loopi(s.h) + { + src -= s.pitch; + memcpy(dst, src, s.bpp*s.w); + dst += d.pitch; + } + s.replace(d); +} + +void texnormal(ImageData &s, int emphasis) +{ + ImageData d(s.w, s.h, 3); + uchar *src = s.data, *dst = d.data; + loop(y, s.h) loop(x, s.w) + { + vec normal(0.0f, 0.0f, 255.0f/emphasis); + normal.x += src[y*s.pitch + ((x+s.w-1)%s.w)*s.bpp]; + normal.x -= src[y*s.pitch + ((x+1)%s.w)*s.bpp]; + normal.y += src[((y+s.h-1)%s.h)*s.pitch + x*s.bpp]; + normal.y -= src[((y+1)%s.h)*s.pitch + x*s.bpp]; + normal.normalize(); + *dst++ = uchar(127.5f + normal.x*127.5f); + *dst++ = uchar(127.5f + normal.y*127.5f); + *dst++ = uchar(127.5f + normal.z*127.5f); + } + s.replace(d); +} + +template +static void blurtexture(int w, int h, uchar *dst, const uchar *src, int margin) +{ + static const int weights3x3[9] = + { + 0x10, 0x20, 0x10, + 0x20, 0x40, 0x20, + 0x10, 0x20, 0x10 + }; + static const int weights5x5[25] = + { + 0x05, 0x05, 0x09, 0x05, 0x05, + 0x05, 0x0A, 0x14, 0x0A, 0x05, + 0x09, 0x14, 0x28, 0x14, 0x09, + 0x05, 0x0A, 0x14, 0x0A, 0x05, + 0x05, 0x05, 0x09, 0x05, 0x05 + }; + const int *mat = n > 1 ? weights5x5 : weights3x3; + int mstride = 2*n + 1, + mstartoffset = n*(mstride + 1), + stride = bpp*w, + startoffset = n*bpp, + nextoffset1 = stride + mstride*bpp, + nextoffset2 = stride - mstride*bpp; + src += margin*(stride + bpp); + for(int y = margin; y < h-margin; y++) + { + for(int x = margin; x < w-margin; x++) + { + int dr = 0, dg = 0, db = 0; + const uchar *p = src - startoffset; + const int *m = mat + mstartoffset; + for(int t = y; t >= y-n; t--, p -= nextoffset1, m -= mstride) + { + if(t < 0) p += stride; + int a = 0; + if(n > 1) { a += m[-2]; if(x >= 2) { dr += p[0] * a; dg += p[1] * a; db += p[2] * a; a = 0; } p += bpp; } + a += m[-1]; if(x >= 1) { dr += p[0] * a; dg += p[1] * a; db += p[2] * a; a = 0; } p += bpp; + int cr = p[0], cg = p[1], cb = p[2]; a += m[0]; dr += cr * a; dg += cg * a; db += cb * a; p += bpp; + if(x+1 < w) { cr = p[0]; cg = p[1]; cb = p[2]; } dr += cr * m[1]; dg += cg * m[1]; db += cb * m[1]; p += bpp; + if(n > 1) { if(x+2 < w) { cr = p[0]; cg = p[1]; cb = p[2]; } dr += cr * m[2]; dg += cg * m[2]; db += cb * m[2]; p += bpp; } + } + p = src - startoffset + stride; + m = mat + mstartoffset + mstride; + for(int t = y+1; t <= y+n; t++, p += nextoffset2, m += mstride) + { + if(t >= h) p -= stride; + int a = 0; + if(n > 1) { a += m[-2]; if(x >= 2) { dr += p[0] * a; dg += p[1] * a; db += p[2] * a; a = 0; } p += bpp; } + a += m[-1]; if(x >= 1) { dr += p[0] * a; dg += p[1] * a; db += p[2] * a; a = 0; } p += bpp; + int cr = p[0], cg = p[1], cb = p[2]; a += m[0]; dr += cr * a; dg += cg * a; db += cb * a; p += bpp; + if(x+1 < w) { cr = p[0]; cg = p[1]; cb = p[2]; } dr += cr * m[1]; dg += cg * m[1]; db += cb * m[1]; p += bpp; + if(n > 1) { if(x+2 < w) { cr = p[0]; cg = p[1]; cb = p[2]; } dr += cr * m[2]; dg += cg * m[2]; db += cb * m[2]; p += bpp; } + } + if(normals) + { + vec v(dr-0x7F80, dg-0x7F80, db-0x7F80); + float mag = 127.5f/v.magnitude(); + dst[0] = uchar(v.x*mag + 127.5f); + dst[1] = uchar(v.y*mag + 127.5f); + dst[2] = uchar(v.z*mag + 127.5f); + } + else + { + dst[0] = dr>>8; + dst[1] = dg>>8; + dst[2] = db>>8; + } + if(bpp > 3) dst[3] = src[3]; + dst += bpp; + src += bpp; + } + src += 2*margin*bpp; + } +} + +void blurtexture(int n, int bpp, int w, int h, uchar *dst, const uchar *src, int margin) +{ + switch((clamp(n, 1, 2)<<4) | bpp) + { + case 0x13: blurtexture<1, 3, false>(w, h, dst, src, margin); break; + case 0x23: blurtexture<2, 3, false>(w, h, dst, src, margin); break; + case 0x14: blurtexture<1, 4, false>(w, h, dst, src, margin); break; + case 0x24: blurtexture<2, 4, false>(w, h, dst, src, margin); break; + } +} + +void blurnormals(int n, int w, int h, bvec *dst, const bvec *src, int margin) +{ + switch(clamp(n, 1, 2)) + { + case 1: blurtexture<1, 3, true>(w, h, dst->v, src->v, margin); break; + case 2: blurtexture<2, 3, true>(w, h, dst->v, src->v, margin); break; + } +} + +void texblur(ImageData &s, int n, int r) +{ + if(s.bpp < 3) return; + loopi(r) + { + ImageData d(s.w, s.h, s.bpp); + blurtexture(n, s.bpp, s.w, s.h, d.data, s.data); + s.replace(d); + } +} + +void scaleimage(ImageData &s, int w, int h) +{ + ImageData d(w, h, s.bpp); + scaletexture(s.data, s.w, s.h, s.bpp, s.pitch, d.data, w, h); + s.replace(d); +} + +bool canloadsurface(const char *name) +{ + stream *f = openfile(name, "rb"); + if(!f) return false; + delete f; + return true; +} + +SDL_Surface *loadsurface(const char *name) +{ + SDL_Surface *s = NULL; + stream *z = openzipfile(name, "rb"); + if(z) + { + SDL_RWops *rw = z->rwops(); + if(rw) + { + char *ext = (char *)strrchr(name, '.'); + if(ext) ++ext; + s = IMG_LoadTyped_RW(rw, 0, ext); + SDL_FreeRW(rw); + } + delete z; + } + if(!s) s = IMG_Load(findfile(name, "rb")); + return fixsurfaceformat(s); +} + +static vec parsevec(const char *arg) +{ + vec v(0, 0, 0); + int i = 0; + for(; arg[0] && (!i || arg[0]=='/') && i<3; arg += strcspn(arg, "/,><"), i++) + { + if(i) arg++; + v[i] = atof(arg); + } + if(i==1) v.y = v.z = v.x; + return v; +} + +VAR(usedds, 0, 1, 1); +VAR(dbgdds, 0, 0, 1); +VAR(scaledds, 0, 2, 4); + +static bool texturedata(ImageData &d, const char *tname, Slot::Tex *tex = NULL, bool msg = true, int *compress = NULL, int *wrap = NULL) +{ + const char *cmds = NULL, *file = tname; + + if(!tname) + { + if(!tex) return false; + if(tex->name[0]=='<') + { + cmds = tex->name; + file = strrchr(tex->name, '>'); + if(!file) { if(msg) conoutf(CON_ERROR, "could not load texture packages/%s", tex->name); return false; } + file++; + } + else file = tex->name; + + static string pname; + formatstring(pname, "packages/%s", file); + file = path(pname); + } + else if(tname[0]=='<') + { + cmds = tname; + file = strrchr(tname, '>'); + if(!file) { if(msg) conoutf(CON_ERROR, "could not load texture %s", tname); return false; } + file++; + } + + int flen = strlen(file); + bool raw = !usedds || !compress, dds = false, guess = false; + for(const char *pcmds = cmds; pcmds;) + { + #define PARSETEXCOMMANDS(cmds) \ + const char *cmd = NULL, *end = NULL, *arg[4] = { NULL, NULL, NULL, NULL }; \ + cmd = &cmds[1]; \ + end = strchr(cmd, '>'); \ + if(!end) break; \ + cmds = strchr(cmd, '<'); \ + size_t len = strcspn(cmd, ":,><"); \ + loopi(4) \ + { \ + arg[i] = strchr(i ? arg[i-1] : cmd, i ? ',' : ':'); \ + if(!arg[i] || arg[i] >= end) arg[i] = ""; \ + else arg[i]++; \ + } + PARSETEXCOMMANDS(pcmds); + if(matchstring(cmd, len, "dds")) dds = true; + else if(matchstring(cmd, len, "thumbnail")) + { + raw = true; + guess = flen >= 4 && !strchr(file+flen-4, '.'); + } + else if(matchstring(cmd, len, "stub")) return canloadsurface(file); + } + + if(msg) renderprogress(loadprogress, file); + + if(flen >= 4 && (!strcasecmp(file + flen - 4, ".dds") || (dds && !raw))) + { + string dfile; + copystring(dfile, file); + memcpy(dfile + flen - 4, ".dds", 4); + if(!loaddds(dfile, d, raw ? 1 : (dds ? 0 : -1)) && (!dds || raw)) + { + if(msg) conoutf(CON_ERROR, "could not load texture %s", dfile); + return false; + } + if(d.data && !d.compressed && !dds && compress) *compress = scaledds; + } + + if(!d.data) + { + SDL_Surface *s = NULL; + if(guess) + { + static const char *exts[] = {".jpg", ".png"}; + string ext; + loopi(sizeof(exts)/sizeof(exts[0])) + { + copystring(ext, file); + concatstring(ext, exts[i]); + s = loadsurface(ext); + if(s) break; + } + } + else s = loadsurface(file); + if(!s) { if(msg) conoutf(CON_ERROR, "could not load texture %s", file); return false; } + int bpp = s->format->BitsPerPixel; + if(bpp%8 || !texformat(bpp/8)) { SDL_FreeSurface(s); conoutf(CON_ERROR, "texture must be 8, 16, 24, or 32 bpp: %s", file); return false; } + if(max(s->w, s->h) > (1<<12)) { SDL_FreeSurface(s); conoutf(CON_ERROR, "texture size exceeded %dx%d pixels: %s", 1<<12, 1<<12, file); return false; } + d.wrap(s); + } + + while(cmds) + { + PARSETEXCOMMANDS(cmds); + if(d.compressed) goto compressed; + if(matchstring(cmd, len, "mad")) texmad(d, parsevec(arg[0]), parsevec(arg[1])); + else if(matchstring(cmd, len, "colorify")) texcolorify(d, parsevec(arg[0]), parsevec(arg[1])); + else if(matchstring(cmd, len, "colormask")) texcolormask(d, parsevec(arg[0]), *arg[1] ? parsevec(arg[1]) : vec(1, 1, 1)); + else if(matchstring(cmd, len, "normal")) + { + int emphasis = atoi(arg[0]); + texnormal(d, emphasis > 0 ? emphasis : 3); + } + else if(matchstring(cmd, len, "dup")) texdup(d, atoi(arg[0]), atoi(arg[1])); + else if(matchstring(cmd, len, "offset")) texoffset(d, atoi(arg[0]), atoi(arg[1])); + else if(matchstring(cmd, len, "rotate")) texrotate(d, atoi(arg[0]), tex ? tex->type : 0); + else if(matchstring(cmd, len, "reorient")) texreorient(d, atoi(arg[0])>0, atoi(arg[1])>0, atoi(arg[2])>0, tex ? tex->type : TEX_DIFFUSE); + else if(matchstring(cmd, len, "mix")) texmix(d, *arg[0] ? atoi(arg[0]) : -1, *arg[1] ? atoi(arg[1]) : -1, *arg[2] ? atoi(arg[2]) : -1, *arg[3] ? atoi(arg[3]) : -1); + else if(matchstring(cmd, len, "grey")) texgrey(d); + else if(matchstring(cmd, len, "blur")) + { + int emphasis = atoi(arg[0]), repeat = atoi(arg[1]); + texblur(d, emphasis > 0 ? clamp(emphasis, 1, 2) : 1, repeat > 0 ? repeat : 1); + } + else if(matchstring(cmd, len, "premul")) texpremul(d); + else if(matchstring(cmd, len, "agrad")) texagrad(d, atof(arg[0]), atof(arg[1]), atof(arg[2]), atof(arg[3])); + else if(matchstring(cmd, len, "compress") || matchstring(cmd, len, "dds")) + { + int scale = atoi(arg[0]); + if(scale <= 0) scale = scaledds; + if(compress) *compress = scale; + } + else if(matchstring(cmd, len, "nocompress")) + { + if(compress) *compress = -1; + } + else if(matchstring(cmd, len, "thumbnail")) + { + int w = atoi(arg[0]), h = atoi(arg[1]); + if(w <= 0 || w > (1<<12)) w = 64; + if(h <= 0 || h > (1<<12)) h = w; + if(d.w > w || d.h > h) scaleimage(d, w, h); + } + else + compressed: + if(matchstring(cmd, len, "mirror")) + { + if(wrap) *wrap |= 0x300; + } + else if(matchstring(cmd, len, "noswizzle")) + { + if(wrap) *wrap |= 0x10000; + } + } + + return true; +} + +uchar *loadalphamask(Texture *t) +{ + if(t->alphamask) return t->alphamask; + if(!(t->type&Texture::ALPHA)) return NULL; + ImageData s; + if(!texturedata(s, t->name, NULL, false) || !s.data || s.compressed) return NULL; + t->alphamask = new uchar[s.h * ((s.w+7)/8)]; + uchar *srcrow = s.data, *dst = t->alphamask-1; + loop(y, s.h) + { + uchar *src = srcrow+s.bpp-1; + loop(x, s.w) + { + int offset = x%8; + if(!offset) *++dst = 0; + if(*src) *dst |= 1<alphamask; +} + +Texture *textureload(const char *name, int clamp, bool mipit, bool msg) +{ + string tname; + copystring(tname, name); + Texture *t = textures.access(path(tname)); + if(t) return t; + int compress = 0; + ImageData s; + if(texturedata(s, tname, NULL, msg, &compress, &clamp)) return newtexture(NULL, tname, s, clamp, mipit, false, false, compress); + return notexture; +} + +bool settexture(const char *name, int clamp) +{ + Texture *t = textureload(name, clamp, true, false); + glBindTexture(GL_TEXTURE_2D, t->id); + return t != notexture; +} + +vector vslots; +vector slots; +MSlot materialslots[(MATF_VOLUME|MATF_INDEX)+1]; +Slot dummyslot; +VSlot dummyvslot(&dummyslot); + +void texturereset(int *n) +{ + if(!(identflags&IDF_OVERRIDDEN) && !game::allowedittoggle()) return; + resetslotshader(); + int limit = clamp(*n, 0, slots.length()); + for(int i = limit; i < slots.length(); i++) + { + Slot *s = slots[i]; + for(VSlot *vs = s->variants; vs; vs = vs->next) vs->slot = &dummyslot; + delete s; + } + slots.setsize(limit); + while(vslots.length()) + { + VSlot *vs = vslots.last(); + if(vs->slot != &dummyslot || vs->changed) break; + delete vslots.pop(); + } +} + +COMMAND(texturereset, "i"); + +void materialreset() +{ + if(!(identflags&IDF_OVERRIDDEN) && !game::allowedittoggle()) return; + loopi((MATF_VOLUME|MATF_INDEX)+1) materialslots[i].reset(); +} + +COMMAND(materialreset, ""); + +static int compactedvslots = 0, compactvslotsprogress = 0, clonedvslots = 0; +static bool markingvslots = false; + +void clearslots() +{ + resetslotshader(); + slots.deletecontents(); + vslots.deletecontents(); + loopi((MATF_VOLUME|MATF_INDEX)+1) materialslots[i].reset(); + clonedvslots = 0; +} + +static void assignvslot(VSlot &vs); + +static inline void assignvslotlayer(VSlot &vs) +{ + if(vs.layer && vslots.inrange(vs.layer)) + { + VSlot &layer = *vslots[vs.layer]; + if(layer.index < 0) assignvslot(layer); + } +} + +static void assignvslot(VSlot &vs) +{ + vs.index = compactedvslots++; + assignvslotlayer(vs); +} + +void compactvslot(int &index) +{ + if(vslots.inrange(index)) + { + VSlot &vs = *vslots[index]; + if(vs.index < 0) assignvslot(vs); + if(!markingvslots) index = vs.index; + } +} + +void compactvslot(VSlot &vs) +{ + if(vs.index < 0) assignvslot(vs); +} + +void compactvslots(cube *c, int n) +{ + if((compactvslotsprogress++&0xFFF)==0) renderprogress(min(float(compactvslotsprogress)/allocnodes, 1.0f), markingvslots ? "marking slots..." : "compacting slots..."); + loopi(n) + { + if(c[i].children) compactvslots(c[i].children); + else loopj(6) if(vslots.inrange(c[i].texture[j])) + { + VSlot &vs = *vslots[c[i].texture[j]]; + if(vs.index < 0) assignvslot(vs); + if(!markingvslots) c[i].texture[j] = vs.index; + } + } +} + +int compactvslots() +{ + clonedvslots = 0; + markingvslots = false; + compactedvslots = 0; + compactvslotsprogress = 0; + loopv(vslots) vslots[i]->index = -1; + loopv(slots) slots[i]->variants->index = compactedvslots++; + loopv(slots) assignvslotlayer(*slots[i]->variants); + loopv(vslots) + { + VSlot &vs = *vslots[i]; + if(!vs.changed && vs.index < 0) { markingvslots = true; break; } + } + compactvslots(worldroot); + int total = compactedvslots; + compacteditvslots(); + loopv(vslots) + { + VSlot *vs = vslots[i]; + if(vs->changed) continue; + while(vs->next) + { + if(vs->next->index < 0) vs->next = vs->next->next; + else vs = vs->next; + } + } + if(markingvslots) + { + markingvslots = false; + compactedvslots = 0; + compactvslotsprogress = 0; + int lastdiscard = 0; + loopv(vslots) + { + VSlot &vs = *vslots[i]; + if(vs.changed || (vs.index < 0 && !vs.next)) vs.index = -1; + else + { + while(lastdiscard < i) + { + VSlot &ds = *vslots[lastdiscard++]; + if(!ds.changed && ds.index < 0) ds.index = compactedvslots++; + } + vs.index = compactedvslots++; + } + } + compactvslots(worldroot); + total = compactedvslots; + compacteditvslots(); + } + compactmruvslots(); + loopv(vslots) + { + VSlot &vs = *vslots[i]; + if(vs.index >= 0 && vs.layer && vslots.inrange(vs.layer)) vs.layer = vslots[vs.layer]->index; + } + loopv(vslots) + { + while(vslots[i]->index >= 0 && vslots[i]->index != i) + swap(vslots[i], vslots[vslots[i]->index]); + } + for(int i = compactedvslots; i < vslots.length(); i++) delete vslots[i]; + vslots.setsize(compactedvslots); + return total; +} + +ICOMMAND(compactvslots, "", (), +{ + extern int nompedit; + if(nompedit && multiplayer()) return; + compactvslots(); + allchanged(); +}); + +static Slot &loadslot(Slot &s, bool forceload); + +static void clampvslotoffset(VSlot &dst, Slot *slot = NULL) +{ + if(!slot) slot = dst.slot; + if(slot && slot->sts.inrange(0)) + { + if(!slot->loaded) loadslot(*slot, false); + Texture *t = slot->sts[0].t; + int xs = t->xs, ys = t->ys; + if(t->type & Texture::MIRROR) { xs *= 2; ys *= 2; } + if(texrotations[dst.rotation].swapxy) swap(xs, ys); + dst.offset.x %= xs; if(dst.offset.x < 0) dst.offset.x += xs; + dst.offset.y %= ys; if(dst.offset.y < 0) dst.offset.y += ys; + } + else dst.offset.max(0); +} + +static void propagatevslot(VSlot &dst, const VSlot &src, int diff, bool edit = false) +{ + if(diff & (1<next; vs; vs = vs->next) + { + int diff = changed & ~vs->changed; + if(diff) propagatevslot(*vs, *root, diff); + } +} + +static void mergevslot(VSlot &dst, const VSlot &src, int diff, Slot *slot = NULL) +{ + if(diff & (1<slot = &owner; + vs->linked = false; + vs = vs->next; + } + return owner.variants; +} + +static VSlot *emptyvslot(Slot &owner) +{ + int offset = 0; + loopvrev(slots) if(slots[i]->variants) { offset = slots[i]->variants->index + 1; break; } + for(int i = offset; i < vslots.length(); i++) if(!vslots[i]->changed) return reassignvslot(owner, vslots[i]); + return vslots.add(new VSlot(&owner, vslots.length())); +} + +static bool comparevslot(const VSlot &dst, const VSlot &src, int diff) +{ + if(diff & (1< &buf, const VSlot &src) +{ + if(src.changed & (1<changed ? src.layer : 0); + } + if(src.changed & (1< &buf, int index) +{ + if(vslots.inrange(index)) packvslot(buf, *vslots[index]); + else buf.put(0xFF); +} + +void packvslot(vector &buf, const VSlot *vs) +{ + if(vs) packvslot(buf, *vs); + else buf.put(0xFF); +} + +bool unpackvslot(ucharbuf &buf, VSlot &dst, bool delta) +{ + while(buf.remaining()) + { + int changed = buf.get(); + if(changed >= 0x80) break; + switch(changed) + { + case VSLOT_SHPARAM: + { + string name; + getstring(name, buf); + SlotShaderParam p = { name[0] ? getshaderparamname(name) : NULL, -1, { 0, 0, 0, 0 } }; + loopi(4) p.val[i] = getfloat(buf); + if(p.name) dst.params.add(p); + break; + } + case VSLOT_SCALE: + dst.scale = getfloat(buf); + if(dst.scale <= 0) dst.scale = 1; + else if(!delta) dst.scale = clamp(dst.scale, 1/8.0f, 8.0f); + break; + case VSLOT_ROTATION: + dst.rotation = getint(buf); + if(!delta) dst.rotation = clamp(dst.rotation, 0, 7); + break; + case VSLOT_OFFSET: + dst.offset.x = getint(buf); + dst.offset.y = getint(buf); + if(!delta) dst.offset.max(0); + break; + case VSLOT_SCROLL: + dst.scroll.x = getfloat(buf); + dst.scroll.y = getfloat(buf); + break; + case VSLOT_LAYER: + { + int tex = getuint(buf); + dst.layer = vslots.inrange(tex) ? tex : 0; + break; + } + case VSLOT_ALPHA: + dst.alphafront = clamp(getfloat(buf), 0.0f, 1.0f); + dst.alphaback = clamp(getfloat(buf), 0.0f, 1.0f); + break; + case VSLOT_COLOR: + dst.colorscale.r = clamp(getfloat(buf), 0.0f, 2.0f); + dst.colorscale.g = clamp(getfloat(buf), 0.0f, 2.0f); + dst.colorscale.b = clamp(getfloat(buf), 0.0f, 2.0f); + break; + default: + return false; + } + dst.changed |= 1<next) + { + if((!dst->changed || dst->changed == (src.changed | delta.changed)) && + comparevslot(*dst, src, src.changed & ~delta.changed) && + comparevslot(*dst, delta, delta.changed)) + return dst; + } + return NULL; +} + +static VSlot *clonevslot(const VSlot &src, const VSlot &delta) +{ + VSlot *dst = vslots.add(new VSlot(src.slot, vslots.length())); + dst->changed = src.changed | delta.changed; + propagatevslot(*dst, src, ((1<=0x10000) + { + compactvslots(); + allchanged(); + if(vslots.length()>=0x10000) return NULL; + } + if(autocompactvslots && ++clonedvslots >= autocompactvslots) + { + compactvslots(); + allchanged(); + } + return clonevslot(src, delta); +} + +static void fixinsidefaces(cube *c, const ivec &o, int size, int tex) +{ + loopi(8) + { + ivec co(i, o, size); + if(c[i].children) fixinsidefaces(c[i].children, co, size>>1, tex); + else loopj(6) if(!visibletris(c[i], j, co, size)) + c[i].texture[j] = tex; + } +} + +ICOMMAND(fixinsidefaces, "i", (int *tex), +{ + extern int nompedit; + if(noedit(true) || (nompedit && multiplayer())) return; + fixinsidefaces(worldroot, ivec(0, 0, 0), worldsize>>1, *tex && vslots.inrange(*tex) ? *tex : DEFAULT_GEOM); + allchanged(); +}); + +const struct slottex +{ + const char *name; + int id; +} slottexs[] = +{ + {"c", TEX_DIFFUSE}, + {"u", TEX_UNKNOWN}, + {"d", TEX_DECAL}, + {"n", TEX_NORMAL}, + {"g", TEX_GLOW}, + {"s", TEX_SPEC}, + {"z", TEX_DEPTH}, + {"a", TEX_ALPHA}, + {"e", TEX_ENVMAP} +}; + +int findslottex(const char *name) +{ + loopi(sizeof(slottexs)/sizeof(slottex)) + { + if(!strcmp(slottexs[i].name, name)) return slottexs[i].id; + } + return -1; +} + +void texture(char *type, char *name, int *rot, int *xoffset, int *yoffset, float *scale) +{ + if(slots.length()>=0x10000) return; + static int lastmatslot = -1; + int tnum = findslottex(type), matslot = findmaterial(type); + if(tnum<0) tnum = atoi(type); + if(tnum==TEX_DIFFUSE) lastmatslot = matslot; + else if(lastmatslot>=0) matslot = lastmatslot; + else if(slots.empty()) return; + Slot &s = matslot>=0 ? materialslots[matslot] : *(tnum!=TEX_DIFFUSE ? slots.last() : slots.add(new Slot(slots.length()))); + s.loaded = false; + s.texmask |= 1<=8) conoutf(CON_WARN, "warning: too many textures in slot %d", slots.length()-1); + Slot::Tex &st = s.sts.add(); + st.type = tnum; + st.combined = -1; + st.t = NULL; + copystring(st.name, name); + path(st.name); + if(tnum==TEX_DIFFUSE) + { + setslotshader(s); + VSlot &vs = matslot >= 0 ? materialslots[matslot] : *emptyvslot(s); + vs.reset(); + vs.rotation = clamp(*rot, 0, 7); + vs.offset = ivec2(*xoffset, *yoffset).max(0); + vs.scale = *scale <= 0 ? 1 : *scale; + propagatevslot(&vs, (1<")) : NULL; +} +COMMAND(autograss, "s"); + +void texscroll(float *scrollS, float *scrollT) +{ + if(slots.empty()) return; + Slot &s = *slots.last(); + s.variants->scroll = vec2(*scrollS, *scrollT).div(1000.0f); + propagatevslot(s.variants, 1<offset = ivec2(*xoffset, *yoffset).max(0); + propagatevslot(s.variants, 1<rotation = clamp(*rot, 0, 7); + propagatevslot(s.variants, 1<scale = *scale <= 0 ? 1 : *scale; + propagatevslot(s.variants, 1<layer = *layer < 0 ? max(slots.length()-1+*layer, 0) : *layer; + s.layermaskname = name[0] ? newstring(path(makerelpath("packages", name))) : NULL; + s.layermaskmode = *mode; + s.layermaskscale = *scale <= 0 ? 1 : *scale; + propagatevslot(s.variants, 1<alphafront = clamp(*front, 0.0f, 1.0f); + s.variants->alphaback = clamp(*back, 0.0f, 1.0f); + propagatevslot(s.variants, 1<colorscale = vec(clamp(*r, 0.0f, 1.0f), clamp(*g, 0.0f, 1.0f), clamp(*b, 0.0f, 1.0f)); + propagatevslot(s.variants, 1< &key, Slot &slot, Slot::Tex &t, bool combined = false, const char *prefix = NULL) +{ + if(combined) key.add('&'); + if(prefix) { while(*prefix) key.add(*prefix++); } + defformatstring(tname, "packages/%s", t.name); + for(const char *s = path(tname); *s; key.add(*s++)); +} + +static void texcombine(Slot &s, int index, Slot::Tex &t, bool forceload = false) +{ + vector key; + addname(key, s, t); + int texmask = 0; + if(!forceload) switch(t.type) + { + case TEX_DIFFUSE: + case TEX_NORMAL: + { + int i = findtextype(s, t.type==TEX_DIFFUSE ? (s.texmask&(1<= 0) continue; + switch(t.type) + { + case TEX_ENVMAP: + t.t = cubemapload(t.name); + break; + + default: + texcombine(s, i, t, forceload); + break; + } + } + s.loaded = true; + return s; +} + +MSlot &lookupmaterialslot(int index, bool load) +{ + if(materialslots[index].sts.empty() && index&MATF_INDEX) index &= ~MATF_INDEX; + MSlot &s = materialslots[index]; + if(load && !s.linked) + { + if(!s.loaded) loadslot(s, true); + linkvslotshader(s); + s.linked = true; + } + return s; +} + +Slot &lookupslot(int index, bool load) +{ + Slot &s = slots.inrange(index) ? *slots[index] : (slots.inrange(DEFAULT_GEOM) ? *slots[DEFAULT_GEOM] : dummyslot); + return s.loaded || !load ? s : loadslot(s, false); +} + +VSlot &lookupvslot(int index, bool load) +{ + VSlot &s = vslots.inrange(index) && vslots[index]->slot ? *vslots[index] : (slots.inrange(DEFAULT_GEOM) && slots[DEFAULT_GEOM]->variants ? *slots[DEFAULT_GEOM]->variants : dummyvslot); + if(load && !s.linked) + { + if(!s.slot->loaded) loadslot(*s.slot, false); + linkvslotshader(s); + s.linked = true; + } + return s; +} + +void linkslotshaders() +{ + loopv(slots) if(slots[i]->loaded) linkslotshader(*slots[i]); + loopv(vslots) if(vslots[i]->linked) linkvslotshader(*vslots[i]); + loopi((MATF_VOLUME|MATF_INDEX)+1) if(materialslots[i].loaded) + { + linkslotshader(materialslots[i]); + linkvslotshader(materialslots[i]); + } +} + +Texture *loadthumbnail(Slot &slot) +{ + if(slot.thumbnail) return slot.thumbnail; + if(!slot.variants) + { + slot.thumbnail = notexture; + return slot.thumbnail; + } + VSlot &vslot = *slot.variants; + linkslotshader(slot, false); + linkvslotshader(vslot, false); + vector name; + if(vslot.colorscale == vec(1, 1, 1)) addname(name, slot, slot.sts[0], false, ""); + else + { + defformatstring(prefix, "", vslot.colorscale.x, vslot.colorscale.y, vslot.colorscale.z); + addname(name, slot, slot.sts[0], false, prefix); + } + int glow = -1; + if(slot.texmask&(1<= 0) + { + defformatstring(prefix, "", vslot.glowcolor.x, vslot.glowcolor.y, vslot.glowcolor.z); + addname(name, slot, slot.sts[glow], true, prefix); + } + } + VSlot *layer = vslot.layer ? &lookupvslot(vslot.layer, false) : NULL; + if(layer) + { + if(layer->colorscale == vec(1, 1, 1)) addname(name, *layer->slot, layer->slot->sts[0], true, ""); + else + { + defformatstring(prefix, "", vslot.colorscale.x, vslot.colorscale.y, vslot.colorscale.z); + addname(name, *layer->slot, layer->slot->sts[0], true, prefix); + } + } + name.add('\0'); + Texture *t = textures.access(path(name.getbuf())); + if(t) slot.thumbnail = t; + else + { + ImageData s, g, l; + texturedata(s, NULL, &slot.sts[0], false); + if(glow >= 0) texturedata(g, NULL, &slot.sts[glow], false); + if(layer) texturedata(l, NULL, &layer->slot->sts[0], false); + if(!s.data) t = slot.thumbnail = notexture; + else + { + if(vslot.colorscale != vec(1, 1, 1)) texmad(s, vslot.colorscale, vec(0, 0, 0)); + int xs = s.w, ys = s.h; + if(s.w > 64 || s.h > 64) scaleimage(s, min(s.w, 64), min(s.h, 64)); + if(g.data) + { + if(g.w != s.w || g.h != s.h) scaleimage(g, s.w, s.h); + addglow(s, g, vslot.glowcolor); + } + if(l.data) + { + if(layer->colorscale != vec(1, 1, 1)) texmad(l, layer->colorscale, vec(0, 0, 0)); + if(l.w != s.w/2 || l.h != s.h/2) scaleimage(l, s.w/2, s.h/2); + forcergbimage(s); + forcergbimage(l); + uchar *dstrow = &s.data[s.pitch*l.h + s.bpp*l.w], *srcrow = l.data; + loop(y, l.h) + { + for(uchar *dst = dstrow, *src = srcrow, *end = &srcrow[l.w*l.bpp]; src < end; dst += s.bpp, src += l.bpp) + loopk(3) dst[k] = src[k]; + dstrow += s.pitch; + srcrow += l.pitch; + } + } + if(s.bpp < 3) forcergbimage(s); + t = newtexture(NULL, name.getbuf(), s, 0, false, false, true); + t->xs = xs; + t->ys = ys; + slot.thumbnail = t; + } + } + return t; +} + +void loadlayermasks() +{ + loopv(slots) + { + Slot &slot = *slots[i]; + if(slot.loaded && slot.layermaskname && !slot.layermask) + { + slot.layermask = new ImageData; + texturedata(*slot.layermask, slot.layermaskname); + if(!slot.layermask->data) DELETEP(slot.layermask); + } + } +} + +// environment mapped reflections + +void forcecubemapload(GLuint tex) +{ + extern int ati_cubemap_bug; + if(!ati_cubemap_bug || !tex) return; + + SETSHADER(cubemap); + GLenum depthtest = glIsEnabled(GL_DEPTH_TEST), blend = glIsEnabled(GL_BLEND); + if(depthtest) glDisable(GL_DEPTH_TEST); + glBindTexture(GL_TEXTURE_CUBE_MAP, tex); + if(!blend) glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + gle::defvertex(2); + gle::deftexcoord0(3); + gle::defcolor(4); + gle::begin(GL_LINES); + loopi(2) + { + gle::attribf(i*1e-3f, 0); + gle::attribf(0, 0, 1); + gle::attribf(1, 1, 1, 0); + } + gle::end(); + if(!blend) glDisable(GL_BLEND); + if(depthtest) glEnable(GL_DEPTH_TEST); +} + +extern const cubemapside cubemapsides[6] = +{ + { GL_TEXTURE_CUBE_MAP_NEGATIVE_X, "lf", true, true, true }, + { GL_TEXTURE_CUBE_MAP_POSITIVE_X, "rt", false, false, true }, + { GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, "ft", true, false, false }, + { GL_TEXTURE_CUBE_MAP_POSITIVE_Y, "bk", false, true, false }, + { GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, "dn", false, false, true }, + { GL_TEXTURE_CUBE_MAP_POSITIVE_Z, "up", false, false, true }, +}; + +VARFP(envmapsize, 4, 7, 10, setupmaterials()); + +Texture *cubemaploadwildcard(Texture *t, const char *name, bool mipit, bool msg, bool transient = false) +{ + string tname; + if(!name) copystring(tname, t->name); + else + { + copystring(tname, name); + t = textures.access(path(tname)); + if(t) + { + if(!transient && t->type&Texture::TRANSIENT) t->type &= ~Texture::TRANSIENT; + return t; + } + } + char *wildcard = strchr(tname, '*'); + ImageData surface[6]; + string sname; + if(!wildcard) copystring(sname, tname); + int tsize = 0, compress = 0; + loopi(6) + { + if(wildcard) + { + copystring(sname, stringslice(tname, wildcard)); + concatstring(sname, cubemapsides[i].name); + concatstring(sname, wildcard+1); + } + ImageData &s = surface[i]; + texturedata(s, sname, NULL, msg, &compress); + if(!s.data) return NULL; + if(s.w != s.h) + { + if(msg) conoutf(CON_ERROR, "cubemap texture %s does not have square size", sname); + return NULL; + } + if(s.compressed ? s.compressed!=surface[0].compressed || s.w!=surface[0].w || s.h!=surface[0].h || s.levels!=surface[0].levels : surface[0].compressed || s.bpp!=surface[0].bpp) + { + if(msg) conoutf(CON_ERROR, "cubemap texture %s doesn't match other sides' format", sname); + return NULL; + } + tsize = max(tsize, max(s.w, s.h)); + } + if(name) + { + char *key = newstring(tname); + t = &textures[key]; + t->name = key; + } + t->type = Texture::CUBEMAP; + if(transient) t->type |= Texture::TRANSIENT; + GLenum format; + if(surface[0].compressed) + { + format = uncompressedformat(surface[0].compressed); + t->bpp = formatsize(format); + t->type |= Texture::COMPRESSED; + } + else + { + format = texformat(surface[0].bpp, true); + t->bpp = surface[0].bpp; + if(hasTRG && !hasTSW && swizzlemask(format)) + { + loopi(6) swizzleimage(surface[i]); + format = texformat(surface[0].bpp, true); + t->bpp = surface[0].bpp; + } + } + if(alphaformat(format)) t->type |= Texture::ALPHA; + t->mipmap = mipit; + t->clamp = 3; + t->xs = t->ys = tsize; + t->w = t->h = min(1<w, t->h, mipit, false, GL_TEXTURE_CUBE_MAP, compress, t->w, t->h); + GLenum component = format; + if(!surface[0].compressed) + { + component = compressedformat(format, t->w, t->h, compress); + switch(component) + { + case GL_RGB: component = GL_RGB5; break; + } + } + glGenTextures(1, &t->id); + loopi(6) + { + ImageData &s = surface[i]; + const cubemapside &side = cubemapsides[i]; + texreorient(s, side.flipx, side.flipy, side.swapxy); + if(s.compressed) + { + int w = s.w, h = s.h, levels = s.levels, level = 0; + uchar *data = s.data; + while(levels > 1 && (w > t->w || h > t->h)) + { + data += s.calclevelsize(level++); + levels--; + if(w > 1) w /= 2; + if(h > 1) h /= 2; + } + createcompressedtexture(t->id, w, h, data, s.align, s.bpp, levels, i ? -1 : 3, mipit ? 2 : 1, s.compressed, side.target, true); + } + else + { + createtexture(t->id, t->w, t->h, s.data, i ? -1 : 3, mipit ? 2 : 1, component, side.target, s.w, s.h, s.pitch, false, format, true); + } + } + forcecubemapload(t->id); + return t; +} + +Texture *cubemapload(const char *name, bool mipit, bool msg, bool transient) +{ + string pname; + copystring(pname, makerelpath("packages", name)); + path(pname); + Texture *t = NULL; + if(!strchr(pname, '*')) + { + defformatstring(jpgname, "%s_*.jpg", pname); + t = cubemaploadwildcard(NULL, jpgname, mipit, false, transient); + if(!t) + { + defformatstring(pngname, "%s_*.png", pname); + t = cubemaploadwildcard(NULL, pngname, mipit, false, transient); + if(!t && msg) conoutf(CON_ERROR, "could not load envmap %s", name); + } + } + else t = cubemaploadwildcard(NULL, pname, mipit, msg, transient); + return t; +} + +VARR(envmapradius, 0, 128, 10000); +VARR(envmapbb, 0, 0, 1); + +struct envmap +{ + int radius, size, blur; + vec o; + GLuint tex; + + envmap() : radius(-1), size(0), blur(0), o(0, 0, 0), tex(0) {} + + void clear() + { + if(tex) { glDeleteTextures(1, &tex); tex = 0; } + } +}; + +static vector envmaps; +static Texture *skyenvmap = NULL; + +void clearenvmaps() +{ + if(skyenvmap) + { + if(skyenvmap->type&Texture::TRANSIENT) cleanuptexture(skyenvmap); + skyenvmap = NULL; + } + loopv(envmaps) envmaps[i].clear(); + envmaps.shrink(0); +} + +VAR(aaenvmap, 0, 2, 4); + +GLuint genenvmap(const vec &o, int envmapsize, int blur, bool onlysky) +{ + int rendersize = 1<<(envmapsize+aaenvmap), sizelimit = min(hwcubetexsize, min(screenw, screenh)); + if(maxtexsize) sizelimit = min(sizelimit, maxtexsize); + while(rendersize > sizelimit) rendersize /= 2; + int texsize = min(rendersize, 1< texsize) + { + scaletexture(src, rendersize, rendersize, 3, 3*rendersize, dst, texsize, texsize); + swap(src, dst); + } + if(blur > 0) + { + blurtexture(blur, 3, texsize, texsize, dst, src); + swap(src, dst); + } + createtexture(tex, texsize, texsize, src, 3, 2, GL_RGB5, side.target); + } + glFrontFace(GL_CW); + delete[] pixels; + glViewport(0, 0, screenw, screenh); + clientkeepalive(); + forcecubemapload(tex); + return tex; +} + +void initenvmaps() +{ + clearenvmaps(); + skyenvmap = NULL; + if(shouldrenderskyenvmap()) envmaps.add().size = 1; + else if(skybox[0]) skyenvmap = cubemapload(skybox, true, false, true); + const vector &ents = entities::getents(); + loopv(ents) + { + const extentity &ent = *ents[i]; + if(ent.type != ET_ENVMAP) continue; + envmap &em = envmaps.add(); + em.radius = ent.attr1 ? clamp(int(ent.attr1), 0, 10000) : envmapradius; + em.size = ent.attr2 ? clamp(int(ent.attr2), 4, 9) : 0; + em.blur = ent.attr3 ? clamp(int(ent.attr3), 1, 2) : 0; + em.o = ent.o; + } +} + +void genenvmaps() +{ + if(envmaps.empty()) return; + renderprogress(0, "generating environment maps..."); + int lastprogress = SDL_GetTicks(); + loopv(envmaps) + { + envmap &em = envmaps[i]; + em.tex = genenvmap(em.o, em.size ? min(em.size, envmapsize) : envmapsize, em.blur, em.radius < 0); + if(renderedframe) continue; + int millis = SDL_GetTicks(); + if(millis - lastprogress >= 250) + { + renderprogress(float(i+1)/envmaps.length(), "generating environment maps...", 0, true); + lastprogress = millis; + } + } +} + +ushort closestenvmap(const vec &o) +{ + ushort minemid = EMID_SKY; + float mindist = 1e16f; + loopv(envmaps) + { + envmap &em = envmaps[i]; + float dist; + if(envmapbb) + { + if(!o.insidebb(vec(em.o).sub(em.radius), vec(em.o).add(em.radius))) continue; + dist = em.o.dist(o); + } + else + { + dist = em.o.dist(o); + if(dist > em.radius) continue; + } + if(dist < mindist) + { + minemid = EMID_RESERVED + i; + mindist = dist; + } + } + return minemid; +} + +ushort closestenvmap(int orient, const ivec &co, int size) +{ + vec loc(co); + int dim = dimension(orient); + if(dimcoord(orient)) loc[dim] += size; + loc[R[dim]] += size/2; + loc[C[dim]] += size/2; + return closestenvmap(loc); +} + +static inline GLuint lookupskyenvmap() +{ + return envmaps.length() && envmaps[0].radius < 0 ? envmaps[0].tex : (skyenvmap ? skyenvmap->id : 0); +} + +GLuint lookupenvmap(Slot &slot) +{ + loopv(slot.sts) if(slot.sts[i].type==TEX_ENVMAP && slot.sts[i].t) return slot.sts[i].t->id; + return lookupskyenvmap(); +} + +GLuint lookupenvmap(ushort emid) +{ + if(emid==EMID_SKY || emid==EMID_CUSTOM) return skyenvmap ? skyenvmap->id : 0; + if(emid==EMID_NONE || !envmaps.inrange(emid-EMID_RESERVED)) return 0; + GLuint tex = envmaps[emid-EMID_RESERVED].tex; + return tex ? tex : lookupskyenvmap(); +} + +void cleanuptexture(Texture *t) +{ + DELETEA(t->alphamask); + if(t->id) { glDeleteTextures(1, &t->id); t->id = 0; } + if(t->type&Texture::TRANSIENT) textures.remove(t->name); +} + +void cleanuptextures() +{ + cleanupmipmaps(); + clearenvmaps(); + loopv(slots) slots[i]->cleanup(); + loopv(vslots) vslots[i]->cleanup(); + loopi((MATF_VOLUME|MATF_INDEX)+1) materialslots[i].cleanup(); + enumerate(textures, Texture, tex, cleanuptexture(&tex)); +} + +bool reloadtexture(const char *name) +{ + Texture *t = textures.access(path(name, true)); + if(t) return reloadtexture(*t); + return true; +} + +bool reloadtexture(Texture &tex) +{ + if(tex.id) return true; + switch(tex.type&Texture::TYPE) + { + case Texture::IMAGE: + { + int compress = 0; + ImageData s; + if(!texturedata(s, tex.name, NULL, true, &compress) || !newtexture(&tex, NULL, s, tex.clamp, tex.mipmap, false, false, compress)) return false; + break; + } + + case Texture::CUBEMAP: + if(!cubemaploadwildcard(&tex, NULL, tex.mipmap, true)) return false; + break; + } + return true; +} + +void reloadtex(char *name) +{ + Texture *t = textures.access(path(name, true)); + if(!t) { conoutf(CON_ERROR, "texture %s is not loaded", name); return; } + if(t->type&Texture::TRANSIENT) { conoutf(CON_ERROR, "can't reload transient texture %s", name); return; } + DELETEA(t->alphamask); + Texture oldtex = *t; + t->id = 0; + if(!reloadtexture(*t)) + { + if(t->id) glDeleteTextures(1, &t->id); + *t = oldtex; + conoutf(CON_ERROR, "failed to reload texture %s", name); + } +} + +COMMAND(reloadtex, "s"); + +void reloadtextures() +{ + int reloaded = 0; + enumerate(textures, Texture, tex, + { + loadprogress = float(++reloaded)/textures.numelems; + reloadtexture(tex); + }); + loadprogress = 0; +} + +enum +{ + DDSD_CAPS = 0x00000001, + DDSD_HEIGHT = 0x00000002, + DDSD_WIDTH = 0x00000004, + DDSD_PITCH = 0x00000008, + DDSD_PIXELFORMAT = 0x00001000, + DDSD_MIPMAPCOUNT = 0x00020000, + DDSD_LINEARSIZE = 0x00080000, + DDSD_BACKBUFFERCOUNT = 0x00800000, + DDPF_ALPHAPIXELS = 0x00000001, + DDPF_FOURCC = 0x00000004, + DDPF_INDEXED = 0x00000020, + DDPF_ALPHA = 0x00000002, + DDPF_RGB = 0x00000040, + DDPF_COMPRESSED = 0x00000080, + DDPF_LUMINANCE = 0x00020000, + DDSCAPS_COMPLEX = 0x00000008, + DDSCAPS_TEXTURE = 0x00001000, + DDSCAPS_MIPMAP = 0x00400000, + DDSCAPS2_CUBEMAP = 0x00000200, + DDSCAPS2_CUBEMAP_POSITIVEX = 0x00000400, + DDSCAPS2_CUBEMAP_NEGATIVEX = 0x00000800, + DDSCAPS2_CUBEMAP_POSITIVEY = 0x00001000, + DDSCAPS2_CUBEMAP_NEGATIVEY = 0x00002000, + DDSCAPS2_CUBEMAP_POSITIVEZ = 0x00004000, + DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x00008000, + DDSCAPS2_VOLUME = 0x00200000, + FOURCC_DXT1 = 0x31545844, + FOURCC_DXT2 = 0x32545844, + FOURCC_DXT3 = 0x33545844, + FOURCC_DXT4 = 0x34545844, + FOURCC_DXT5 = 0x35545844, + FOURCC_ATI1 = 0x31495441, + FOURCC_ATI2 = 0x32495441 +}; + +struct DDCOLORKEY { uint dwColorSpaceLowValue, dwColorSpaceHighValue; }; +struct DDPIXELFORMAT +{ + uint dwSize, dwFlags, dwFourCC; + union { uint dwRGBBitCount, dwYUVBitCount, dwZBufferBitDepth, dwAlphaBitDepth, dwLuminanceBitCount, dwBumpBitCount, dwPrivateFormatBitCount; }; + union { uint dwRBitMask, dwYBitMask, dwStencilBitDepth, dwLuminanceBitMask, dwBumpDuBitMask, dwOperations; }; + union { uint dwGBitMask, dwUBitMask, dwZBitMask, dwBumpDvBitMask; struct { ushort wFlipMSTypes, wBltMSTypes; } MultiSampleCaps; }; + union { uint dwBBitMask, dwVBitMask, dwStencilBitMask, dwBumpLuminanceBitMask; }; + union { uint dwRGBAlphaBitMask, dwYUVAlphaBitMask, dwLuminanceAlphaBitMask, dwRGBZBitMask, dwYUVZBitMask; }; + +}; +struct DDSCAPS2 { uint dwCaps, dwCaps2, dwCaps3, dwCaps4; }; +struct DDSURFACEDESC2 +{ + uint dwSize, dwFlags, dwHeight, dwWidth; + union { int lPitch; uint dwLinearSize; }; + uint dwBackBufferCount; + union { uint dwMipMapCount, dwRefreshRate, dwSrcVBHandle; }; + uint dwAlphaBitDepth, dwReserved, lpSurface; + union { DDCOLORKEY ddckCKDestOverlay; uint dwEmptyFaceColor; }; + DDCOLORKEY ddckCKDestBlt, ddckCKSrcOverlay, ddckCKSrcBlt; + union { DDPIXELFORMAT ddpfPixelFormat; uint dwFVF; }; + DDSCAPS2 ddsCaps; + uint dwTextureStage; +}; + +#define DECODEDDS(name, dbpp, initblock, writeval, nextval) \ +static void name(ImageData &s) \ +{ \ + ImageData d(s.w, s.h, dbpp); \ + uchar *dst = d.data; \ + const uchar *src = s.data; \ + for(int by = 0; by < s.h; by += s.align) \ + { \ + for(int bx = 0; bx < s.w; bx += s.align, src += s.bpp) \ + { \ + int maxy = min(d.h - by, s.align), maxx = min(d.w - bx, s.align); \ + initblock; \ + loop(y, maxy) \ + { \ + int x; \ + for(x = 0; x < maxx; ++x) \ + { \ + writeval; \ + nextval; \ + dst += d.bpp; \ + } \ + for(; x < s.align; ++x) { nextval; } \ + dst += d.pitch - maxx*d.bpp; \ + } \ + dst += maxx*d.bpp - maxy*d.pitch; \ + } \ + dst += (s.align-1)*d.pitch; \ + } \ + s.replace(d); \ +} + +DECODEDDS(decodedxt1, s.compressed == GL_COMPRESSED_RGBA_S3TC_DXT1_EXT ? 4 : 3, + ushort color0 = lilswap(*(const ushort *)src); + ushort color1 = lilswap(*(const ushort *)&src[2]); + uint bits = lilswap(*(const uint *)&src[4]); + bvec4 rgba[4]; + rgba[0] = bvec4(bvec::from565(color0), 0xFF); + rgba[1] = bvec4(bvec::from565(color1), 0xFF); + if(color0 > color1) + { + rgba[2].lerp(rgba[0], rgba[1], 2, 1, 3); + rgba[3].lerp(rgba[0], rgba[1], 1, 2, 3); + } + else + { + rgba[2].lerp(rgba[0], rgba[1], 1, 1, 2); + rgba[3] = bvec4(0, 0, 0, 0); + } +, + memcpy(dst, rgba[bits&3].v, d.bpp); +, + bits >>= 2; +); + +DECODEDDS(decodedxt3, 4, + ullong alpha = lilswap(*(const ullong *)src); + ushort color0 = lilswap(*(const ushort *)&src[8]); + ushort color1 = lilswap(*(const ushort *)&src[10]); + uint bits = lilswap(*(const uint *)&src[12]); + bvec rgb[4]; + rgb[0] = bvec::from565(color0); + rgb[1] = bvec::from565(color1); + rgb[2].lerp(rgb[0], rgb[1], 2, 1, 3); + rgb[3].lerp(rgb[0], rgb[1], 1, 2, 3); +, + memcpy(dst, rgb[bits&3].v, 3); + dst[3] = ((alpha&0xF)*1088 + 32) >> 6; +, + bits >>= 2; + alpha >>= 4; +); + +static inline void decodealpha(uchar alpha0, uchar alpha1, uchar alpha[8]) +{ + alpha[0] = alpha0; + alpha[1] = alpha1; + if(alpha0 > alpha1) + { + alpha[2] = (6*alpha0 + alpha1)/7; + alpha[3] = (5*alpha0 + 2*alpha1)/7; + alpha[4] = (4*alpha0 + 3*alpha1)/7; + alpha[5] = (3*alpha0 + 4*alpha1)/7; + alpha[6] = (2*alpha0 + 5*alpha1)/7; + alpha[7] = (alpha0 + 6*alpha1)/7; + } + else + { + alpha[2] = (4*alpha0 + alpha1)/5; + alpha[3] = (3*alpha0 + 2*alpha1)/5; + alpha[4] = (2*alpha0 + 3*alpha1)/5; + alpha[5] = (alpha0 + 4*alpha1)/5; + alpha[6] = 0; + alpha[7] = 0xFF; + } +} + +DECODEDDS(decodedxt5, 4, + uchar alpha[8]; + decodealpha(src[0], src[1], alpha); + ullong alphabits = lilswap(*(const ushort *)&src[2]) + ((ullong)lilswap(*(const uint *)&src[4]) << 16); + ushort color0 = lilswap(*(const ushort *)&src[8]); + ushort color1 = lilswap(*(const ushort *)&src[10]); + uint bits = lilswap(*(const uint *)&src[12]); + bvec rgb[4]; + rgb[0] = bvec::from565(color0); + rgb[1] = bvec::from565(color1); + rgb[2].lerp(rgb[0], rgb[1], 2, 1, 3); + rgb[3].lerp(rgb[0], rgb[1], 1, 2, 3); +, + memcpy(dst, rgb[bits&3].v, 3); + dst[3] = alpha[alphabits&7]; +, + bits >>= 2; + alphabits >>= 3; +); + +DECODEDDS(decodergtc1, 1, + uchar red[8]; + decodealpha(src[0], src[1], red); + ullong redbits = lilswap(*(const ushort *)&src[2]) + ((ullong)lilswap(*(const uint *)&src[4]) << 16); +, + dst[0] = red[redbits&7]; +, + redbits >>= 3; +); + +DECODEDDS(decodergtc2, 2, + uchar red[8]; + decodealpha(src[0], src[1], red); + ullong redbits = lilswap(*(const ushort *)&src[2]) + ((ullong)lilswap(*(const uint *)&src[4]) << 16); + uchar green[8]; + decodealpha(src[8], src[9], green); + ullong greenbits = lilswap(*(const ushort *)&src[10]) + ((ullong)lilswap(*(const uint *)&src[12]) << 16); +, + dst[0] = red[redbits&7]; + dst[1] = green[greenbits&7]; +, + redbits >>= 3; + greenbits >>= 3; +); + +bool loaddds(const char *filename, ImageData &image, int force) +{ + stream *f = openfile(filename, "rb"); + if(!f) return false; + GLenum format = GL_FALSE; + uchar magic[4]; + if(f->read(magic, 4) != 4 || memcmp(magic, "DDS ", 4)) { delete f; return false; } + DDSURFACEDESC2 d; + if(f->read(&d, sizeof(d)) != sizeof(d)) { delete f; return false; } + lilswap((uint *)&d, sizeof(d)/sizeof(uint)); + if(d.dwSize != sizeof(DDSURFACEDESC2) || d.ddpfPixelFormat.dwSize != sizeof(DDPIXELFORMAT)) { delete f; return false; } + bool supported = false; + if(d.ddpfPixelFormat.dwFlags & DDPF_FOURCC) + { + switch(d.ddpfPixelFormat.dwFourCC) + { + case FOURCC_DXT1: + if((supported = hasS3TC) || force) format = d.ddpfPixelFormat.dwFlags & DDPF_ALPHAPIXELS ? GL_COMPRESSED_RGBA_S3TC_DXT1_EXT : GL_COMPRESSED_RGB_S3TC_DXT1_EXT; + break; + case FOURCC_DXT2: + case FOURCC_DXT3: + if((supported = hasS3TC) || force) format = GL_COMPRESSED_RGBA_S3TC_DXT3_EXT; + break; + case FOURCC_DXT4: + case FOURCC_DXT5: + if((supported = hasS3TC) || force) format = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT; + break; + case FOURCC_ATI1: + if((supported = hasRGTC) || force) format = GL_COMPRESSED_RED_RGTC1; + else if((supported = hasLATC)) format = GL_COMPRESSED_LUMINANCE_LATC1_EXT; + break; + case FOURCC_ATI2: + if((supported = hasRGTC) || force) format = GL_COMPRESSED_RG_RGTC2; + else if((supported = hasLATC)) format = GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT; + break; + } + } + if(!format || (!supported && !force)) { delete f; return false; } + if(dbgdds) conoutf(CON_DEBUG, "%s: format 0x%X, %d x %d, %d mipmaps", filename, format, d.dwWidth, d.dwHeight, d.dwMipMapCount); + int bpp = 0; + switch(format) + { + case GL_COMPRESSED_RGB_S3TC_DXT1_EXT: + case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT: bpp = 8; break; + case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT: + case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT: bpp = 16; break; + case GL_COMPRESSED_LUMINANCE_LATC1_EXT: + case GL_COMPRESSED_RED_RGTC1: bpp = 8; break; + case GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT: + case GL_COMPRESSED_RG_RGTC2: bpp = 16; break; + } + image.setdata(NULL, d.dwWidth, d.dwHeight, bpp, !supported || force > 0 ? 1 : d.dwMipMapCount, 4, format); + size_t size = image.calcsize(); + if(f->read(image.data, size) != size) { delete f; image.cleanup(); return false; } + delete f; + if(!supported || force > 0) switch(format) + { + case GL_COMPRESSED_RGB_S3TC_DXT1_EXT: + case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT: + decodedxt1(image); + break; + case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT: + decodedxt3(image); + break; + case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT: + decodedxt5(image); + break; + case GL_COMPRESSED_LUMINANCE_LATC1_EXT: + case GL_COMPRESSED_RED_RGTC1: + decodergtc1(image); + break; + case GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT: + case GL_COMPRESSED_RG_RGTC2: + decodergtc2(image); + break; + } + return true; +} + +void gendds(char *infile, char *outfile) +{ + if(!hasS3TC || usetexcompress <= 1) { conoutf(CON_ERROR, "OpenGL driver does not support S3TC texture compression"); return; } + + glHint(GL_TEXTURE_COMPRESSION_HINT, GL_NICEST); + + defformatstring(cfile, "%s", infile); + extern void reloadtex(char *name); + Texture *t = textures.access(path(cfile)); + if(t) reloadtex(cfile); + t = textureload(cfile); + if(t==notexture) { conoutf(CON_ERROR, "failed loading %s", infile); return; } + + glBindTexture(GL_TEXTURE_2D, t->id); + GLint compressed = 0, format = 0, width = 0, height = 0; + glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_COMPRESSED, &compressed); + glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &format); + glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &width); + glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &height); + + if(!compressed) { conoutf(CON_ERROR, "failed compressing %s", infile); return; } + int fourcc = 0; + switch(format) + { + case GL_COMPRESSED_RGB_S3TC_DXT1_EXT: fourcc = FOURCC_DXT1; conoutf("compressed as DXT1"); break; + case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT: fourcc = FOURCC_DXT1; conoutf("compressed as DXT1a"); break; + case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT: fourcc = FOURCC_DXT3; conoutf("compressed as DXT3"); break; + case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT: fourcc = FOURCC_DXT5; conoutf("compressed as DXT5"); break; + case GL_COMPRESSED_LUMINANCE_LATC1_EXT: + case GL_COMPRESSED_RED_RGTC1: fourcc = FOURCC_ATI1; conoutf("compressed as ATI1"); break; + case GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT: + case GL_COMPRESSED_RG_RGTC2: fourcc = FOURCC_ATI2; conoutf("compressed as ATI2"); break; + default: + conoutf(CON_ERROR, "failed compressing %s: unknown format: 0x%X", infile, format); break; + return; + } + + if(!outfile[0]) + { + static string buf; + copystring(buf, infile); + int len = strlen(buf); + if(len > 4 && buf[len-4]=='.') memcpy(&buf[len-4], ".dds", 4); + else concatstring(buf, ".dds"); + outfile = buf; + } + + stream *f = openfile(path(outfile, true), "wb"); + if(!f) { conoutf(CON_ERROR, "failed writing to %s", outfile); return; } + + int csize = 0; + for(int lw = width, lh = height, level = 0;;) + { + GLint size = 0; + glGetTexLevelParameteriv(GL_TEXTURE_2D, level++, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &size); + csize += size; + if(max(lw, lh) <= 1) break; + if(lw > 1) lw /= 2; + if(lh > 1) lh /= 2; + } + + DDSURFACEDESC2 d; + memset(&d, 0, sizeof(d)); + d.dwSize = sizeof(DDSURFACEDESC2); + d.dwWidth = width; + d.dwHeight = height; + d.dwLinearSize = csize; + d.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT | DDSD_LINEARSIZE | DDSD_MIPMAPCOUNT; + d.ddsCaps.dwCaps = DDSCAPS_TEXTURE | DDSCAPS_COMPLEX | DDSCAPS_MIPMAP; + d.ddpfPixelFormat.dwSize = sizeof(DDPIXELFORMAT); + d.ddpfPixelFormat.dwFlags = DDPF_FOURCC | (alphaformat(uncompressedformat(format)) ? DDPF_ALPHAPIXELS : 0); + d.ddpfPixelFormat.dwFourCC = fourcc; + + uchar *data = new uchar[csize], *dst = data; + for(int lw = width, lh = height;;) + { + GLint size; + glGetTexLevelParameteriv(GL_TEXTURE_2D, d.dwMipMapCount, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &size); + glGetCompressedTexImage_(GL_TEXTURE_2D, d.dwMipMapCount++, dst); + dst += size; + if(max(lw, lh) <= 1) break; + if(lw > 1) lw /= 2; + if(lh > 1) lh /= 2; + } + + lilswap((uint *)&d, sizeof(d)/sizeof(uint)); + + f->write("DDS ", 4); + f->write(&d, sizeof(d)); + f->write(data, csize); + delete f; + + delete[] data; + + conoutf("wrote DDS file %s", outfile); + + setuptexcompress(); +} +COMMAND(gendds, "ss"); + +void writepngchunk(stream *f, const char *type, uchar *data = NULL, uint len = 0) +{ + f->putbig(len); + f->write(type, 4); + f->write(data, len); + + uint crc = crc32(0, Z_NULL, 0); + crc = crc32(crc, (const Bytef *)type, 4); + if(data) crc = crc32(crc, data, len); + f->putbig(crc); +} + +VARP(compresspng, 0, 9, 9); + +void savepng(const char *filename, ImageData &image, bool flip) +{ + uchar ctype = 0; + switch(image.bpp) + { + case 1: ctype = 0; break; + case 2: ctype = 4; break; + case 3: ctype = 2; break; + case 4: ctype = 6; break; + default: conoutf(CON_ERROR, "failed saving png to %s", filename); return; + } + stream *f = openfile(filename, "wb"); + if(!f) { conoutf(CON_ERROR, "could not write to %s", filename); return; } + + uchar signature[] = { 137, 80, 78, 71, 13, 10, 26, 10 }; + f->write(signature, sizeof(signature)); + + struct pngihdr + { + uint width, height; + uchar bitdepth, colortype, compress, filter, interlace; + } ihdr = { bigswap(image.w), bigswap(image.h), 8, ctype, 0, 0, 0 }; + writepngchunk(f, "IHDR", (uchar *)&ihdr, 13); + + stream::offset idat = f->tell(); + uint len = 0; + f->write("\0\0\0\0IDAT", 8); + uint crc = crc32(0, Z_NULL, 0); + crc = crc32(crc, (const Bytef *)"IDAT", 4); + + z_stream z; + z.zalloc = NULL; + z.zfree = NULL; + z.opaque = NULL; + + if(deflateInit(&z, compresspng) != Z_OK) + goto error; + + uchar buf[1<<12]; + z.next_out = (Bytef *)buf; + z.avail_out = sizeof(buf); + + loopi(image.h) + { + uchar filter = 0; + loopj(2) + { + z.next_in = j ? (Bytef *)image.data + (flip ? image.h-i-1 : i)*image.pitch : (Bytef *)&filter; + z.avail_in = j ? image.w*image.bpp : 1; + while(z.avail_in > 0) + { + if(deflate(&z, Z_NO_FLUSH) != Z_OK) goto cleanuperror; + #define FLUSHZ do { \ + int flush = sizeof(buf) - z.avail_out; \ + crc = crc32(crc, buf, flush); \ + len += flush; \ + f->write(buf, flush); \ + z.next_out = (Bytef *)buf; \ + z.avail_out = sizeof(buf); \ + } while(0) + FLUSHZ; + } + } + } + + for(;;) + { + int err = deflate(&z, Z_FINISH); + if(err != Z_OK && err != Z_STREAM_END) goto cleanuperror; + FLUSHZ; + if(err == Z_STREAM_END) break; + } + + deflateEnd(&z); + + f->seek(idat, SEEK_SET); + f->putbig(len); + f->seek(0, SEEK_END); + f->putbig(crc); + + writepngchunk(f, "IEND"); + + delete f; + return; + +cleanuperror: + deflateEnd(&z); + +error: + delete f; + + conoutf(CON_ERROR, "failed saving png to %s", filename); +} + +struct tgaheader +{ + uchar identsize; + uchar cmaptype; + uchar imagetype; + uchar cmaporigin[2]; + uchar cmapsize[2]; + uchar cmapentrysize; + uchar xorigin[2]; + uchar yorigin[2]; + uchar width[2]; + uchar height[2]; + uchar pixelsize; + uchar descbyte; +}; + +VARP(compresstga, 0, 1, 1); + +void savetga(const char *filename, ImageData &image, bool flip) +{ + switch(image.bpp) + { + case 3: case 4: break; + default: conoutf(CON_ERROR, "failed saving tga to %s", filename); return; + } + + stream *f = openfile(filename, "wb"); + if(!f) { conoutf(CON_ERROR, "could not write to %s", filename); return; } + + tgaheader hdr; + memset(&hdr, 0, sizeof(hdr)); + hdr.pixelsize = image.bpp*8; + hdr.width[0] = image.w&0xFF; + hdr.width[1] = (image.w>>8)&0xFF; + hdr.height[0] = image.h&0xFF; + hdr.height[1] = (image.h>>8)&0xFF; + hdr.imagetype = compresstga ? 10 : 2; + f->write(&hdr, sizeof(hdr)); + + uchar buf[128*4]; + loopi(image.h) + { + uchar *src = image.data + (flip ? i : image.h - i - 1)*image.pitch; + for(int remaining = image.w; remaining > 0;) + { + int raw = 1; + if(compresstga) + { + int run = 1; + for(uchar *scan = src; run < min(remaining, 128); run++) + { + scan += image.bpp; + if(src[0]!=scan[0] || src[1]!=scan[1] || src[2]!=scan[2] || (image.bpp==4 && src[3]!=scan[3])) break; + } + if(run > 1) + { + f->putchar(0x80 | (run-1)); + f->putchar(src[2]); f->putchar(src[1]); f->putchar(src[0]); + if(image.bpp==4) f->putchar(src[3]); + src += run*image.bpp; + remaining -= run; + if(remaining <= 0) break; + } + for(uchar *scan = src; raw < min(remaining, 128); raw++) + { + scan += image.bpp; + if(src[0]==scan[0] && src[1]==scan[1] && src[2]==scan[2] && (image.bpp!=4 || src[3]==scan[3])) break; + } + f->putchar(raw - 1); + } + else raw = min(remaining, 128); + uchar *dst = buf; + loopj(raw) + { + dst[0] = src[2]; + dst[1] = src[1]; + dst[2] = src[0]; + if(image.bpp==4) dst[3] = src[3]; + dst += image.bpp; + src += image.bpp; + } + f->write(buf, raw*image.bpp); + remaining -= raw; + } + } + + delete f; +} + +enum +{ + IMG_BMP = 0, + IMG_TGA = 1, + IMG_PNG = 2, + IMG_JPG = 3, + NUMIMG +}; + +VARP(screenshotquality, 0, 97, 100); +VARP(screenshotformat, 0, IMG_PNG, NUMIMG-1); + +const char *imageexts[NUMIMG] = { ".bmp", ".tga", ".png", ".jpg" }; + +int guessimageformat(const char *filename, int format = IMG_BMP) +{ + int len = strlen(filename); + loopi(NUMIMG) + { + int extlen = strlen(imageexts[i]); + if(len >= extlen && !strcasecmp(&filename[len-extlen], imageexts[i])) return i; + } + return format; +} + +void saveimage(const char *filename, int format, ImageData &image, bool flip = false) +{ + switch(format) + { + case IMG_PNG: savepng(filename, image, flip); break; + case IMG_TGA: savetga(filename, image, flip); break; + default: + { + ImageData flipped(image.w, image.h, image.bpp, image.data); + if(flip) texflip(flipped); + SDL_Surface *s = wrapsurface(flipped.data, flipped.w, flipped.h, flipped.bpp); + if(!s) break; + stream *f = openfile(filename, "wb"); + if(f) + { + switch(format) { + case IMG_JPG: +#if SDL_IMAGE_VERSION_ATLEAST(2, 0, 2) + IMG_SaveJPG_RW(s, f->rwops(), 1, screenshotquality); +#else + conoutf(CON_ERROR, "JPG screenshot support requires SDL_image 2.0.2"); +#endif + break; + default: SDL_SaveBMP_RW(s, f->rwops(), 1); break; + } + delete f; + } + SDL_FreeSurface(s); + break; + } + } +} + +bool loadimage(const char *filename, ImageData &image) +{ + SDL_Surface *s = loadsurface(path(filename, true)); + if(!s) return false; + image.wrap(s); + return true; +} + +SVARP(screenshotdir, "screenshot"); + +void screenshot(char *filename) +{ + static string buf; + int format = -1, dirlen = 0; + copystring(buf, screenshotdir); + if(screenshotdir[0]) + { + dirlen = strlen(buf); + if(buf[dirlen] != '/' && buf[dirlen] != '\\' && dirlen+1 < (int)sizeof(buf)) { buf[dirlen++] = '/'; buf[dirlen] = '\0'; } + const char *dir = findfile(buf, "w"); + if(!fileexists(dir, "w")) createdir(dir); + } + if(filename[0]) + { + concatstring(buf, filename); + format = guessimageformat(buf, -1); + } + else + { + string sstime; + time_t t = time(NULL); + size_t len = strftime(sstime, sizeof(sstime), "%Y-%m-%d_%H.%M.%S", localtime(&t)); + sstime[min(len, sizeof(sstime)-1)] = '\0'; + concatstring(buf, sstime); + + const char *map = game::getclientmap(), *ssinfo = game::getscreenshotinfo(); + if(map && map[0]) + { + concatstring(buf, "_"); + concatstring(buf, map); + } + if(ssinfo && ssinfo[0]) + { + concatstring(buf, "_"); + concatstring(buf, ssinfo); + } + + for(char *s = &buf[dirlen]; *s; s++) if(iscubespace(*s) || *s == '/' || *s == '\\') *s = '-'; + } + if(format < 0) + { + format = screenshotformat; + concatstring(buf, imageexts[format]); + } + + ImageData image(screenw, screenh, 3); + glPixelStorei(GL_PACK_ALIGNMENT, texalign(image.data, screenw, 3)); + glReadPixels(0, 0, screenw, screenh, GL_RGB, GL_UNSIGNED_BYTE, image.data); + saveimage(path(buf), format, image, true); +} + +COMMAND(screenshot, "s"); + +void flipnormalmapy(char *destfile, char *normalfile) // jpg/png /tga-> tga +{ + ImageData ns; + if(!loadimage(normalfile, ns)) return; + ImageData d(ns.w, ns.h, 3); + readwritetex(d, ns, + dst[0] = src[0]; + dst[1] = 255 - src[1]; + dst[2] = src[2]; + ); + saveimage(destfile, guessimageformat(destfile, IMG_TGA), d); +} + +void mergenormalmaps(char *heightfile, char *normalfile) // jpg/png/tga + tga -> tga +{ + ImageData hs, ns; + if(!loadimage(heightfile, hs) || !loadimage(normalfile, ns) || hs.w != ns.w || hs.h != ns.h) return; + ImageData d(ns.w, ns.h, 3); + read2writetex(d, hs, srch, ns, srcn, + *(bvec *)dst = bvec(((bvec *)srcn)->tonormal().mul(2).add(((bvec *)srch)->tonormal()).normalize()); + ); + saveimage(normalfile, guessimageformat(normalfile, IMG_TGA), d); +} + +COMMAND(flipnormalmapy, "ss"); +COMMAND(mergenormalmaps, "ss"); + diff --git a/src/engine/texture.h b/src/engine/texture.h new file mode 100644 index 0000000..17bce5f --- /dev/null +++ b/src/engine/texture.h @@ -0,0 +1,779 @@ +struct GlobalShaderParamState +{ + const char *name; + union + { + float fval[32]; + int ival[32]; + uint uval[32]; + uchar buf[32*sizeof(float)]; + }; + int version; + + static int nextversion; + + void resetversions(); + + void changed() + { + if(++nextversion < 0) resetversions(); + version = nextversion; + } +}; + +struct ShaderParamBinding +{ + int loc, size; + GLenum format; +}; + +struct GlobalShaderParamUse : ShaderParamBinding +{ + + GlobalShaderParamState *param; + int version; + + void flush() + { + if(version == param->version) return; + switch(format) + { + case GL_BOOL: + case GL_FLOAT: glUniform1fv_(loc, size, param->fval); break; + case GL_BOOL_VEC2: + case GL_FLOAT_VEC2: glUniform2fv_(loc, size, param->fval); break; + case GL_BOOL_VEC3: + case GL_FLOAT_VEC3: glUniform3fv_(loc, size, param->fval); break; + case GL_BOOL_VEC4: + case GL_FLOAT_VEC4: glUniform4fv_(loc, size, param->fval); break; + case GL_INT: glUniform1iv_(loc, size, param->ival); break; + case GL_INT_VEC2: glUniform2iv_(loc, size, param->ival); break; + case GL_INT_VEC3: glUniform3iv_(loc, size, param->ival); break; + case GL_INT_VEC4: glUniform4iv_(loc, size, param->ival); break; + case GL_FLOAT_MAT2: glUniformMatrix2fv_(loc, 1, GL_FALSE, param->fval); break; + case GL_FLOAT_MAT3: glUniformMatrix3fv_(loc, 1, GL_FALSE, param->fval); break; + case GL_FLOAT_MAT4: glUniformMatrix4fv_(loc, 1, GL_FALSE, param->fval); break; + } + version = param->version; + } +}; + +struct LocalShaderParamState : ShaderParamBinding +{ + const char *name; +}; + +struct SlotShaderParam +{ + const char *name; + int loc; + float val[4]; +}; + +struct SlotShaderParamState : LocalShaderParamState +{ + float val[4]; + + SlotShaderParamState() {} + SlotShaderParamState(const SlotShaderParam &p) + { + name = p.name; + loc = -1; + size = 1; + format = GL_FLOAT_VEC4; + memcpy(val, p.val, sizeof(val)); + } +}; + +enum +{ + SHADER_DEFAULT = 0, + SHADER_NORMALSLMS = 1<<0, + SHADER_ENVMAP = 1<<1, + SHADER_OPTION = 1<<3, + + SHADER_INVALID = 1<<8, + SHADER_DEFERRED = 1<<9 +}; + +#define MAXSHADERDETAIL 3 +#define MAXVARIANTROWS 5 + +extern int shaderdetail; + +struct Slot; +struct VSlot; + +struct UniformLoc +{ + const char *name, *blockname; + int loc, version, binding, stride, offset, size; + void *data; + UniformLoc(const char *name = NULL, const char *blockname = NULL, int binding = -1, int stride = -1) : name(name), blockname(blockname), loc(-1), version(-1), binding(binding), stride(stride), offset(-1), size(-1), data(NULL) {} +}; + +struct AttribLoc +{ + const char *name; + int loc; + AttribLoc(const char *name = NULL, int loc = -1) : name(name), loc(loc) {} +}; + +struct Shader +{ + static Shader *lastshader; + + char *name, *vsstr, *psstr, *defer; + int type; + GLuint program, vsobj, psobj; + vector defaultparams; + vector globalparams; + vector localparams; + vector localparamremap; + Shader *detailshader, *variantshader, *altshader, *fastshader[MAXSHADERDETAIL]; + vector variants; + ushort *variantrows; + bool standard, forced, used; + Shader *reusevs, *reuseps; + vector uniformlocs; + vector attriblocs; + const void *owner; + + Shader() : name(NULL), vsstr(NULL), psstr(NULL), defer(NULL), type(SHADER_DEFAULT), program(0), vsobj(0), psobj(0), detailshader(NULL), variantshader(NULL), altshader(NULL), variantrows(NULL), standard(false), forced(false), used(false), reusevs(NULL), reuseps(NULL), owner(NULL) + { + loopi(MAXSHADERDETAIL) fastshader[i] = this; + } + + ~Shader() + { + DELETEA(name); + DELETEA(vsstr); + DELETEA(psstr); + DELETEA(defer); + DELETEA(variantrows); + } + + void fixdetailshader(bool force = true, bool recurse = true); + void allocparams(Slot *slot = NULL); + void setslotparams(Slot &slot); + void setslotparams(Slot &slot, VSlot &vslot); + void bindprograms(); + + void flushparams(Slot *slot = NULL) + { + if(!used) { allocparams(slot); used = true; } + loopv(globalparams) globalparams[i].flush(); + } + + void force(); + + bool invalid() const { return (type&SHADER_INVALID)!=0; } + bool deferred() const { return (type&SHADER_DEFERRED)!=0; } + bool loaded() const { return detailshader!=NULL; } + + static bool isnull(const Shader *s); + + bool isnull() const { return isnull(this); } + + int numvariants(int row) const + { + if(row < 0 || row >= MAXVARIANTROWS || !variantrows) return 0; + return variantrows[row+1] - variantrows[row]; + } + + Shader *getvariant(int col, int row) const + { + if(row < 0 || row >= MAXVARIANTROWS || col < 0 || !variantrows) return NULL; + int start = variantrows[row], end = variantrows[row+1]; + return col < end - start ? variants[start + col] : NULL; + } + + bool hasoption(int row) + { + if(detailshader) + { + Shader *s = getvariant(0, row); + if(s) return (s->type&SHADER_OPTION)!=0; + } + return false; + } + + void addvariant(int row, Shader *s) + { + if(row < 0 || row >= MAXVARIANTROWS || variants.length() >= USHRT_MAX) return; + if(!variantrows) { variantrows = new ushort[MAXVARIANTROWS+1]; memset(variantrows, 0, (MAXVARIANTROWS+1)*sizeof(ushort)); } + variants.insert(variantrows[row+1], s); + for(int i = row+1; i <= MAXVARIANTROWS; ++i) ++variantrows[i]; + } + + void setvariant_(int col, int row, Shader *fallbackshader) + { + Shader *s = fallbackshader; + if(detailshader->variantrows) + { + int start = detailshader->variantrows[row], end = detailshader->variantrows[row+1]; + for(col = min(start + col, end-1); col >= start; --col) + { + if(!detailshader->variants[col]->invalid()) { s = detailshader->variants[col]; break; } + } + } + if(lastshader!=s) s->bindprograms(); + } + + void setvariant(int col, int row, Shader *fallbackshader) + { + if(isnull() || !loaded()) return; + setvariant_(col, row, fallbackshader); + lastshader->flushparams(); + } + + void setvariant(int col, int row) + { + if(isnull() || !loaded()) return; + setvariant_(col, row, detailshader); + lastshader->flushparams(); + } + + void setvariant(int col, int row, Slot &slot, VSlot &vslot, Shader *fallbackshader) + { + if(isnull() || !loaded()) return; + setvariant_(col, row, fallbackshader); + lastshader->flushparams(&slot); + lastshader->setslotparams(slot, vslot); + } + + void setvariant(int col, int row, Slot &slot, VSlot &vslot) + { + if(isnull() || !loaded()) return; + setvariant_(col, row, detailshader); + lastshader->flushparams(&slot); + lastshader->setslotparams(slot, vslot); + } + + void set_() + { + if(lastshader!=detailshader) detailshader->bindprograms(); + } + + void set() + { + if(isnull() || !loaded()) return; + set_(); + lastshader->flushparams(); + } + + void set(Slot &slot, VSlot &vslot) + { + if(isnull() || !loaded()) return; + set_(); + lastshader->flushparams(&slot); + lastshader->setslotparams(slot, vslot); + } + + bool compile(); + void cleanup(bool invalid = false); + + static int uniformlocversion(); +}; + +struct GlobalShaderParam +{ + const char *name; + GlobalShaderParamState *param; + + GlobalShaderParam(const char *name) : name(name), param(NULL) {} + + GlobalShaderParamState *resolve() + { + extern GlobalShaderParamState *getglobalparam(const char *name); + if(!param) param = getglobalparam(name); + param->changed(); + return param; + } + + void setf(float x = 0, float y = 0, float z = 0, float w = 0) + { + GlobalShaderParamState *g = resolve(); + g->fval[0] = x; + g->fval[1] = y; + g->fval[2] = z; + g->fval[3] = w; + } + void set(const vec &v, float w = 0) { setf(v.x, v.y, v.z, w); } + void set(const vec2 &v, float z = 0, float w = 0) { setf(v.x, v.y, z, w); } + void set(const vec4 &v) { setf(v.x, v.y, v.z, v.w); } + void set(const plane &p) { setf(p.x, p.y, p.z, p.offset); } + void set(const matrix2 &m) { memcpy(resolve()->fval, m.a.v, sizeof(m)); } + void set(const matrix3 &m) { memcpy(resolve()->fval, m.a.v, sizeof(m)); } + void set(const matrix4 &m) { memcpy(resolve()->fval, m.a.v, sizeof(m)); } + + template + void setv(const T *v, int n = 1) { memcpy(resolve()->buf, v, n*sizeof(T)); } + + void seti(int x = 0, int y = 0, int z = 0, int w = 0) + { + GlobalShaderParamState *g = resolve(); + g->ival[0] = x; + g->ival[1] = y; + g->ival[2] = z; + g->ival[3] = w; + } + void set(const ivec &v, int w = 0) { seti(v.x, v.y, v.z, w); } + void set(const ivec2 &v, int z = 0, int w = 0) { seti(v.x, v.y, z, w); } + void set(const ivec4 &v) { seti(v.x, v.y, v.z, v.w); } + + void setu(uint x = 0, uint y = 0, uint z = 0, uint w = 0) + { + GlobalShaderParamState *g = resolve(); + g->uval[0] = x; + g->uval[1] = y; + g->uval[2] = z; + g->uval[3] = w; + } + + template + T *reserve(int n = 1) { return (T *)resolve()->buf; } +}; + +struct LocalShaderParam +{ + const char *name; + int loc; + + LocalShaderParam(const char *name) : name(name), loc(-1) {} + + LocalShaderParamState *resolve() + { + Shader *s = Shader::lastshader; + if(!s) return NULL; + if(!s->localparamremap.inrange(loc)) + { + extern int getlocalparam(const char *name); + if(loc == -1) loc = getlocalparam(name); + if(!s->localparamremap.inrange(loc)) return NULL; + } + uchar remap = s->localparamremap[loc]; + return s->localparams.inrange(remap) ? &s->localparams[remap] : NULL; + } + + void setf(float x = 0, float y = 0, float z = 0, float w = 0) + { + ShaderParamBinding *b = resolve(); + if(b) switch(b->format) + { + case GL_BOOL: + case GL_FLOAT: glUniform1f_(b->loc, x); break; + case GL_BOOL_VEC2: + case GL_FLOAT_VEC2: glUniform2f_(b->loc, x, y); break; + case GL_BOOL_VEC3: + case GL_FLOAT_VEC3: glUniform3f_(b->loc, x, y, z); break; + case GL_BOOL_VEC4: + case GL_FLOAT_VEC4: glUniform4f_(b->loc, x, y, z, w); break; + case GL_INT: glUniform1i_(b->loc, int(x)); break; + case GL_INT_VEC2: glUniform2i_(b->loc, int(x), int(y)); break; + case GL_INT_VEC3: glUniform3i_(b->loc, int(x), int(y), int(z)); break; + case GL_INT_VEC4: glUniform4i_(b->loc, int(x), int(y), int(z), int(w)); break; + } + } + void set(const vec &v, float w = 0) { setf(v.x, v.y, v.z, w); } + void set(const vec2 &v, float z = 0, float w = 0) { setf(v.x, v.y, z, w); } + void set(const vec4 &v) { setf(v.x, v.y, v.z, v.w); } + void set(const plane &p) { setf(p.x, p.y, p.z, p.offset); } + void setv(const float *f, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniform1fv_(b->loc, n, f); } + void setv(const vec *v, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniform3fv_(b->loc, n, v->v); } + void setv(const vec2 *v, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniform2fv_(b->loc, n, v->v); } + void setv(const vec4 *v, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniform4fv_(b->loc, n, v->v); } + void setv(const plane *p, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniform4fv_(b->loc, n, p->v); } + void setv(const matrix2 *m, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniformMatrix2fv_(b->loc, n, GL_FALSE, m->a.v); } + void setv(const matrix3 *m, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniformMatrix3fv_(b->loc, n, GL_FALSE, m->a.v); } + void setv(const matrix4 *m, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniformMatrix4fv_(b->loc, n, GL_FALSE, m->a.v); } + void set(const matrix2 &m) { setv(&m); } + void set(const matrix3 &m) { setv(&m); } + void set(const matrix4 &m) { setv(&m); } + + template + void sett(T x, T y, T z, T w) + { + ShaderParamBinding *b = resolve(); + if(b) switch(b->format) + { + case GL_FLOAT: glUniform1f_(b->loc, x); break; + case GL_FLOAT_VEC2: glUniform2f_(b->loc, x, y); break; + case GL_FLOAT_VEC3: glUniform3f_(b->loc, x, y, z); break; + case GL_FLOAT_VEC4: glUniform4f_(b->loc, x, y, z, w); break; + case GL_BOOL: + case GL_INT: glUniform1i_(b->loc, x); break; + case GL_BOOL_VEC2: + case GL_INT_VEC2: glUniform2i_(b->loc, x, y); break; + case GL_BOOL_VEC3: + case GL_INT_VEC3: glUniform3i_(b->loc, x, y, z); break; + case GL_BOOL_VEC4: + case GL_INT_VEC4: glUniform4i_(b->loc, x, y, z, w); break; + } + } + void seti(int x = 0, int y = 0, int z = 0, int w = 0) { sett(x, y, z, w); } + void set(const ivec &v, int w = 0) { seti(v.x, v.y, v.z, w); } + void set(const ivec2 &v, int z = 0, int w = 0) { seti(v.x, v.y, z, w); } + void set(const ivec4 &v) { seti(v.x, v.y, v.z, v.w); } + void setv(const int *i, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniform1iv_(b->loc, n, i); } + void setv(const ivec *v, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniform3iv_(b->loc, n, v->v); } + void setv(const ivec2 *v, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniform2iv_(b->loc, n, v->v); } + void setv(const ivec4 *v, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniform4iv_(b->loc, n, v->v); } +}; + +#define LOCALPARAM(name, vals) do { static LocalShaderParam param( #name ); param.set(vals); } while(0) +#define LOCALPARAMF(name, ...) do { static LocalShaderParam param( #name ); param.setf(__VA_ARGS__); } while(0) +#define LOCALPARAMI(name, ...) do { static LocalShaderParam param( #name ); param.seti(__VA_ARGS__); } while(0) +#define LOCALPARAMV(name, vals, num) do { static LocalShaderParam param( #name ); param.setv(vals, num); } while(0) +#define GLOBALPARAM(name, vals) do { static GlobalShaderParam param( #name ); param.set(vals); } while(0) +#define GLOBALPARAMF(name, ...) do { static GlobalShaderParam param( #name ); param.setf(__VA_ARGS__); } while(0) +#define GLOBALPARAMI(name, ...) do { static GlobalShaderParam param( #name ); param.seti(__VA_ARGS__); } while(0) +#define GLOBALPARAMV(name, vals, num) do { static GlobalShaderParam param( #name ); param.setv(vals, num); } while(0) + +#define SETSHADER(name, ...) \ + do { \ + static Shader *name##shader = NULL; \ + if(!name##shader) name##shader = lookupshaderbyname(#name); \ + name##shader->set(__VA_ARGS__); \ + } while(0) +#define SETVARIANT(name, ...) \ + do { \ + static Shader *name##shader = NULL; \ + if(!name##shader) name##shader = lookupshaderbyname(#name); \ + name##shader->setvariant(__VA_ARGS__); \ + } while(0) + +struct ImageData +{ + int w, h, bpp, levels, align, pitch; + GLenum compressed; + uchar *data; + void *owner; + void (*freefunc)(void *); + + ImageData() + : data(NULL), owner(NULL), freefunc(NULL) + {} + + + ImageData(int nw, int nh, int nbpp, int nlevels = 1, int nalign = 0, GLenum ncompressed = GL_FALSE) + { + setdata(NULL, nw, nh, nbpp, nlevels, nalign, ncompressed); + } + + ImageData(int nw, int nh, int nbpp, uchar *data) + : owner(NULL), freefunc(NULL) + { + setdata(data, nw, nh, nbpp); + } + + ImageData(SDL_Surface *s) { wrap(s); } + ~ImageData() { cleanup(); } + + void setdata(uchar *ndata, int nw, int nh, int nbpp, int nlevels = 1, int nalign = 0, GLenum ncompressed = GL_FALSE) + { + w = nw; + h = nh; + bpp = nbpp; + levels = nlevels; + align = nalign; + pitch = align ? 0 : w*bpp; + compressed = ncompressed; + data = ndata ? ndata : new uchar[calcsize()]; + if(!ndata) { owner = this; freefunc = NULL; } + } + + int calclevelsize(int level) const { return ((max(w>>level, 1)+align-1)/align)*((max(h>>level, 1)+align-1)/align)*bpp; } + + int calcsize() const + { + if(!align) return w*h*bpp; + int lw = w, lh = h, + size = 0; + loopi(levels) + { + if(lw<=0) lw = 1; + if(lh<=0) lh = 1; + size += ((lw+align-1)/align)*((lh+align-1)/align)*bpp; + if(lw*lh==1) break; + lw >>= 1; + lh >>= 1; + } + return size; + } + + void disown() + { + data = NULL; + owner = NULL; + freefunc = NULL; + } + + void cleanup() + { + if(owner==this) delete[] data; + else if(freefunc) (*freefunc)(owner); + disown(); + } + + void replace(ImageData &d) + { + cleanup(); + *this = d; + if(owner == &d) owner = this; + d.disown(); + } + + void wrap(SDL_Surface *s) + { + setdata((uchar *)s->pixels, s->w, s->h, s->format->BytesPerPixel); + pitch = s->pitch; + owner = s; + freefunc = (void (*)(void *))SDL_FreeSurface; + } +}; + +// management of texture slots +// each texture slot can have multiple texture frames, of which currently only the first is used +// additional frames can be used for various shaders + +struct Texture +{ + enum + { + IMAGE = 0, + CUBEMAP = 1, + TYPE = 0xFF, + + STUB = 1<<8, + TRANSIENT = 1<<9, + COMPRESSED = 1<<10, + ALPHA = 1<<11, + MIRROR = 1<<12, + FLAGS = 0xFF00 + }; + + char *name; + int type, w, h, xs, ys, bpp, clamp; + bool mipmap, canreduce; + GLuint id; + uchar *alphamask; + + Texture() : alphamask(NULL) {} +}; + +enum +{ + TEX_DIFFUSE = 0, + TEX_UNKNOWN, + TEX_DECAL, + TEX_NORMAL, + TEX_GLOW, + TEX_SPEC, + TEX_DEPTH, + TEX_ALPHA, + TEX_ENVMAP +}; + +enum +{ + VSLOT_SHPARAM = 0, + VSLOT_SCALE, + VSLOT_ROTATION, + VSLOT_OFFSET, + VSLOT_SCROLL, + VSLOT_LAYER, + VSLOT_ALPHA, + VSLOT_COLOR, + VSLOT_NUM +}; + +struct VSlot +{ + Slot *slot; + VSlot *next; + int index, changed; + vector params; + bool linked; + float scale; + int rotation; + ivec2 offset; + vec2 scroll; + int layer; + float alphafront, alphaback; + vec colorscale; + vec glowcolor; + + VSlot(Slot *slot = NULL, int index = -1) : slot(slot), next(NULL), index(index), changed(0) + { + reset(); + if(slot) addvariant(slot); + } + + void addvariant(Slot *slot); + + void reset() + { + params.shrink(0); + linked = false; + scale = 1; + rotation = 0; + offset = ivec2(0, 0); + scroll = vec2(0, 0); + layer = 0; + alphafront = 0.5f; + alphaback = 0; + colorscale = vec(1, 1, 1); + glowcolor = vec(1, 1, 1); + } + + void cleanup() + { + linked = false; + } +}; + +struct Slot +{ + struct Tex + { + int type; + Texture *t; + string name; + int combined; + }; + + int index; + vector sts; + Shader *shader; + vector params; + VSlot *variants; + bool loaded; + uint texmask; + char *autograss; + Texture *grasstex, *thumbnail; + char *layermaskname; + int layermaskmode; + float layermaskscale; + ImageData *layermask; + + Slot(int index = -1) : index(index), variants(NULL), autograss(NULL), layermaskname(NULL), layermask(NULL) { reset(); } + + void reset() + { + sts.shrink(0); + shader = NULL; + params.shrink(0); + loaded = false; + texmask = 0; + DELETEA(autograss); + grasstex = NULL; + thumbnail = NULL; + DELETEA(layermaskname); + layermaskmode = 0; + layermaskscale = 1; + if(layermask) DELETEP(layermask); + } + + void cleanup() + { + loaded = false; + grasstex = NULL; + thumbnail = NULL; + loopv(sts) + { + Tex &t = sts[i]; + t.t = NULL; + t.combined = -1; + } + } +}; + +inline void VSlot::addvariant(Slot *slot) +{ + if(!slot->variants) slot->variants = this; + else + { + VSlot *prev = slot->variants; + while(prev->next) prev = prev->next; + prev->next = this; + } +} + +struct MSlot : Slot, VSlot +{ + MSlot() : VSlot(this) {} + + void reset() + { + Slot::reset(); + VSlot::reset(); + } + + void cleanup() + { + Slot::cleanup(); + VSlot::cleanup(); + } +}; + +struct texrotation +{ + bool flipx, flipy, swapxy; +}; + +struct cubemapside +{ + GLenum target; + const char *name; + bool flipx, flipy, swapxy; +}; + +extern const texrotation texrotations[8]; +extern const cubemapside cubemapsides[6]; +extern Texture *notexture; +extern Shader *nullshader, *hudshader, *hudnotextureshader, *textureshader, *notextureshader, *nocolorshader, *foggedshader, *foggednotextureshader, *stdworldshader; +extern int reservevpparams, maxvsuniforms, maxfsuniforms; + +extern Shader *lookupshaderbyname(const char *name); +extern Shader *useshaderbyname(const char *name); +extern Shader *generateshader(const char *name, const char *cmd, ...); +extern Texture *loadthumbnail(Slot &slot); +extern void resetslotshader(); +extern void setslotshader(Slot &s); +extern void linkslotshader(Slot &s, bool load = true); +extern void linkvslotshader(VSlot &s, bool load = true); +extern void linkslotshaders(); +extern const char *getshaderparamname(const char *name, bool insert = true); +extern void setupshaders(); +extern void reloadshaders(); +extern void cleanupshaders(); + +#define MAXDYNLIGHTS 5 +#define DYNLIGHTBITS 6 +#define DYNLIGHTMASK ((1< &buf, const VSlot &src); +extern bool unpackvslot(ucharbuf &buf, VSlot &dst, bool delta); + +extern Slot dummyslot; +extern VSlot dummyvslot; +extern vector slots; +extern vector vslots; + diff --git a/src/engine/vertmodel.h b/src/engine/vertmodel.h new file mode 100644 index 0000000..eb09001 --- /dev/null +++ b/src/engine/vertmodel.h @@ -0,0 +1,490 @@ +struct vertmodel : animmodel +{ + struct vert { vec pos, norm; }; + struct vvert { vec pos; vec2 tc; }; + struct vvertn : vvert { vec norm; }; + struct vvertbump : vvert { squat tangent; }; + struct tcvert { vec2 tc; }; + struct bumpvert { vec4 tangent; }; + struct tri { ushort vert[3]; }; + + struct vbocacheentry + { + GLuint vbuf; + animstate as; + int millis; + + vbocacheentry() : vbuf(0) { as.cur.fr1 = as.prev.fr1 = -1; } + }; + + struct vertmesh : mesh + { + vert *verts; + tcvert *tcverts; + bumpvert *bumpverts; + tri *tris; + int numverts, numtris; + + int voffset, eoffset, elen; + ushort minvert, maxvert; + + vertmesh() : verts(0), tcverts(0), bumpverts(0), tris(0) + { + } + + virtual ~vertmesh() + { + DELETEA(verts); + DELETEA(tcverts); + DELETEA(bumpverts); + DELETEA(tris); + } + + void smoothnorms(float limit = 0, bool areaweight = true) + { + if(((vertmeshgroup *)group)->numframes == 1) mesh::smoothnorms(verts, numverts, tris, numtris, limit, areaweight); + else buildnorms(areaweight); + } + + void buildnorms(bool areaweight = true) + { + mesh::buildnorms(verts, numverts, tris, numtris, areaweight, ((vertmeshgroup *)group)->numframes); + } + + void calctangents(bool areaweight = true) + { + if(bumpverts) return; + bumpverts = new bumpvert[((vertmeshgroup *)group)->numframes*numverts]; + mesh::calctangents(bumpverts, verts, tcverts, numverts, tris, numtris, areaweight, ((vertmeshgroup *)group)->numframes); + } + + void calcbb(vec &bbmin, vec &bbmax, const matrix4x3 &m) + { + loopj(numverts) + { + vec v = m.transform(verts[j].pos); + loopi(3) + { + bbmin[i] = min(bbmin[i], v[i]); + bbmax[i] = max(bbmax[i], v[i]); + } + } + } + + void genBIH(BIH::mesh &m) + { + m.tris = (const BIH::tri *)tris; + m.numtris = numtris; + m.pos = (const uchar *)&verts->pos; + m.posstride = sizeof(vert); + m.tc = (const uchar *)&tcverts->tc; + m.tcstride = sizeof(tcvert); + } + + static inline void assignvert(vvertn &vv, int j, tcvert &tc, vert &v) + { + vv.pos = v.pos; + vv.norm = v.norm; + vv.tc = tc.tc; + } + + inline void assignvert(vvertbump &vv, int j, tcvert &tc, vert &v) + { + vv.pos = v.pos; + vv.tc = tc.tc; + vv.tangent = bumpverts[j].tangent; + } + + template + int genvbo(vector &idxs, int offset, vector &vverts, int *htdata, int htlen) + { + voffset = offset; + eoffset = idxs.length(); + minvert = 0xFFFF; + loopi(numtris) + { + tri &t = tris[i]; + loopj(3) + { + int index = t.vert[j]; + tcvert &tc = tcverts[index]; + vert &v = verts[index]; + T vv; + assignvert(vv, index, tc, v); + int htidx = hthash(v.pos)&(htlen-1); + loopk(htlen) + { + int &vidx = htdata[(htidx+k)&(htlen-1)]; + if(vidx < 0) { vidx = idxs.add(ushort(vverts.length())); vverts.add(vv); break; } + else if(!memcmp(&vverts[vidx], &vv, sizeof(vv))) { minvert = min(minvert, idxs.add(ushort(vidx))); break; } + } + } + } + minvert = min(minvert, ushort(voffset)); + maxvert = max(minvert, ushort(vverts.length()-1)); + elen = idxs.length()-eoffset; + return vverts.length()-voffset; + } + + int genvbo(vector &idxs, int offset) + { + voffset = offset; + eoffset = idxs.length(); + loopi(numtris) + { + tri &t = tris[i]; + loopj(3) idxs.add(voffset+t.vert[j]); + } + minvert = voffset; + maxvert = voffset + numverts-1; + elen = idxs.length()-eoffset; + return numverts; + } + + template + static inline void fillvert(T &vv, int j, tcvert &tc, vert &v) + { + vv.tc = tc.tc; + } + + template + void fillverts(T *vdata) + { + vdata += voffset; + loopi(numverts) fillvert(vdata[i], i, tcverts[i], verts[i]); + } + + void interpverts(const animstate &as, bool tangents, void * RESTRICT vdata, skin &s) + { + const vert * RESTRICT vert1 = &verts[as.cur.fr1 * numverts], + * RESTRICT vert2 = &verts[as.cur.fr2 * numverts], + * RESTRICT pvert1 = as.interp<1 ? &verts[as.prev.fr1 * numverts] : NULL, + * RESTRICT pvert2 = as.interp<1 ? &verts[as.prev.fr2 * numverts] : NULL; + #define ipvert(attrib) v.attrib.lerp(vert1[i].attrib, vert2[i].attrib, as.cur.t) + #define ipbvert(attrib, type) v.attrib.lerp(bvert1[i].attrib, bvert2[i].attrib, as.cur.t) + #define ipvertp(attrib) v.attrib.lerp(pvert1[i].attrib, pvert2[i].attrib, as.prev.t).lerp(vec().lerp(vert1[i].attrib, vert2[i].attrib, as.cur.t), as.interp) + #define ipbvertp(attrib, type) v.attrib.lerp(type().lerp(bpvert1[i].attrib, bpvert2[i].attrib, as.prev.t), type().lerp(bvert1[i].attrib, bvert2[i].attrib, as.cur.t), as.interp) + #define iploop(type, body) \ + loopi(numverts) \ + { \ + type &v = ((type * RESTRICT)vdata)[i]; \ + body; \ + } + if(tangents) + { + const bumpvert * RESTRICT bvert1 = &bumpverts[as.cur.fr1 * numverts], + * RESTRICT bvert2 = &bumpverts[as.cur.fr2 * numverts], + * RESTRICT bpvert1 = as.interp<1 ? &bumpverts[as.prev.fr1 * numverts] : NULL, + * RESTRICT bpvert2 = as.interp<1 ? &bumpverts[as.prev.fr2 * numverts] : NULL; + if(as.interp<1) iploop(vvertbump, { ipvertp(pos); ipbvertp(tangent, vec4); }) + else iploop(vvertbump, { ipvert(pos); ipbvert(tangent, vec4); }) + } + else + { + if(as.interp<1) iploop(vvertn, { ipvertp(pos); ipvertp(norm); }) + else iploop(vvertn, { ipvert(pos); ipvert(norm); }) + } + #undef iploop + #undef ipvert + #undef ipbvert + #undef ipvertp + #undef ipbvertp + } + + void render(const animstate *as, skin &s, vbocacheentry &vc) + { + if(!Shader::lastshader) return; + glDrawRangeElements_(GL_TRIANGLES, minvert, maxvert, elen, GL_UNSIGNED_SHORT, &((vertmeshgroup *)group)->edata[eoffset]); + glde++; + xtravertsva += numverts; + } + }; + + struct tag + { + char *name; + matrix4x3 transform; + + tag() : name(NULL) {} + ~tag() { DELETEA(name); } + }; + + struct vertmeshgroup : meshgroup + { + int numframes; + tag *tags; + int numtags; + + static const int MAXVBOCACHE = 16; + vbocacheentry vbocache[MAXVBOCACHE]; + + ushort *edata; + GLuint ebuf; + bool vtangents; + int vlen, vertsize; + uchar *vdata; + + vertmeshgroup() : numframes(0), tags(NULL), numtags(0), edata(NULL), ebuf(0), vtangents(false), vlen(0), vertsize(0), vdata(NULL) + { + } + + virtual ~vertmeshgroup() + { + DELETEA(tags); + if(ebuf) glDeleteBuffers_(1, &ebuf); + loopi(MAXVBOCACHE) + { + if(vbocache[i].vbuf) glDeleteBuffers_(1, &vbocache[i].vbuf); + } + DELETEA(vdata); + } + + int findtag(const char *name) + { + loopi(numtags) if(!strcmp(tags[i].name, name)) return i; + return -1; + } + + int totalframes() const { return numframes; } + + void concattagtransform(part *p, int i, const matrix4x3 &m, matrix4x3 &n) + { + n.mul(m, tags[numtags + i].transform); + n.posttranslate(m.transformnormal(p->translate), p->model->scale); + } + + void calctagmatrix(part *p, int i, const animstate &as, matrix4 &matrix) + { + const matrix4x3 &tag1 = tags[as.cur.fr1*numtags + i].transform, + &tag2 = tags[as.cur.fr2*numtags + i].transform; + matrix4x3 tag; + tag.lerp(tag1, tag2, as.cur.t); + if(as.interp<1) + { + const matrix4x3 &tag1p = tags[as.prev.fr1*numtags + i].transform, + &tag2p = tags[as.prev.fr2*numtags + i].transform; + matrix4x3 tagp; + tagp.lerp(tag1p, tag2p, as.prev.t); + tag.lerp(tagp, tag, as.interp); + } + tag.d.add(p->translate).mul(p->model->scale); + matrix = matrix4(tag); + } + + void genvbo(bool tangents, vbocacheentry &vc) + { + if(!vc.vbuf) glGenBuffers_(1, &vc.vbuf); + if(ebuf) return; + + vector idxs; + + if(tangents) loopv(meshes) ((vertmesh *)meshes[i])->calctangents(); + + vtangents = tangents; + vertsize = tangents ? sizeof(vvertbump) : sizeof(vvertn); + vlen = 0; + if(numframes>1) + { + loopv(meshes) vlen += ((vertmesh *)meshes[i])->genvbo(idxs, vlen); + DELETEA(vdata); + vdata = new uchar[vlen*vertsize]; + #define FILLVDATA(type) do { \ + loopv(meshes) ((vertmesh *)meshes[i])->fillverts((type *)vdata); \ + } while(0) + if(tangents) FILLVDATA(vvertbump); + else FILLVDATA(vvertn); + #undef FILLVDATA + } + else + { + gle::bindvbo(vc.vbuf); + #define GENVBO(type) do { \ + vector vverts; \ + loopv(meshes) vlen += ((vertmesh *)meshes[i])->genvbo(idxs, vlen, vverts, htdata, htlen); \ + glBufferData_(GL_ARRAY_BUFFER, vverts.length()*sizeof(type), vverts.getbuf(), GL_STATIC_DRAW); \ + } while(0) + int numverts = 0, htlen = 128; + loopv(meshes) numverts += ((vertmesh *)meshes[i])->numverts; + while(htlen < numverts) htlen *= 2; + if(numverts*4 > htlen*3) htlen *= 2; + int *htdata = new int[htlen]; + memset(htdata, -1, htlen*sizeof(int)); + if(tangents) GENVBO(vvertbump); + else GENVBO(vvertn); + delete[] htdata; + #undef GENVBO + gle::clearvbo(); + } + + glGenBuffers_(1, &ebuf); + gle::bindebo(ebuf); + glBufferData_(GL_ELEMENT_ARRAY_BUFFER, idxs.length()*sizeof(ushort), idxs.getbuf(), GL_STATIC_DRAW); + gle::clearebo(); + } + + void bindvbo(const animstate *as, vbocacheentry &vc) + { + vvert *vverts = 0; + bindpos(ebuf, vc.vbuf, &vverts->pos, vertsize); + if(as->cur.anim&ANIM_NOSKIN) + { + if(enabletc) disabletc(); + if(enablenormals) disablenormals(); + if(enabletangents) disabletangents(); + } + else + { + if(vtangents) + { + if(enablenormals) disablenormals(); + vvertbump *vvertbumps = 0; + bindtangents(&vvertbumps->tangent, vertsize); + } + else + { + if(enabletangents) disabletangents(); + vvertn *vvertns = 0; + bindnormals(&vvertns->norm, vertsize); + } + + bindtc(&vverts->tc, vertsize); + } + if(enablebones) disablebones(); + } + + void cleanup() + { + loopi(MAXVBOCACHE) + { + vbocacheentry &c = vbocache[i]; + if(c.vbuf) { glDeleteBuffers_(1, &c.vbuf); c.vbuf = 0; } + c.as.cur.fr1 = -1; + } + if(ebuf) { glDeleteBuffers_(1, &ebuf); ebuf = 0; } + } + + void preload(part *p) + { + if(numframes > 1) return; + bool tangents = p->tangents(); + if(tangents!=vtangents) cleanup(); + if(!vbocache->vbuf) genvbo(tangents, *vbocache); + } + + void render(const animstate *as, float pitch, const vec &axis, const vec &forward, dynent *d, part *p) + { + if(as->cur.anim&ANIM_NORENDER) + { + loopv(p->links) calctagmatrix(p, p->links[i].tag, *as, p->links[i].matrix); + return; + } + + bool tangents = p->tangents(); + if(tangents!=vtangents) { cleanup(); disablevbo(); } + vbocacheentry *vc = NULL; + if(numframes<=1) vc = vbocache; + else + { + loopi(MAXVBOCACHE) + { + vbocacheentry &c = vbocache[i]; + if(!c.vbuf) continue; + if(c.as==*as) { vc = &c; break; } + } + if(!vc) loopi(MAXVBOCACHE) { vc = &vbocache[i]; if(!vc->vbuf || vc->millis < lastmillis) break; } + } + if(!vc->vbuf) genvbo(tangents, *vc); + if(numframes>1) + { + if(vc->as!=*as) + { + vc->as = *as; + vc->millis = lastmillis; + loopv(meshes) + { + vertmesh &m = *(vertmesh *)meshes[i]; + m.interpverts(*as, tangents, vdata + m.voffset*vertsize, p->skins[i]); + } + gle::bindvbo(vc->vbuf); + glBufferData_(GL_ARRAY_BUFFER, vlen*vertsize, vdata, GL_STREAM_DRAW); + } + vc->millis = lastmillis; + } + + bindvbo(as, *vc); + loopv(meshes) + { + vertmesh *m = (vertmesh *)meshes[i]; + p->skins[i].bind(m, as); + m->render(as, p->skins[i], *vc); + } + + loopv(p->links) calctagmatrix(p, p->links[i].tag, *as, p->links[i].matrix); + } + }; + + vertmodel(const char *name) : animmodel(name) + { + } +}; + +template struct vertloader : modelloader +{ + vertloader(const char *name) : modelloader(name) {} +}; + +template struct vertcommands : modelcommands +{ + typedef struct MDL::part part; + typedef struct MDL::skin skin; + + static void loadpart(char *model, float *smooth) + { + if(!MDL::loading) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; } + defformatstring(filename, "%s/%s", MDL::dir, model); + part &mdl = MDL::loading->addpart(); + if(mdl.index) mdl.pitchscale = mdl.pitchoffset = mdl.pitchmin = mdl.pitchmax = 0; + mdl.meshes = MDL::loading->sharemeshes(path(filename), double(*smooth > 0 ? cos(clamp(*smooth, 0.0f, 180.0f)*RAD) : 2)); + if(!mdl.meshes) conoutf(CON_ERROR, "could not load %s", filename); + else mdl.initskins(); + } + + static void setpitch(float *pitchscale, float *pitchoffset, float *pitchmin, float *pitchmax) + { + if(!MDL::loading || MDL::loading->parts.empty()) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; } + part &mdl = *MDL::loading->parts.last(); + + mdl.pitchscale = *pitchscale; + mdl.pitchoffset = *pitchoffset; + if(*pitchmin || *pitchmax) + { + mdl.pitchmin = *pitchmin; + mdl.pitchmax = *pitchmax; + } + else + { + mdl.pitchmin = -360*fabs(mdl.pitchscale) + mdl.pitchoffset; + mdl.pitchmax = 360*fabs(mdl.pitchscale) + mdl.pitchoffset; + } + } + + static void setanim(char *anim, int *frame, int *range, float *speed, int *priority) + { + if(!MDL::loading || MDL::loading->parts.empty()) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; } + vector anims; + findanims(anim, anims); + if(anims.empty()) conoutf(CON_ERROR, "could not find animation %s", anim); + else loopv(anims) + { + MDL::loading->parts.last()->setanim(0, anims[i], *frame, *range, *speed, *priority); + } + } + + vertcommands() + { + if(MDL::multiparted()) this->modelcommand(loadpart, "load", "sf"); + this->modelcommand(setpitch, "pitch", "ffff"); + if(MDL::animated()) this->modelcommand(setanim, "anim", "siiff"); + } +}; + diff --git a/src/engine/water.cpp b/src/engine/water.cpp new file mode 100644 index 0000000..07c24c2 --- /dev/null +++ b/src/engine/water.cpp @@ -0,0 +1,1061 @@ +#include "engine.h" + +VARFP(waterreflect, 0, 1, 1, { cleanreflections(); preloadwatershaders(); }); +VARFP(waterrefract, 0, 1, 1, { cleanreflections(); preloadwatershaders(); }); +VARFP(waterenvmap, 0, 1, 1, { cleanreflections(); preloadwatershaders(); }); +VARFP(waterfallrefract, 0, 0, 1, { cleanreflections(); preloadwatershaders(); }); + +/* vertex water */ +VARP(watersubdiv, 0, 2, 3); +VARP(waterlod, 0, 1, 3); + +static int wx1, wy1, wx2, wy2, wz, wsize, wsubdiv; +static float whoffset, whphase; + +static inline float vertwangle(int v1, int v2) +{ + static const float whscale = 59.0f/23.0f/(2*M_PI); + v1 &= wsize-1; + v2 &= wsize-1; + return v1*v2*whscale+whoffset; +} + +static inline float vertwphase(float angle) +{ + float s = angle - int(angle) - 0.5f; + s *= 8 - fabs(s)*16; + return WATER_AMPLITUDE*s-WATER_OFFSET; +} + +static inline void vertw(int v1, int v2, int v3) +{ + float h = vertwphase(vertwangle(v1, v2)); + gle::attribf(v1, v2, v3+h); +} + +static inline void vertwq(float v1, float v2, float v3) +{ + gle::attribf(v1, v2, v3+whphase); +} + +static inline void vertwn(float v1, float v2, float v3) +{ + float h = -WATER_OFFSET; + gle::attribf(v1, v2, v3+h); +} + +struct waterstrip +{ + int x1, y1, x2, y2, z; + ushort size, subdiv; + + int numverts() const { return 2*((y2-y1)/subdiv + 1)*((x2-x1)/subdiv); } + + void save() + { + x1 = wx1; + y1 = wy1; + x2 = wx2; + y2 = wy2; + z = wz; + size = wsize; + subdiv = wsubdiv; + } + + void restore() + { + wx1 = x1; + wy1 = y1; + wx2 = x2; + wy2 = y2; + wz = z; + wsize = size; + wsubdiv = subdiv; + } +}; +vector waterstrips; + +void flushwaterstrips() +{ + if(gle::attribbuf.length()) xtraverts += gle::end(); + gle::defvertex(); + int numverts = 0; + loopv(waterstrips) numverts += waterstrips[i].numverts(); + gle::begin(GL_TRIANGLE_STRIP, numverts); + loopv(waterstrips) + { + waterstrips[i].restore(); + for(int x = wx1; x < wx2; x += wsubdiv) + { + for(int y = wy1; y <= wy2; y += wsubdiv) + { + vertw(x, y, wz); + vertw(x+wsubdiv, y, wz); + } + x += wsubdiv; + if(x >= wx2) break; + for(int y = wy2; y >= wy1; y -= wsubdiv) + { + vertw(x, y, wz); + vertw(x+wsubdiv, y, wz); + } + } + gle::multidraw(); + } + waterstrips.setsize(0); + wsize = 0; + xtraverts += gle::end(); +} + +void flushwater(int mat = MAT_WATER, bool force = true) +{ + if(wsize) + { + if(wsubdiv >= wsize) + { + if(gle::attribbuf.empty()) { gle::defvertex(); gle::begin(GL_QUADS); } + vertwq(wx1, wy1, wz); + vertwq(wx2, wy1, wz); + vertwq(wx2, wy2, wz); + vertwq(wx1, wy2, wz); + } + else waterstrips.add().save(); + wsize = 0; + } + + if(force) + { + if(gle::attribbuf.length()) xtraverts += gle::end(); + if(waterstrips.length()) flushwaterstrips(); + } +} + +void rendervertwater(int subdiv, int xo, int yo, int z, int size, int mat) +{ + if(wsize == size && wsubdiv == subdiv && wz == z) + { + if(wx2 == xo) + { + if(wy1 == yo && wy2 == yo + size) { wx2 += size; return; } + } + else if(wy2 == yo && wx1 == xo && wx2 == xo + size) { wy2 += size; return; } + } + + flushwater(mat, false); + + wx1 = xo; + wy1 = yo; + wx2 = xo + size, + wy2 = yo + size; + wz = z; + wsize = size; + wsubdiv = subdiv; + + ASSERT((wx1 & (subdiv - 1)) == 0); + ASSERT((wy1 & (subdiv - 1)) == 0); +} + +int calcwatersubdiv(int x, int y, int z, int size) +{ + float dist; + if(camera1->o.x >= x && camera1->o.x < x + size && + camera1->o.y >= y && camera1->o.y < y + size) + dist = fabs(camera1->o.z - float(z)); + else + dist = vec(x + size/2, y + size/2, z + size/2).dist(camera1->o) - size*1.42f/2; + int subdiv = watersubdiv + int(dist) / (32 << waterlod); + return subdiv >= 31 ? INT_MAX : 1<= size) + { + if(subdiv < size * 2) rendervertwater(size, x, y, z, size, mat); + return subdiv; + } + int childsize = size / 2, + subdiv1 = renderwaterlod(x, y, z, childsize, mat), + subdiv2 = renderwaterlod(x + childsize, y, z, childsize, mat), + subdiv3 = renderwaterlod(x + childsize, y + childsize, z, childsize, mat), + subdiv4 = renderwaterlod(x, y + childsize, z, childsize, mat), + minsubdiv = subdiv1; + minsubdiv = min(minsubdiv, subdiv2); + minsubdiv = min(minsubdiv, subdiv3); + minsubdiv = min(minsubdiv, subdiv4); + if(minsubdiv < size * 2) + { + if(minsubdiv >= size) rendervertwater(size, x, y, z, size, mat); + else + { + if(subdiv1 >= size) rendervertwater(childsize, x, y, z, childsize, mat); + if(subdiv2 >= size) rendervertwater(childsize, x + childsize, y, z, childsize, mat); + if(subdiv3 >= size) rendervertwater(childsize, x + childsize, y + childsize, z, childsize, mat); + if(subdiv4 >= size) rendervertwater(childsize, x, y + childsize, z, childsize, mat); + } + } + return minsubdiv; + } +} + +void renderflatwater(int x, int y, int z, int rsize, int csize, int mat) +{ + if(gle::attribbuf.empty()) { gle::defvertex(); gle::begin(GL_QUADS); } + vertwn(x, y, z); + vertwn(x+rsize, y, z); + vertwn(x+rsize, y+csize, z); + vertwn(x, y+csize, z); +} + +VARFP(vertwater, 0, 1, 1, allchanged()); + +static inline void renderwater(const materialsurface &m, int mat = MAT_WATER) +{ + if(!vertwater || drawtex == DRAWTEX_MINIMAP) renderflatwater(m.o.x, m.o.y, m.o.z, m.rsize, m.csize, mat); + else if(renderwaterlod(m.o.x, m.o.y, m.o.z, m.csize, mat) >= int(m.csize) * 2) + rendervertwater(m.csize, m.o.x, m.o.y, m.o.z, m.csize, mat); +} + +void setuplava(Texture *tex, float scale) +{ + float xk = TEX_SCALE/(tex->xs*scale); + float yk = TEX_SCALE/(tex->ys*scale); + float scroll = lastmillis/1000.0f; + LOCALPARAMF(lavatexgen, xk, yk, scroll, scroll); + gle::normal(vec(0, 0, 1)); + whoffset = fmod(float(lastmillis/2000.0f/(2*M_PI)), 1.0f); + whphase = vertwphase(whoffset); +} + +void renderlava(const materialsurface &m) +{ + renderwater(m, MAT_LAVA); +} + +void flushlava() +{ + flushwater(MAT_LAVA); +} + +/* reflective/refractive water */ + +#define MAXREFLECTIONS 16 + +struct Reflection +{ + GLuint tex, refracttex; + int material, height, depth, age; + bool init; + matrix4 projmat; + occludequery *query, *prevquery; + vector matsurfs; + + Reflection() : tex(0), refracttex(0), material(-1), height(-1), depth(0), age(0), init(false), query(NULL), prevquery(NULL) + {} +}; + +VARP(reflectdist, 0, 2000, 10000); + +#define WATERVARS(name) \ + bvec name##color(0x14, 0x46, 0x50), name##fallcolor(0, 0, 0); \ + HVARFR(name##colour, 0, 0x144650, 0xFFFFFF, \ + { \ + if(!name##colour) name##colour = 0x144650; \ + name##color = bvec((name##colour>>16)&0xFF, (name##colour>>8)&0xFF, name##colour&0xFF); \ + }); \ + VARR(name##fog, 0, 150, 10000); \ + VARR(name##spec, 0, 150, 1000); \ + HVARFR(name##fallcolour, 0, 0, 0xFFFFFF, \ + { \ + name##fallcolor = bvec((name##fallcolour>>16)&0xFF, (name##fallcolour>>8)&0xFF, name##fallcolour&0xFF); \ + }); + +WATERVARS(water) +WATERVARS(water2) +WATERVARS(water3) +WATERVARS(water4) + +GETMATIDXVAR(water, colour, int) +GETMATIDXVAR(water, color, const bvec &) +GETMATIDXVAR(water, fallcolour, int) +GETMATIDXVAR(water, fallcolor, const bvec &) +GETMATIDXVAR(water, fog, int) +GETMATIDXVAR(water, spec, int) + +#define LAVAVARS(name) \ + bvec name##color(0xFF, 0x40, 0x00); \ + HVARFR(name##colour, 0, 0xFF4000, 0xFFFFFF, \ + { \ + if(!name##colour) name##colour = 0xFF4000; \ + name##color = bvec((name##colour>>16)&0xFF, (name##colour>>8)&0xFF, name##colour&0xFF); \ + }); \ + VARR(name##fog, 0, 50, 10000); + +LAVAVARS(lava) +LAVAVARS(lava2) +LAVAVARS(lava3) +LAVAVARS(lava4) + +GETMATIDXVAR(lava, colour, int) +GETMATIDXVAR(lava, color, const bvec &) +GETMATIDXVAR(lava, fog, int) + +void setprojtexmatrix(Reflection &ref) +{ + if(ref.init) + { + ref.init = false; + (ref.projmat = camprojmatrix).projective(); + } + + LOCALPARAM(watermatrix, ref.projmat); +} + +Reflection reflections[MAXREFLECTIONS]; +Reflection waterfallrefraction; +GLuint reflectionfb = 0, reflectiondb = 0; + +GLuint getwaterfalltex() { return waterfallrefraction.refracttex ? waterfallrefraction.refracttex : notexture->id; } + +VAR(oqwater, 0, 2, 2); +VARFP(waterfade, 0, 1, 1, { cleanreflections(); preloadwatershaders(); }); + +void preloadwatershaders(bool force) +{ + static bool needwater = false; + if(force) needwater = true; + if(!needwater) return; + + useshaderbyname("waterglare"); + + if(waterenvmap && !waterreflect) + useshaderbyname(waterrefract ? (waterfade ? "waterenvfade" : "waterenvrefract") : "waterenv"); + else useshaderbyname(waterrefract ? (waterfade ? "waterfade" : "waterrefract") : (waterreflect ? "waterreflect" : "water")); + + useshaderbyname(waterrefract ? (waterfade ? "underwaterfade" : "underwaterrefract") : "underwater"); + + extern int waterfallenv; + useshaderbyname(waterfallenv ? "waterfallenv" : "waterfall"); + if(waterfallrefract) useshaderbyname(waterfallenv ? "waterfallenvrefract" : "waterfallrefract"); +} + +void renderwater() +{ + if(editmode && showmat && !drawtex) return; + if(!rplanes) return; + + glDisable(GL_CULL_FACE); + + if(!glaring && drawtex != DRAWTEX_MINIMAP) + { + if(waterrefract) + { + if(waterfade) + { + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + } + } + else + { + glDepthMask(GL_FALSE); + glEnable(GL_BLEND); + glBlendFunc(GL_ONE, GL_SRC_ALPHA); + } + } + + GLOBALPARAM(camera, camera1->o); + GLOBALPARAMF(millis, lastmillis/1000.0f); + + #define SETWATERSHADER(which, name) \ + do { \ + static Shader *name##shader = NULL; \ + if(!name##shader) name##shader = lookupshaderbyname(#name); \ + which##shader = name##shader; \ + } while(0) + + Shader *aboveshader = NULL; + if(glaring) SETWATERSHADER(above, waterglare); + else if(drawtex == DRAWTEX_MINIMAP) aboveshader = notextureshader; + else if(waterenvmap && !waterreflect) + { + if(waterrefract) + { + if(waterfade) SETWATERSHADER(above, waterenvfade); + else SETWATERSHADER(above, waterenvrefract); + } + else SETWATERSHADER(above, waterenv); + } + else if(waterrefract) + { + if(waterfade) SETWATERSHADER(above, waterfade); + else SETWATERSHADER(above, waterrefract); + } + else if(waterreflect) SETWATERSHADER(above, waterreflect); + else SETWATERSHADER(above, water); + + Shader *belowshader = NULL; + if(!glaring && drawtex != DRAWTEX_MINIMAP) + { + if(waterrefract) + { + if(waterfade) SETWATERSHADER(below, underwaterfade); + else SETWATERSHADER(below, underwaterrefract); + } + else SETWATERSHADER(below, underwater); + } + + vec ambient(max(skylightcolor[0], ambientcolor[0]), max(skylightcolor[1], ambientcolor[1]), max(skylightcolor[2], ambientcolor[2])); + float offset = -WATER_OFFSET; + loopi(MAXREFLECTIONS) + { + Reflection &ref = reflections[i]; + if(ref.height<0 || ref.age || ref.matsurfs.empty()) continue; + if(!glaring && oqfrags && oqwater && ref.query && ref.query->owner==&ref) + { + if(!ref.prevquery || ref.prevquery->owner!=&ref || checkquery(ref.prevquery)) + { + if(checkquery(ref.query)) continue; + } + } + + bool below = camera1->o.z < ref.height+offset; + if(below) + { + if(!belowshader) continue; + belowshader->set(); + } + else aboveshader->set(); + + if(!glaring && drawtex != DRAWTEX_MINIMAP) + { + if(waterreflect || waterrefract) + { + if(waterreflect || !waterenvmap) glBindTexture(GL_TEXTURE_2D, waterreflect ? ref.tex : ref.refracttex); + setprojtexmatrix(ref); + } + + if(waterrefract) + { + glActiveTexture_(GL_TEXTURE3); + glBindTexture(GL_TEXTURE_2D, ref.refracttex); + if(waterfade) + { + float fadeheight = ref.height+offset+(below ? -2 : 2); + LOCALPARAMF(waterheight, fadeheight); + } + } + } + + MSlot &mslot = lookupmaterialslot(ref.material); + glActiveTexture_(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, mslot.sts.inrange(2) ? mslot.sts[2].t->id : notexture->id); + glActiveTexture_(GL_TEXTURE2); + glBindTexture(GL_TEXTURE_2D, mslot.sts.inrange(3) ? mslot.sts[3].t->id : notexture->id); + glActiveTexture_(GL_TEXTURE0); + if(!glaring && waterenvmap && !waterreflect && drawtex != DRAWTEX_MINIMAP) + { + glBindTexture(GL_TEXTURE_CUBE_MAP, lookupenvmap(mslot)); + } + + whoffset = fmod(float(lastmillis/600.0f/(2*M_PI)), 1.0f); + whphase = vertwphase(whoffset); + + gle::color(getwatercolor(ref.material)); + int wfog = getwaterfog(ref.material), wspec = getwaterspec(ref.material); + + const entity *lastlight = (const entity *)-1; + int lastdepth = -1; + loopvj(ref.matsurfs) + { + materialsurface &m = *ref.matsurfs[j]; + + entity *light = (m.light && m.light->type==ET_LIGHT ? m.light : NULL); + if(light!=lastlight) + { + flushwater(); + vec lightpos = light ? light->o : vec(worldsize/2, worldsize/2, worldsize); + float lightrad = light && light->attr1 ? light->attr1 : worldsize*8.0f; + vec lightcol = (light ? vec(light->attr2, light->attr3, light->attr4) : vec(ambient)).div(255.0f).mul(wspec/100.0f); + LOCALPARAM(lightpos, lightpos); + LOCALPARAM(lightcolor, lightcol); + LOCALPARAMF(lightradius, lightrad); + lastlight = light; + } + + if(!glaring && !waterrefract && m.depth!=lastdepth) + { + flushwater(); + float depth = !wfog ? 1.0f : min(0.75f*m.depth/wfog, 0.95f); + depth = max(depth, !below && (waterreflect || waterenvmap) ? 0.3f : 0.6f); + LOCALPARAMF(depth, depth, 1.0f-depth); + lastdepth = m.depth; + } + + renderwater(m); + } + flushwater(); + } + + if(!glaring && drawtex != DRAWTEX_MINIMAP) + { + if(waterrefract) + { + if(waterfade) glDisable(GL_BLEND); + } + else + { + glDepthMask(GL_TRUE); + glDisable(GL_BLEND); + } + } + + glEnable(GL_CULL_FACE); +} + +void setupwaterfallrefract() +{ + glBindTexture(GL_TEXTURE_2D, waterfallrefraction.refracttex ? waterfallrefraction.refracttex : notexture->id); + setprojtexmatrix(waterfallrefraction); +} + +void cleanreflection(Reflection &ref) +{ + ref.material = -1; + ref.height = -1; + ref.init = false; + ref.query = ref.prevquery = NULL; + ref.matsurfs.setsize(0); + if(ref.tex) + { + glDeleteTextures(1, &ref.tex); + ref.tex = 0; + } + if(ref.refracttex) + { + glDeleteTextures(1, &ref.refracttex); + ref.refracttex = 0; + } +} + +void cleanreflections() +{ + loopi(MAXREFLECTIONS) cleanreflection(reflections[i]); + cleanreflection(waterfallrefraction); + if(reflectionfb) + { + glDeleteFramebuffers_(1, &reflectionfb); + reflectionfb = 0; + } + if(reflectiondb) + { + glDeleteRenderbuffers_(1, &reflectiondb); + reflectiondb = 0; + } +} + +VARFP(reflectsize, 6, 8, 11, cleanreflections()); + +void genwatertex(GLuint &tex, GLuint &fb, GLuint &db, bool refract = false) +{ + static const GLenum colorfmts[] = { GL_RGBA, GL_RGBA8, GL_RGB, GL_RGB8, GL_FALSE }, + depthfmts[] = { GL_DEPTH_COMPONENT24, GL_DEPTH_COMPONENT, GL_DEPTH_COMPONENT16, GL_DEPTH_COMPONENT32, GL_FALSE }; + static GLenum reflectfmt = GL_FALSE, refractfmt = GL_FALSE, depthfmt = GL_FALSE; + static bool usingalpha = false; + bool needsalpha = refract && waterrefract && waterfade; + if(refract && usingalpha!=needsalpha) + { + usingalpha = needsalpha; + refractfmt = GL_FALSE; + } + int size = 1<hwtexsize) size /= 2; + + glGenTextures(1, &tex); + char *buf = new char[size*size*4]; + memset(buf, 0, size*size*4); + + GLenum &colorfmt = refract ? refractfmt : reflectfmt; + if(colorfmt && fb && db) + { + createtexture(tex, size, size, buf, 3, 1, colorfmt); + delete[] buf; + return; + } + + if(!fb) glGenFramebuffers_(1, &fb); + int find = needsalpha ? 0 : 2; + do + { + createtexture(tex, size, size, buf, 3, 1, colorfmt ? colorfmt : colorfmts[find]); + glBindFramebuffer_(GL_FRAMEBUFFER, fb); + glFramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0); + if(glCheckFramebufferStatus_(GL_FRAMEBUFFER)==GL_FRAMEBUFFER_COMPLETE) break; + } + while(!colorfmt && colorfmts[++find]); + if(!colorfmt) colorfmt = colorfmts[find]; + + delete[] buf; + + if(!db) { glGenRenderbuffers_(1, &db); depthfmt = GL_FALSE; } + if(!depthfmt) glBindRenderbuffer_(GL_RENDERBUFFER, db); + find = 0; + do + { + if(!depthfmt) glRenderbufferStorage_(GL_RENDERBUFFER, depthfmts[find], size, size); + glFramebufferRenderbuffer_(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, db); + if(glCheckFramebufferStatus_(GL_FRAMEBUFFER)==GL_FRAMEBUFFER_COMPLETE) break; + } + while(!depthfmt && depthfmts[++find]); + if(!depthfmt) + { + glBindRenderbuffer_(GL_RENDERBUFFER, 0); + depthfmt = depthfmts[find]; + } + + glBindFramebuffer_(GL_FRAMEBUFFER, 0); +} + +void addwaterfallrefraction(materialsurface &m) +{ + Reflection &ref = waterfallrefraction; + if(ref.age>=0) + { + ref.age = -1; + ref.init = false; + ref.matsurfs.setsize(0); + ref.material = MAT_WATER; + ref.height = INT_MAX; + } + ref.matsurfs.add(&m); + + if(!ref.refracttex) genwatertex(ref.refracttex, reflectionfb, reflectiondb); +} + +void addreflection(materialsurface &m) +{ + int mat = m.material, height = m.o.z; + Reflection *ref = NULL, *oldest = NULL; + loopi(MAXREFLECTIONS) + { + Reflection &r = reflections[i]; + if(r.height<0) + { + if(!ref) ref = &r; + } + else if(r.height==height && r.material==mat) + { + r.matsurfs.add(&m); + r.depth = max(r.depth, int(m.depth)); + if(r.age<0) return; + ref = &r; + break; + } + else if(!oldest || r.age>oldest->age) oldest = &r; + } + if(!ref) + { + if(!oldest || oldest->age<0) return; + ref = oldest; + } + if(ref->height!=height || ref->material!=mat) + { + ref->material = mat; + ref->height = height; + ref->prevquery = NULL; + } + rplanes++; + ref->age = -1; + ref->init = false; + ref->matsurfs.setsize(0); + ref->matsurfs.add(&m); + ref->depth = m.depth; + if(drawtex == DRAWTEX_MINIMAP) return; + + if(waterreflect && !ref->tex) genwatertex(ref->tex, reflectionfb, reflectiondb); + if(waterrefract && !ref->refracttex) genwatertex(ref->refracttex, reflectionfb, reflectiondb, true); +} + +static void drawmaterialquery(const materialsurface &m, float offset, float border = 0, float reflect = -1) +{ + if(gle::attribbuf.empty()) + { + gle::defvertex(); + gle::begin(GL_QUADS); + } + float x = m.o.x, y = m.o.y, z = m.o.z, csize = m.csize + border, rsize = m.rsize + border; + if(reflect >= 0) z = 2*reflect - z; + switch(m.orient) + { +#define GENFACEORIENT(orient, v0, v1, v2, v3) \ + case orient: v0 v1 v2 v3 break; +#define GENFACEVERT(orient, vert, mx,my,mz, sx,sy,sz) \ + gle::attribf(mx sx, my sy, mz sz); + GENFACEVERTS(x, x, y, y, z, z, - border, + csize, - border, + rsize, + offset, - offset) +#undef GENFACEORIENT +#undef GENFACEVERT + } +} + +extern void drawreflection(float z, bool refract, int fogdepth = -1, const bvec &col = bvec(0, 0, 0)); + +int rplanes = 0; + +void queryreflection(Reflection &ref, bool init) +{ + if(init) + { + nocolorshader->set(); + glDepthMask(GL_FALSE); + glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + glDisable(GL_CULL_FACE); + } + startquery(ref.query); + loopvj(ref.matsurfs) + { + materialsurface &m = *ref.matsurfs[j]; + float offset = 0.1f; + if(m.orient==O_TOP) + { + offset = WATER_OFFSET + + (vertwater ? WATER_AMPLITUDE*(camera1->pitch > 0 || m.depth < WATER_AMPLITUDE+0.5f ? -1 : 1) : 0); + if(fabs(m.o.z-offset - camera1->o.z) < 0.5f && m.depth > WATER_AMPLITUDE+1.5f) + offset += camera1->pitch > 0 ? -1 : 1; + } + drawmaterialquery(m, offset); + } + xtraverts += gle::end(); + endquery(ref.query); +} + +void queryreflections() +{ + rplanes = 0; + + static int lastsize = 0; + int size = 1<hwtexsize) size /= 2; + if(size!=lastsize) { if(lastsize) cleanreflections(); lastsize = size; } + + for(vtxarray *va = visibleva; va; va = va->next) + { + if(!va->matsurfs || va->occluded >= OCCLUDE_BB || va->curvfc >= VFC_FOGGED) continue; + int lastmat = -1; + loopi(va->matsurfs) + { + materialsurface &m = va->matbuf[i]; + if(m.material != lastmat) + { + if((m.material&MATF_VOLUME) != MAT_WATER || m.orient == O_BOTTOM) { i += m.skip; continue; } + if(m.orient != O_TOP) + { + if(!waterfallrefract || !getwaterfog(m.material)) { i += m.skip; continue; } + } + lastmat = m.material; + } + if(m.orient==O_TOP) addreflection(m); + else addwaterfallrefraction(m); + } + } + + loopi(MAXREFLECTIONS) + { + Reflection &ref = reflections[i]; + ++ref.age; + if(ref.height>=0 && !ref.age && ref.matsurfs.length()) + { + if(waterpvsoccluded(ref.height)) ref.matsurfs.setsize(0); + } + } + if(waterfallrefract) + { + Reflection &ref = waterfallrefraction; + ++ref.age; + if(ref.height>=0 && !ref.age && ref.matsurfs.length()) + { + if(waterpvsoccluded(-1)) ref.matsurfs.setsize(0); + } + } + + if((editmode && showmat && !drawtex) || !oqfrags || !oqwater || drawtex == DRAWTEX_MINIMAP) return; + + int refs = 0; + if(waterreflect || waterrefract) loopi(MAXREFLECTIONS) + { + Reflection &ref = reflections[i]; + ref.prevquery = oqwater > 1 ? ref.query : NULL; + ref.query = ref.height>=0 && !ref.age && ref.matsurfs.length() ? newquery(&ref) : NULL; + if(ref.query) queryreflection(ref, !refs++); + } + if(waterfallrefract) + { + Reflection &ref = waterfallrefraction; + ref.prevquery = oqwater > 1 ? ref.query : NULL; + ref.query = ref.height>=0 && !ref.age && ref.matsurfs.length() ? newquery(&ref) : NULL; + if(ref.query) queryreflection(ref, !refs++); + } + + if(refs) + { + glDepthMask(GL_TRUE); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + glEnable(GL_CULL_FACE); + } + + glFlush(); +} + +VARP(maxreflect, 1, 2, 8); + +int refracting = 0, refractfog = 0; +bvec refractcolor(0, 0, 0); +bool reflecting = false, fading = false, fogging = false; +float reflectz = 1e16f; + +VAR(maskreflect, 0, 2, 16); + +void maskreflection(Reflection &ref, float offset, bool reflect, bool clear = false) +{ + const bvec &wcol = getwatercolor(ref.material); + vec color = wcol.tocolor(); + if(!maskreflect) + { + if(clear) glClearColor(color.r, color.g, color.b, 1); + glClear(GL_DEPTH_BUFFER_BIT | (clear ? GL_COLOR_BUFFER_BIT : 0)); + return; + } + glClearDepth(0); + glClear(GL_DEPTH_BUFFER_BIT); + glClearDepth(1); + glDepthRange(1, 1); + glDepthFunc(GL_ALWAYS); + glDisable(GL_CULL_FACE); + if(clear) + { + notextureshader->set(); + gle::color(color); + } + else + { + nocolorshader->set(); + glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + } + float reflectheight = reflect ? ref.height + offset : -1; + loopv(ref.matsurfs) + { + materialsurface &m = *ref.matsurfs[i]; + drawmaterialquery(m, -offset, maskreflect, reflectheight); + } + xtraverts += gle::end(); + if(!clear) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + glEnable(GL_CULL_FACE); + glDepthFunc(GL_LESS); + glDepthRange(0, 1); +} + +VAR(reflectscissor, 0, 1, 1); +VAR(reflectvfc, 0, 1, 1); + +static bool calcscissorbox(Reflection &ref, int size, vec &clipmin, vec &clipmax, int &sx, int &sy, int &sw, int &sh) +{ + materialsurface &m0 = *ref.matsurfs[0]; + int dim = dimension(m0.orient), r = R[dim], c = C[dim]; + ivec bbmin = m0.o, bbmax = bbmin; + bbmax[r] += m0.rsize; + bbmax[c] += m0.csize; + loopvj(ref.matsurfs) + { + materialsurface &m = *ref.matsurfs[j]; + bbmin[r] = min(bbmin[r], m.o[r]); + bbmin[c] = min(bbmin[c], m.o[c]); + bbmax[r] = max(bbmax[r], m.o[r] + m.rsize); + bbmax[c] = max(bbmax[c], m.o[c] + m.csize); + bbmin[dim] = min(bbmin[dim], m.o[dim]); + bbmax[dim] = max(bbmax[dim], m.o[dim]); + } + + vec4 v[8]; + float sx1 = 1, sy1 = 1, sx2 = -1, sy2 = -1; + loopi(8) + { + vec4 &p = v[i]; + camprojmatrix.transform(vec(i&1 ? bbmax.x : bbmin.x, i&2 ? bbmax.y : bbmin.y, (i&4 ? bbmax.z + WATER_AMPLITUDE : bbmin.z - WATER_AMPLITUDE) - WATER_OFFSET), p); + if(p.z >= -p.w) + { + float x = p.x / p.w, y = p.y / p.w; + sx1 = min(sx1, x); + sy1 = min(sy1, y); + sx2 = max(sx2, x); + sy2 = max(sy2, y); + } + } + if(sx1 >= sx2 || sy1 >= sy2) return false; + loopi(8) + { + const vec4 &p = v[i]; + if(p.z >= -p.w) continue; + loopj(3) + { + const vec4 &o = v[i^(1<= 1 && sy2 >= 1) return false; + sx1 = max(sx1, -1.0f); + sy1 = max(sy1, -1.0f); + sx2 = min(sx2, 1.0f); + sy2 = min(sy2, 1.0f); + if(reflectvfc) + { + clipmin.x = clamp(clipmin.x, sx1, sx2); + clipmin.y = clamp(clipmin.y, sy1, sy2); + clipmax.x = clamp(clipmax.x, sx1, sx2); + clipmax.y = clamp(clipmax.y, sy1, sy2); + } + sx = int(floor((sx1+1)*0.5f*size)); + sy = int(floor((sy1+1)*0.5f*size)); + sw = max(int(ceil((sx2+1)*0.5f*size)) - sx, 0); + sh = max(int(ceil((sy2+1)*0.5f*size)) - sy, 0); + return true; +} + +VARR(refractclear, 0, 0, 1); + +void drawreflections() +{ + if((editmode && showmat && !drawtex) || drawtex == DRAWTEX_MINIMAP) return; + + static int lastdrawn = 0; + int refs = 0, n = lastdrawn; + float offset = -WATER_OFFSET; + int size = 1<hwtexsize) size /= 2; + + if(waterreflect || waterrefract) loopi(MAXREFLECTIONS) + { + Reflection &ref = reflections[++n%MAXREFLECTIONS]; + if(ref.height<0 || ref.age || ref.matsurfs.empty()) continue; + if(oqfrags && oqwater && ref.query && ref.query->owner==&ref) + { + if(!ref.prevquery || ref.prevquery->owner!=&ref || checkquery(ref.prevquery)) + { + if(checkquery(ref.query)) continue; + } + } + + if(!refs) + { + glViewport(0, 0, size, size); + glBindFramebuffer_(GL_FRAMEBUFFER, reflectionfb); + } + refs++; + ref.init = true; + lastdrawn = n; + + vec clipmin(-1, -1, -1), clipmax(1, 1, 1); + int sx, sy, sw, sh; + bool scissor = reflectscissor && calcscissorbox(ref, size, clipmin, clipmax, sx, sy, sw, sh); + if(scissor) glScissor(sx, sy, sw, sh); + else + { + sx = sy = 0; + sw = sh = size; + } + + const bvec &wcol = getwatercolor(ref.material); + int wfog = getwaterfog(ref.material); + + if(waterreflect && ref.tex && camera1->o.z >= ref.height+offset) + { + glFramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ref.tex, 0); + if(scissor) glEnable(GL_SCISSOR_TEST); + maskreflection(ref, offset, true); + savevfcP(); + setvfcP(ref.height+offset, clipmin, clipmax); + drawreflection(ref.height+offset, false); + restorevfcP(); + if(scissor) glDisable(GL_SCISSOR_TEST); + } + + if(waterrefract && ref.refracttex) + { + glFramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ref.refracttex, 0); + if(scissor) glEnable(GL_SCISSOR_TEST); + maskreflection(ref, offset, false, refractclear || !wfog || (ref.depth>=10000 && camera1->o.z >= ref.height + offset)); + if(wfog || waterfade) + { + savevfcP(); + setvfcP(-1, clipmin, clipmax); + drawreflection(ref.height+offset, true, wfog, wcol); + restorevfcP(); + } + if(scissor) glDisable(GL_SCISSOR_TEST); + } + + if(refs>=maxreflect) break; + } + + if(waterfallrefract && waterfallrefraction.refracttex) + { + Reflection &ref = waterfallrefraction; + + if(ref.height<0 || ref.age || ref.matsurfs.empty()) goto nowaterfall; + if(oqfrags && oqwater && ref.query && ref.query->owner==&ref) + { + if(!ref.prevquery || ref.prevquery->owner!=&ref || checkquery(ref.prevquery)) + { + if(checkquery(ref.query)) goto nowaterfall; + } + } + + if(!refs) + { + glViewport(0, 0, size, size); + glBindFramebuffer_(GL_FRAMEBUFFER, reflectionfb); + } + refs++; + ref.init = true; + + vec clipmin(-1, -1, -1), clipmax(1, 1, 1); + int sx, sy, sw, sh; + bool scissor = reflectscissor && calcscissorbox(ref, size, clipmin, clipmax, sx, sy, sw, sh); + if(scissor) glScissor(sx, sy, sw, sh); + else + { + sx = sy = 0; + sw = sh = size; + } + + glFramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ref.refracttex, 0); + if(scissor) glEnable(GL_SCISSOR_TEST); + maskreflection(ref, -0.1f, false); + savevfcP(); + setvfcP(-1, clipmin, clipmax); + drawreflection(-1, true); + restorevfcP(); + if(scissor) glDisable(GL_SCISSOR_TEST); + } +nowaterfall: + + if(!refs) return; + glViewport(0, 0, screenw, screenh); + glBindFramebuffer_(GL_FRAMEBUFFER, 0); +} + diff --git a/src/engine/world.cpp b/src/engine/world.cpp new file mode 100644 index 0000000..fcf64c2 --- /dev/null +++ b/src/engine/world.cpp @@ -0,0 +1,1391 @@ +// world.cpp: core map management stuff + +#include "engine.h" + +VARR(mapversion, 1, MAPVERSION, 0); +VARNR(mapscale, worldscale, 1, 0, 0); +VARNR(mapsize, worldsize, 1, 0, 0); +SVARR(maptitle, "Untitled Map by Unknown"); + +VAR(octaentsize, 0, 64, 1024); +VAR(entselradius, 0, 2, 10); + +static inline void mmboundbox(const entity &e, model *m, vec ¢er, vec &radius) +{ + m->boundbox(center, radius); + rotatebb(center, radius, e.attr1); +} + +static inline void mmcollisionbox(const entity &e, model *m, vec ¢er, vec &radius) +{ + m->collisionbox(center, radius); + rotatebb(center, radius, e.attr1); +} + +bool getentboundingbox(const extentity &e, ivec &o, ivec &r) +{ + switch(e.type) + { + case ET_EMPTY: + return false; + case ET_MAPMODEL: + { + model *m = loadmapmodel(e.attr2); + if(m) + { + vec center, radius; + mmboundbox(e, m, center, radius); + center.add(e.o); + radius.max(entselradius); + o = ivec(vec(center).sub(radius)); + r = ivec(vec(center).add(radius).add(1)); + break; + } + } + // invisible mapmodels use entselradius + default: + o = ivec(vec(e.o).sub(entselradius)); + r = ivec(vec(e.o).add(entselradius+1)); + break; + } + return true; +} + +enum +{ + MODOE_ADD = 1<<0, + MODOE_UPDATEBB = 1<<1, + MODOE_LIGHTENT = 1<<2 +}; + +void modifyoctaentity(int flags, int id, extentity &e, cube *c, const ivec &cor, int size, const ivec &bo, const ivec &br, int leafsize, vtxarray *lastva = NULL) +{ + loopoctabox(cor, size, bo, br) + { + ivec o(i, cor, size); + vtxarray *va = c[i].ext && c[i].ext->va ? c[i].ext->va : lastva; + if(c[i].children != NULL && size > leafsize) + modifyoctaentity(flags, id, e, c[i].children, o, size>>1, bo, br, leafsize, va); + else if(flags&MODOE_ADD) + { + if(!c[i].ext || !c[i].ext->ents) ext(c[i]).ents = new octaentities(o, size); + octaentities &oe = *c[i].ext->ents; + switch(e.type) + { + case ET_MAPMODEL: + if(loadmapmodel(e.attr2)) + { + if(va) + { + va->bbmin.x = -1; + if(oe.mapmodels.empty()) va->mapmodels.add(&oe); + } + oe.mapmodels.add(id); + oe.bbmin.min(bo).max(oe.o); + oe.bbmax.max(br).min(ivec(oe.o).add(oe.size)); + break; + } + // invisible mapmodel + default: + oe.other.add(id); + break; + } + + } + else if(c[i].ext && c[i].ext->ents) + { + octaentities &oe = *c[i].ext->ents; + switch(e.type) + { + case ET_MAPMODEL: + if(loadmapmodel(e.attr2)) + { + oe.mapmodels.removeobj(id); + if(va) + { + va->bbmin.x = -1; + if(oe.mapmodels.empty()) va->mapmodels.removeobj(&oe); + } + oe.bbmin = oe.bbmax = oe.o; + oe.bbmin.add(oe.size); + loopvj(oe.mapmodels) + { + extentity &e = *entities::getents()[oe.mapmodels[j]]; + ivec eo, er; + if(getentboundingbox(e, eo, er)) + { + oe.bbmin.min(eo); + oe.bbmax.max(er); + } + } + oe.bbmin.max(oe.o); + oe.bbmax.min(ivec(oe.o).add(oe.size)); + break; + } + // invisible mapmodel + default: + oe.other.removeobj(id); + break; + } + if(oe.mapmodels.empty() && oe.other.empty()) + freeoctaentities(c[i]); + } + if(c[i].ext && c[i].ext->ents) c[i].ext->ents->query = NULL; + if(va && va!=lastva) + { + if(lastva) + { + if(va->bbmin.x < 0) lastva->bbmin.x = -1; + } + else if(flags&MODOE_UPDATEBB) updatevabb(va); + } + } +} + +vector outsideents; + +static bool modifyoctaent(int flags, int id, extentity &e) +{ + if(flags&MODOE_ADD ? e.flags&EF_OCTA : !(e.flags&EF_OCTA)) return false; + + ivec o, r; + if(!getentboundingbox(e, o, r)) return false; + + if(!insideworld(e.o)) + { + int idx = outsideents.find(id); + if(flags&MODOE_ADD) + { + if(idx < 0) outsideents.add(id); + } + else if(idx >= 0) outsideents.removeunordered(idx); + } + else + { + int leafsize = octaentsize, limit = max(r.x - o.x, max(r.y - o.y, r.z - o.z)); + while(leafsize < limit) leafsize *= 2; + int diff = ~(leafsize-1) & ((o.x^r.x)|(o.y^r.y)|(o.z^r.z)); + if(diff && (limit > octaentsize/2 || diff < leafsize*2)) leafsize *= 2; + modifyoctaentity(flags, id, e, worldroot, ivec(0, 0, 0), worldsize>>1, o, r, leafsize); + } + e.flags ^= EF_OCTA; + if(e.type == ET_LIGHT) clearlightcache(id); + else if(e.type == ET_PARTICLES) clearparticleemitters(); + else if(flags&MODOE_LIGHTENT) lightent(e); + return true; +} + +static inline bool modifyoctaent(int flags, int id) +{ + vector &ents = entities::getents(); + return ents.inrange(id) && modifyoctaent(flags, id, *ents[id]); +} + +static inline void addentity(int id) { modifyoctaent(MODOE_ADD|MODOE_UPDATEBB|MODOE_LIGHTENT, id); } +static inline void removeentity(int id) { modifyoctaent(MODOE_UPDATEBB, id); } + +void freeoctaentities(cube &c) +{ + if(!c.ext) return; + if(entities::getents().length()) + { + while(c.ext->ents && !c.ext->ents->mapmodels.empty()) removeentity(c.ext->ents->mapmodels.pop()); + while(c.ext->ents && !c.ext->ents->other.empty()) removeentity(c.ext->ents->other.pop()); + } + if(c.ext->ents) + { + delete c.ext->ents; + c.ext->ents = NULL; + } +} + +void entitiesinoctanodes() +{ + vector &ents = entities::getents(); + loopv(ents) modifyoctaent(MODOE_ADD, i, *ents[i]); +} + +static inline void findents(octaentities &oe, int low, int high, bool notspawned, const vec &pos, const vec &invradius, vector &found) +{ + vector &ents = entities::getents(); + loopv(oe.other) + { + int id = oe.other[i]; + extentity &e = *ents[id]; + if(e.type >= low && e.type <= high && (e.spawned() || notspawned) && vec(e.o).sub(pos).mul(invradius).squaredlen() <= 1) found.add(id); + } +} + +static inline void findents(cube *c, const ivec &o, int size, const ivec &bo, const ivec &br, int low, int high, bool notspawned, const vec &pos, const vec &invradius, vector &found) +{ + loopoctabox(o, size, bo, br) + { + if(c[i].ext && c[i].ext->ents) findents(*c[i].ext->ents, low, high, notspawned, pos, invradius, found); + if(c[i].children && size > octaentsize) + { + ivec co(i, o, size); + findents(c[i].children, co, size>>1, bo, br, low, high, notspawned, pos, invradius, found); + } + } +} + +void findents(int low, int high, bool notspawned, const vec &pos, const vec &radius, vector &found) +{ + vec invradius(1/radius.x, 1/radius.y, 1/radius.z); + ivec bo(vec(pos).sub(radius).sub(1)), + br(vec(pos).add(radius).add(1)); + int diff = (bo.x^br.x) | (bo.y^br.y) | (bo.z^br.z) | octaentsize, + scale = worldscale-1; + if(diff&~((1<= uint(worldsize)) + { + findents(worldroot, ivec(0, 0, 0), 1<ext && c->ext->ents) findents(*c->ext->ents, low, high, notspawned, pos, invradius, found); + scale--; + while(c->children && !(diff&(1<children[octastep(bo.x, bo.y, bo.z, scale)]; + if(c->ext && c->ext->ents) findents(*c->ext->ents, low, high, notspawned, pos, invradius, found); + scale--; + } + if(c->children && 1<= octaentsize) findents(c->children, ivec(bo).mask(~((2<= sel.o.x + && o.y <= sel.o.y+sel.s.y*sel.grid + && o.y >= sel.o.y + && o.z <= sel.o.z+sel.s.z*sel.grid + && o.z >= sel.o.z); +} + +vector entgroup; + +bool haveselent() +{ + return entgroup.length() > 0; +} + +void entcancel() +{ + entgroup.shrink(0); +} + +void entadd(int id) +{ + undonext = true; + entgroup.add(id); +} + +undoblock *newundoent() +{ + int numents = entgroup.length(); + if(numents <= 0) return NULL; + undoblock *u = (undoblock *)new uchar[sizeof(undoblock) + numents*sizeof(undoent)]; + u->numents = numents; + undoent *e = (undoent *)(u + 1); + loopv(entgroup) + { + e->i = entgroup[i]; + e->e = *entities::getents()[entgroup[i]]; + e++; + } + return u; +} + +void makeundoent() +{ + if(!undonext) return; + undonext = false; + oldhover = enthover; + undoblock *u = newundoent(); + if(u) addundo(u); +} + +void detachentity(extentity &e) +{ + if(!e.attached) return; + e.attached->attached = NULL; + e.attached = NULL; +} + +VAR(attachradius, 1, 100, 1000); + +void attachentity(extentity &e) +{ + switch(e.type) + { + case ET_SPOTLIGHT: + break; + + default: + if(e.type &ents = entities::getents(); + int closest = -1; + float closedist = 1e10f; + loopv(ents) + { + extentity *a = ents[i]; + if(a->attached) continue; + switch(e.type) + { + case ET_SPOTLIGHT: + if(a->type!=ET_LIGHT) continue; + break; + + default: + if(e.typeo); + if(dist < closedist) + { + closest = i; + closedist = dist; + } + } + if(closedist>attachradius) return; + e.attached = ents[closest]; + ents[closest]->attached = &e; +} + +void attachentities() +{ + vector &ents = entities::getents(); + loopv(ents) attachentity(*ents[i]); +} + +// convenience macros implicitly define: +// e entity, currently edited ent +// n int, index to currently edited ent +#define addimplicit(f) { if(entgroup.empty() && enthover>=0) { entadd(enthover); undonext = (enthover != oldhover); f; entgroup.drop(); } else f; } +#define entfocusv(i, f, v){ int n = efocus = (i); if(n>=0) { extentity &e = *v[n]; f; } } +#define entfocus(i, f) entfocusv(i, f, entities::getents()) +#define enteditv(i, f, v) \ +{ \ + entfocusv(i, \ + { \ + int oldtype = e.type; \ + removeentity(n); \ + f; \ + if(oldtype!=e.type) detachentity(e); \ + if(e.type!=ET_EMPTY) { addentity(n); if(oldtype!=e.type) attachentity(e); } \ + entities::editent(n, true); \ + }, v); \ +} +#define entedit(i, f) enteditv(i, f, entities::getents()) +#define addgroup(exp) { vector &ents = entities::getents(); loopv(ents) entfocusv(i, if(exp) entadd(n), ents); } +#define setgroup(exp) { entcancel(); addgroup(exp); } +#define groupeditloop(f){ vector &ents = entities::getents(); entlooplevel++; int _ = efocus; loopv(entgroup) enteditv(entgroup[i], f, ents); efocus = _; entlooplevel--; } +#define groupeditpure(f){ if(entlooplevel>0) { entedit(efocus, f); } else groupeditloop(f); } +#define groupeditundo(f){ makeundoent(); groupeditpure(f); } +#define groupedit(f) { addimplicit(groupeditundo(f)); } + +vec getselpos() +{ + vector &ents = entities::getents(); + if(entgroup.length() && ents.inrange(entgroup[0])) return ents[entgroup[0]]->o; + if(ents.inrange(enthover)) return ents[enthover]->o; + return vec(sel.o); +} + +undoblock *copyundoents(undoblock *u) +{ + entcancel(); + undoent *e = u->ents(); + loopi(u->numents) + entadd(e[i].i); + undoblock *c = newundoent(); + loopi(u->numents) if(e[i].e.type==ET_EMPTY) + entgroup.removeobj(e[i].i); + return c; +} + +void pasteundoent(int idx, const entity &ue) +{ + if(idx < 0 || idx >= MAXENTS) return; + vector &ents = entities::getents(); + while(ents.length() < idx) ents.add(entities::newentity())->type = ET_EMPTY; + int efocus = -1; + entedit(idx, (entity &)e = ue); +} + +void pasteundoents(undoblock *u) +{ + undoent *ue = u->ents(); + loopi(u->numents) + entedit(ue[i].i, (entity &)e = ue[i].e); +} + +void entflip() +{ + if(noentedit()) return; + int d = dimension(sel.orient); + float mid = sel.s[d]*sel.grid/2+sel.o[d]; + groupeditundo(e.o[d] -= (e.o[d]-mid)*2); +} + +void entrotate(int *cw) +{ + if(noentedit()) return; + int d = dimension(sel.orient); + int dd = (*cw<0) == dimcoord(sel.orient) ? R[d] : C[d]; + float mid = sel.s[dd]*sel.grid/2+sel.o[dd]; + vec s(sel.o.v); + groupeditundo( + e.o[dd] -= (e.o[dd]-mid)*2; + e.o.sub(s); + swap(e.o[R[d]], e.o[C[d]]); + e.o.add(s); + ); +} + +void entselectionbox(const entity &e, vec &eo, vec &es) +{ + model *m = NULL; + const char *mname = entities::entmodel(e); + if(mname && (m = loadmodel(mname))) + { + m->collisionbox(eo, es); + if(es.x > es.y) es.y = es.x; else es.x = es.y; // square + es.z = (es.z + eo.z + 1 + entselradius)/2; // enclose ent radius box and model box + eo.x += e.o.x; + eo.y += e.o.y; + eo.z = e.o.z - entselradius + es.z; + } + else if(e.type == ET_MAPMODEL && (m = loadmapmodel(e.attr2))) + { + mmcollisionbox(e, m, eo, es); + es.max(entselradius); + eo.add(e.o); + } + else + { + es = vec(entselradius); + eo = e.o; + } + eo.sub(es); + es.mul(2); +} + +VAR(entselsnap, 0, 0, 1); +VAR(entmovingshadow, 0, 1, 1); + +extern void boxs(int orient, vec o, const vec &s, float size); +extern void boxs(int orient, vec o, const vec &s); +extern void boxs3D(const vec &o, vec s, int g); +extern bool editmoveplane(const vec &o, const vec &ray, int d, float off, vec &handle, vec &dest, bool first); + +int entmoving = 0; + +void entdrag(const vec &ray) +{ + if(noentedit() || !haveselent()) return; + + float r = 0, c = 0; + static vec v, handle; + vec eo, es; + int d = dimension(entorient), + dc= dimcoord(entorient); + + entfocus(entgroup.last(), + entselectionbox(e, eo, es); + + if(!editmoveplane(e.o, ray, d, eo[d] + (dc ? es[d] : 0), handle, v, entmoving==1)) + return; + + ivec g(v); + int z = g[d]&(~(sel.grid-1)); + g.add(sel.grid/2).mask(~(sel.grid-1)); + g[d] = z; + + r = (entselsnap ? g[R[d]] : v[R[d]]) - e.o[R[d]]; + c = (entselsnap ? g[C[d]] : v[C[d]]) - e.o[C[d]]; + ); + + if(entmoving==1) makeundoent(); + groupeditpure(e.o[R[d]] += r; e.o[C[d]] += c); + entmoving = 2; +} + +VAR(showentradius, 0, 1, 1); + +void renderentring(const extentity &e, float radius, int axis) +{ + if(radius <= 0) return; + gle::defvertex(); + gle::begin(GL_LINE_LOOP); + loopi(15) + { + vec p(e.o); + const vec2 &sc = sincos360[i*(360/15)]; + p[axis>=2 ? 1 : 0] += radius*sc.x; + p[axis>=1 ? 2 : 1] += radius*sc.y; + gle::attrib(p); + } + xtraverts += gle::end(); +} + +void renderentsphere(const extentity &e, float radius) +{ + if(radius <= 0) return; + loopk(3) renderentring(e, radius, k); +} + +void renderentattachment(const extentity &e) +{ + if(!e.attached) return; + gle::defvertex(); + gle::begin(GL_LINES); + gle::attrib(e.o); + gle::attrib(e.attached->o); + xtraverts += gle::end(); +} + +void renderentarrow(const extentity &e, const vec &dir, float radius) +{ + if(radius <= 0) return; + float arrowsize = min(radius/8, 0.5f); + vec target = vec(dir).mul(radius).add(e.o), arrowbase = vec(dir).mul(radius - arrowsize).add(e.o), spoke; + spoke.orthogonal(dir); + spoke.normalize(); + spoke.mul(arrowsize); + + gle::defvertex(); + + gle::begin(GL_LINES); + gle::attrib(e.o); + gle::attrib(target); + xtraverts += gle::end(); + + gle::begin(GL_TRIANGLE_FAN); + gle::attrib(target); + loopi(5) gle::attrib(vec(spoke).rotate(2*M_PI*i/4.0f, dir).add(arrowbase)); + xtraverts += gle::end(); +} + +void renderentcone(const extentity &e, const vec &dir, float radius, float angle) +{ + if(radius <= 0) return; + vec spot = vec(dir).mul(radius*cosf(angle*RAD)).add(e.o), spoke; + spoke.orthogonal(dir); + spoke.normalize(); + spoke.mul(radius*sinf(angle*RAD)); + + gle::defvertex(); + + gle::begin(GL_LINES); + loopi(8) + { + gle::attrib(e.o); + gle::attrib(vec(spoke).rotate(2*M_PI*i/8.0f, dir).add(spot)); + } + xtraverts += gle::end(); + + gle::begin(GL_LINE_LOOP); + loopi(8) gle::attrib(vec(spoke).rotate(2*M_PI*i/8.0f, dir).add(spot)); + xtraverts += gle::end(); +} + +void renderentradius(extentity &e, bool color) +{ + switch(e.type) + { + case ET_LIGHT: + if(color) gle::colorf(e.attr2/255.0f, e.attr3/255.0f, e.attr4/255.0f); + renderentsphere(e, e.attr1); + break; + + case ET_SPOTLIGHT: + if(e.attached) + { + if(color) gle::colorf(0, 1, 1); + float radius = e.attached->attr1; + if(!radius) radius = 2*e.o.dist(e.attached->o); + vec dir = vec(e.o).sub(e.attached->o).normalize(); + float angle = clamp(int(e.attr1), 1, 89); + renderentattachment(e); + renderentcone(*e.attached, dir, radius, angle); + } + break; + + case ET_SOUND: + if(color) gle::colorf(0, 1, 1); + renderentsphere(e, e.attr2); + break; + + case ET_ENVMAP: + { + extern int envmapradius; + if(color) gle::colorf(0, 1, 1); + renderentsphere(e, e.attr1 ? max(0, min(10000, int(e.attr1))) : envmapradius); + break; + } + + case ET_MAPMODEL: + case ET_PLAYERSTART: + { + if(color) gle::colorf(0, 1, 1); + entities::entradius(e, color); + vec dir; + vecfromyawpitch(e.attr1, 0, 1, 0, dir); + renderentarrow(e, dir, 4); + break; + } + + default: + if(e.type>=ET_GAMESPECIFIC) + { + if(color) gle::colorf(0, 1, 1); + entities::entradius(e, color); + } + break; + } +} + +static void renderentbox(const vec &eo, vec es) +{ + es.add(eo); + + // bottom quad + gle::attrib(eo.x, eo.y, eo.z); gle::attrib(es.x, eo.y, eo.z); + gle::attrib(es.x, eo.y, eo.z); gle::attrib(es.x, es.y, eo.z); + gle::attrib(es.x, es.y, eo.z); gle::attrib(eo.x, es.y, eo.z); + gle::attrib(eo.x, es.y, eo.z); gle::attrib(eo.x, eo.y, eo.z); + + // top quad + gle::attrib(eo.x, eo.y, es.z); gle::attrib(es.x, eo.y, es.z); + gle::attrib(es.x, eo.y, es.z); gle::attrib(es.x, es.y, es.z); + gle::attrib(es.x, es.y, es.z); gle::attrib(eo.x, es.y, es.z); + gle::attrib(eo.x, es.y, es.z); gle::attrib(eo.x, eo.y, es.z); + + // sides + gle::attrib(eo.x, eo.y, eo.z); gle::attrib(eo.x, eo.y, es.z); + gle::attrib(es.x, eo.y, eo.z); gle::attrib(es.x, eo.y, es.z); + gle::attrib(es.x, es.y, eo.z); gle::attrib(es.x, es.y, es.z); + gle::attrib(eo.x, es.y, eo.z); gle::attrib(eo.x, es.y, es.z); +} + +void renderentselection(const vec &o, const vec &ray, bool entmoving) +{ + if(noentedit()) return; + vec eo, es; + + if(entgroup.length()) + { + gle::colorub(0, 40, 0); + gle::defvertex(); + gle::begin(GL_LINES, entgroup.length()*24); + loopv(entgroup) entfocus(entgroup[i], + entselectionbox(e, eo, es); + renderentbox(eo, es); + ); + xtraverts += gle::end(); + } + + if(enthover >= 0) + { + gle::colorub(0, 40, 0); + entfocus(enthover, entselectionbox(e, eo, es)); // also ensures enthover is back in focus + boxs3D(eo, es, 1); + if(entmoving && entmovingshadow==1) + { + vec a, b; + gle::colorub(20, 20, 20); + (a = eo).x = eo.x - fmod(eo.x, worldsize); (b = es).x = a.x + worldsize; boxs3D(a, b, 1); + (a = eo).y = eo.y - fmod(eo.y, worldsize); (b = es).y = a.x + worldsize; boxs3D(a, b, 1); + (a = eo).z = eo.z - fmod(eo.z, worldsize); (b = es).z = a.x + worldsize; boxs3D(a, b, 1); + } + gle::colorub(150,0,0); + boxs(entorient, eo, es); + boxs(entorient, eo, es, clamp(0.015f*camera1->o.dist(eo)*tan(fovy*0.5f*RAD), 0.1f, 1.0f)); + } + + if(showentradius && (entgroup.length() || enthover >= 0)) + { + glDepthFunc(GL_GREATER); + gle::colorf(0.25f, 0.25f, 0.25f); + loopv(entgroup) entfocus(entgroup[i], renderentradius(e, false)); + if(enthover>=0) entfocus(enthover, renderentradius(e, false)); + glDepthFunc(GL_LESS); + loopv(entgroup) entfocus(entgroup[i], renderentradius(e, true)); + if(enthover>=0) entfocus(enthover, renderentradius(e, true)); + } +} + +bool enttoggle(int id) +{ + undonext = true; + int i = entgroup.find(id); + if(i < 0) + entadd(id); + else + entgroup.remove(i); + return i < 0; +} + +bool hoveringonent(int ent, int orient) +{ + if(noentedit()) return false; + entorient = orient; + if((efocus = enthover = ent) >= 0) + return true; + efocus = entgroup.empty() ? -1 : entgroup.last(); + enthover = -1; + return false; +} + +VAR(entitysurf, 0, 0, 1); + +ICOMMAND(entadd, "", (), +{ + if(enthover >= 0 && !noentedit()) + { + if(entgroup.find(enthover) < 0) entadd(enthover); + if(entmoving > 1) entmoving = 1; + } +}); + +ICOMMAND(enttoggle, "", (), +{ + if(enthover < 0 || noentedit() || !enttoggle(enthover)) { entmoving = 0; intret(0); } + else { if(entmoving > 1) entmoving = 1; intret(1); } +}); + +ICOMMAND(entmoving, "b", (int *n), +{ + if(*n >= 0) + { + if(!*n || enthover < 0 || noentedit()) entmoving = 0; + else + { + if(entgroup.find(enthover) < 0) { entadd(enthover); entmoving = 1; } + else if(!entmoving) entmoving = 1; + } + } + intret(entmoving); +}); + +void entpush(int *dir) +{ + if(noentedit()) return; + int d = dimension(entorient); + int s = dimcoord(entorient) ? -*dir : *dir; + if(entmoving) + { + groupeditpure(e.o[d] += float(s*sel.grid)); // editdrag supplies the undo + } + else + groupedit(e.o[d] += float(s*sel.grid)); + if(entitysurf==1) + { + player->o[d] += float(s*sel.grid); + player->resetinterp(); + } +} + +VAR(entautoviewdist, 0, 25, 100); +void entautoview(int *dir) +{ + if(!haveselent()) return; + static int s = 0; + vec v(player->o); + v.sub(worldpos); + v.normalize(); + v.mul(entautoviewdist); + int t = s + *dir; + s = abs(t) % entgroup.length(); + if(t<0 && s>0) s = entgroup.length() - s; + entfocus(entgroup[s], + v.add(e.o); + player->o = v; + player->resetinterp(); + ); +} + +COMMAND(entautoview, "i"); +COMMAND(entflip, ""); +COMMAND(entrotate, "i"); +COMMAND(entpush, "i"); + +void delent() +{ + if(noentedit()) return; + groupedit(e.type = ET_EMPTY;); + entcancel(); +} + +int findtype(char *what) +{ + for(int i = 0; *entities::entname(i); i++) if(strcmp(what, entities::entname(i))==0) return i; + conoutf(CON_ERROR, "unknown entity type \"%s\"", what); + return ET_EMPTY; +} + +VAR(entdrop, 0, 2, 3); + +bool dropentity(entity &e, int drop = -1) +{ + vec radius(4.0f, 4.0f, 4.0f); + if(drop<0) drop = entdrop; + if(e.type == ET_MAPMODEL) + { + model *m = loadmapmodel(e.attr2); + if(m) + { + vec center; + mmboundbox(e, m, center, radius); + radius.x += fabs(center.x); + radius.y += fabs(center.y); + } + radius.z = 0.0f; + } + switch(drop) + { + case 1: + if(e.type != ET_LIGHT && e.type != ET_SPOTLIGHT) + dropenttofloor(&e); + break; + case 2: + case 3: + int cx = 0, cy = 0; + if(sel.cxs == 1 && sel.cys == 1) + { + cx = (sel.cx ? 1 : -1) * sel.grid / 2; + cy = (sel.cy ? 1 : -1) * sel.grid / 2; + } + e.o = vec(sel.o); + int d = dimension(sel.orient), dc = dimcoord(sel.orient); + e.o[R[d]] += sel.grid / 2 + cx; + e.o[C[d]] += sel.grid / 2 + cy; + if(!dc) + e.o[D[d]] -= radius[D[d]]; + else + e.o[D[d]] += sel.grid + radius[D[d]]; + + if(drop == 3) + dropenttofloor(&e); + break; + } + return true; +} + +void dropent() +{ + if(noentedit()) return; + groupedit(dropentity(e)); +} + +void attachent() +{ + if(noentedit()) return; + groupedit(attachentity(e)); +} + +COMMAND(attachent, ""); + +VARP(entcamdir, 0, 1, 1); + +static int keepents = 0; + +extentity *newentity(bool local, const vec &o, int type, int v1, int v2, int v3, int v4, int v5, int &idx) +{ + vector &ents = entities::getents(); + if(local) + { + idx = -1; + for(int i = keepents; i < ents.length(); i++) if(ents[i]->type == ET_EMPTY) { idx = i; break; } + if(idx < 0 && ents.length() >= MAXENTS) { conoutf(CON_ERROR, "too many entities"); return NULL; } + } + else while(ents.length() < idx) ents.add(entities::newentity())->type = ET_EMPTY; + extentity &e = *entities::newentity(); + e.o = o; + e.attr1 = v1; + e.attr2 = v2; + e.attr3 = v3; + e.attr4 = v4; + e.attr5 = v5; + e.type = type; + e.reserved = 0; + e.light.color = vec(1, 1, 1); + e.light.dir = vec(0, 0, 1); + if(local) + { + if(entcamdir) switch(type) + { + case ET_MAPMODEL: + case ET_PLAYERSTART: + e.attr5 = e.attr4; + e.attr4 = e.attr3; + e.attr3 = e.attr2; + e.attr2 = e.attr1; + e.attr1 = (int)camera1->yaw; + break; + } + entities::fixentity(e); + } + if(ents.inrange(idx)) { entities::deleteentity(ents[idx]); ents[idx] = &e; } + else { idx = ents.length(); ents.add(&e); } + return &e; +} + +void newentity(int type, int a1, int a2, int a3, int a4, int a5) +{ + int idx; + extentity *t = newentity(true, player->o, type, a1, a2, a3, a4, a5, idx); + if(!t) return; + dropentity(*t); + t->type = ET_EMPTY; + enttoggle(idx); + makeundoent(); + entedit(idx, e.type = type); +} + +void newent(char *what, int *a1, int *a2, int *a3, int *a4, int *a5) +{ + if(noentedit()) return; + int type = findtype(what); + if(type != ET_EMPTY) + newentity(type, *a1, *a2, *a3, *a4, *a5); +} + +int entcopygrid; +vector entcopybuf; + +void entcopy() +{ + if(noentedit()) return; + entcopygrid = sel.grid; + entcopybuf.shrink(0); + loopv(entgroup) + entfocus(entgroup[i], entcopybuf.add(e).o.sub(vec(sel.o))); +} + +void entpaste() +{ + if(noentedit()) return; + if(entcopybuf.length()==0) return; + entcancel(); + float m = float(sel.grid)/float(entcopygrid); + loopv(entcopybuf) + { + entity &c = entcopybuf[i]; + vec o(c.o); + o.mul(m).add(vec(sel.o)); + int idx; + extentity *e = newentity(true, o, ET_EMPTY, c.attr1, c.attr2, c.attr3, c.attr4, c.attr5, idx); + if(!e) continue; + entadd(idx); + keepents = max(keepents, idx+1); + } + keepents = 0; + int j = 0; + groupeditundo(e.type = entcopybuf[j++].type;); +} + +COMMAND(newent, "siiiii"); +COMMAND(delent, ""); +COMMAND(dropent, ""); +COMMAND(entcopy, ""); +COMMAND(entpaste, ""); + +void entset(char *what, int *a1, int *a2, int *a3, int *a4, int *a5) +{ + if(noentedit()) return; + int type = findtype(what); + if(type != ET_EMPTY) + groupedit(e.type=type; + e.attr1=*a1; + e.attr2=*a2; + e.attr3=*a3; + e.attr4=*a4; + e.attr5=*a5); +} + +void printent(extentity &e, char *buf, int len) +{ + switch(e.type) + { + case ET_PARTICLES: + if(printparticles(e, buf, len)) return; + break; + + default: + if(e.type >= ET_GAMESPECIFIC && entities::printent(e, buf, len)) return; + break; + } + nformatstring(buf, len, "%s %d %d %d %d %d", entities::entname(e.type), e.attr1, e.attr2, e.attr3, e.attr4, e.attr5); +} + +void nearestent() +{ + if(noentedit()) return; + int closest = -1; + float closedist = 1e16f; + vector &ents = entities::getents(); + loopv(ents) + { + extentity &e = *ents[i]; + if(e.type == ET_EMPTY) continue; + float dist = e.o.dist(player->o); + if(dist < closedist) + { + closest = i; + closedist = dist; + } + } + if(closest >= 0 && entgroup.find(closest) < 0) entadd(closest); +} + +ICOMMAND(enthavesel,"", (), addimplicit(intret(entgroup.length()))); +ICOMMAND(entselect, "e", (uint *body), if(!noentedit()) addgroup(e.type != ET_EMPTY && entgroup.find(n)<0 && executebool(body))); +ICOMMAND(entloop, "e", (uint *body), if(!noentedit()) addimplicit(groupeditloop(((void)e, execute(body))))); +ICOMMAND(insel, "", (), entfocus(efocus, intret(pointinsel(sel, e.o)))); +ICOMMAND(entget, "", (), entfocus(efocus, string s; printent(e, s, sizeof(s)); result(s))); +ICOMMAND(entindex, "", (), intret(efocus)); +COMMAND(entset, "siiiii"); +COMMAND(nearestent, ""); + +void enttype(char *type, int *numargs) +{ + if(*numargs >= 1) + { + int typeidx = findtype(type); + if(typeidx != ET_EMPTY) groupedit(e.type = typeidx); + } + else entfocus(efocus, + { + result(entities::entname(e.type)); + }) +} + +void entattr(int *attr, int *val, int *numargs) +{ + if(*numargs >= 2) + { + if(*attr >= 0 && *attr <= 4) + groupedit( + switch(*attr) + { + case 0: e.attr1 = *val; break; + case 1: e.attr2 = *val; break; + case 2: e.attr3 = *val; break; + case 3: e.attr4 = *val; break; + case 4: e.attr5 = *val; break; + } + ); + } + else entfocus(efocus, + { + switch(*attr) + { + case 0: intret(e.attr1); break; + case 1: intret(e.attr2); break; + case 2: intret(e.attr3); break; + case 3: intret(e.attr4); break; + case 4: intret(e.attr5); break; + } + }); +} + +COMMAND(enttype, "sN"); +COMMAND(entattr, "iiN"); + +int findentity(int type, int index, int attr1, int attr2) +{ + const vector &ents = entities::getents(); + if(index > ents.length()) index = ents.length(); + else for(int i = index; i &spawninfos) +{ + const vector &ents = entities::getents(); + float total = 0.0f; + loopv(ents) + { + const extentity &e = *ents[i]; + if(e.type != ET_PLAYERSTART || e.attr2 != tag) continue; + spawninfo &s = spawninfos.add(); + s.e = &e; + s.weight = game::ratespawn(d, e); + total += s.weight; + } + return total; +} + +// Randomly picks a weighted spawn from the provided vector and removes it. +// The probability of a given spawn being picked is proportional to its weight. +// If all weights are zero, the index is picked uniformly. +static const extentity *poprandomspawn(vector &spawninfos, float &total) +{ + if(spawninfos.empty()) return NULL; + int index = 0; + if(total > 0.0f) + { + float x = rndscale(total); + do x -= spawninfos[index].weight; while(x > 0 && ++index < spawninfos.length()-1); + } + else index = rnd(spawninfos.length()); + spawninfo s = spawninfos.removeunordered(index); + total -= s.weight; + return s.e; +} + +static inline bool tryspawn(dynent *d, const extentity &e) +{ + d->o = e.o; + d->yaw = e.attr1; + return entinmap(d, true); +} + +void findplayerspawn(dynent *d, int forceent, int tag) +{ + const vector &ents = entities::getents(); + d->pitch = 0; + d->roll = 0; + if(ents.inrange(forceent) && tryspawn(d, *ents[forceent])) return; + vector spawninfos; + float total = gatherspawninfos(d, tag, spawninfos); + while(const extentity *e = poprandomspawn(spawninfos, total)) if(tryspawn(d, *e)) return; + d->o = vec(0.5f * worldsize).addz(1); + d->yaw = 0; + entinmap(d); +} + +void splitocta(cube *c, int size) +{ + if(size <= 0x1000) return; + loopi(8) + { + if(!c[i].children) c[i].children = newcubes(isempty(c[i]) ? F_EMPTY : F_SOLID); + splitocta(c[i].children, size>>1); + } +} + +void resetmap() +{ + clearoverrides(); + clearmapsounds(); + cleanreflections(); + resetblendmap(); + resetlightmaps(); + clearpvs(); + clearslots(); + clearparticles(); + cleardecals(); + cleardamagescreen(); + clearsleep(); + cancelsel(); + pruneundos(); + clearmapcrc(); + + entities::clearents(); + outsideents.setsize(0); +} + +void startmap(const char *name) +{ + game::startmap(name); +} + +bool emptymap(int scale, bool force, const char *mname, bool usecfg) // main empty world creation routine +{ + if(!force && !editmode) + { + conoutf(CON_ERROR, "newmap only allowed in edit mode"); + return false; + } + + resetmap(); + + setvar("mapscale", scale<10 ? 10 : (scale>16 ? 16 : scale), true, false); + setvar("mapsize", 1< 0x1000) splitocta(worldroot, worldsize>>1); + + clearmainmenu(); + + if(usecfg) + { + identflags |= IDF_OVERRIDDEN; + execfile("data/default_map_settings.cfg", false); + identflags &= ~IDF_OVERRIDDEN; + } + + initlights(); + allchanged(true); + + startmap(mname); + + return true; +} + +bool enlargemap(bool force) +{ + if(!force && !editmode) + { + conoutf(CON_ERROR, "mapenlarge only allowed in edit mode"); + return false; + } + if(worldsize >= 1<<16) return false; + + while(outsideents.length()) removeentity(outsideents.pop()); + + worldscale++; + worldsize *= 2; + cube *c = newcubes(F_EMPTY); + c[0].children = worldroot; + loopi(3) solidfaces(c[i+1]); + worldroot = c; + + if(worldsize > 0x1000) splitocta(worldroot, worldsize>>1); + + enlargeblendmap(); + + allchanged(); + + return true; +} + +static bool isallempty(cube &c) +{ + if(!c.children) return isempty(c); + loopi(8) if(!isallempty(c.children[i])) return false; + return true; +} + +void shrinkmap() +{ + extern int nompedit; + if(noedit(true) || (nompedit && multiplayer())) return; + if(worldsize <= 1<<10) return; + + int octant = -1; + loopi(8) if(!isallempty(worldroot[i])) + { + if(octant >= 0) return; + octant = i; + } + if(octant < 0) return; + + while(outsideents.length()) removeentity(outsideents.pop()); + + if(!worldroot[octant].children) subdividecube(worldroot[octant], false, false); + cube *root = worldroot[octant].children; + worldroot[octant].children = NULL; + freeocta(worldroot); + worldroot = root; + worldscale--; + worldsize /= 2; + + ivec offset(octant, ivec(0, 0, 0), worldsize); + vector &ents = entities::getents(); + loopv(ents) ents[i]->o.sub(vec(offset)); + + shrinkblendmap(octant); + + allchanged(); + + conoutf("shrunk map to size %d", worldscale); +} + +void newmap(int *i) { bool force = !isconnected(); if(force) game::forceedit(""); if(emptymap(*i, force, NULL)) game::newmap(max(*i, 0)); } +void mapenlarge() { if(enlargemap(false)) game::newmap(-1); } +COMMAND(newmap, "i"); +COMMAND(mapenlarge, ""); +COMMAND(shrinkmap, ""); + +void mapname() +{ + result(game::getclientmap()); +} + +COMMAND(mapname, ""); + +void mpeditent(int i, const vec &o, int type, int attr1, int attr2, int attr3, int attr4, int attr5, bool local) +{ + if(i < 0 || i >= MAXENTS) return; + vector &ents = entities::getents(); + if(ents.length()<=i) + { + extentity *e = newentity(local, o, type, attr1, attr2, attr3, attr4, attr5, i); + if(!e) return; + addentity(i); + attachentity(*e); + } + else + { + extentity &e = *ents[i]; + removeentity(i); + int oldtype = e.type; + if(oldtype!=type) detachentity(e); + e.type = type; + e.o = o; + e.attr1 = attr1; e.attr2 = attr2; e.attr3 = attr3; e.attr4 = attr4; e.attr5 = attr5; + addentity(i); + if(oldtype!=type) attachentity(e); + } + entities::editent(i, local); +} + +int getworldsize() { return worldsize; } +int getmapversion() { return mapversion; } + diff --git a/src/engine/world.h b/src/engine/world.h new file mode 100644 index 0000000..9c5be78 --- /dev/null +++ b/src/engine/world.h @@ -0,0 +1,59 @@ + +enum // hardcoded texture numbers +{ + DEFAULT_SKY = 0, + DEFAULT_GEOM +}; + +#define MAPVERSION 33 // bump if map format changes, see worldio.cpp + +struct octaheader +{ + char magic[4]; // "OCTA" + int version; // any >8bit quantity is little endian + int headersize; // sizeof(header) + int worldsize; + int numents; + int numpvs; + int lightmaps; + int blendmap; + int numvars; + int numvslots; +}; + +struct compatheader // map file format header +{ + char magic[4]; // "OCTA" + int version; // any >8bit quantity is little endian + int headersize; // sizeof(header) + int worldsize; + int numents; + int numpvs; + int lightmaps; + int lightprecision, lighterror, lightlod; + uchar ambient; + uchar watercolour[3]; + uchar blendmap; + uchar lerpangle, lerpsubdiv, lerpsubdivsize; + uchar bumperror; + uchar skylight[3]; + uchar lavacolour[3]; + uchar waterfallcolour[3]; + uchar reserved[10]; + char maptitle[128]; +}; + +#define WATER_AMPLITUDE 0.4f +#define WATER_OFFSET 1.1f + +enum +{ + MATSURF_NOT_VISIBLE = 0, + MATSURF_VISIBLE, + MATSURF_EDIT_ONLY +}; + +#define TEX_SCALE 8.0f + +struct vertex { vec pos; bvec4 norm; vec2 tc; svec2 lm; bvec4 tangent; }; + diff --git a/src/engine/worldio.cpp b/src/engine/worldio.cpp new file mode 100644 index 0000000..514b45e --- /dev/null +++ b/src/engine/worldio.cpp @@ -0,0 +1,1388 @@ +// worldio.cpp: loading & saving of maps and savegames + +#include "engine.h" + +void validmapname(char *dst, const char *src, const char *prefix = NULL, const char *alt = "untitled", size_t maxlen = 100) +{ + if(prefix) while(*prefix) *dst++ = *prefix++; + const char *start = dst; + if(src) loopi(maxlen) + { + char c = *src++; + if(iscubealnum(c) || c == '_' || c == '-' || c == '/' || c == '\\') *dst++ = c; + else break; + } + if(dst > start) *dst = '\0'; + else if(dst != alt) copystring(dst, alt, maxlen); +} + +void fixmapname(char *name) +{ + validmapname(name, name, NULL, ""); +} + +void getmapfilenames(const char *fname, const char *cname, char *pakname, char *mapname, char *cfgname) +{ + if(!cname) cname = fname; + string name; + validmapname(name, cname); + char *slash = strpbrk(name, "/\\"); + if(slash) + { + copystring(pakname, name, slash-name+1); + copystring(cfgname, slash+1, MAXSTRLEN); + } + else + { + copystring(pakname, "base", MAXSTRLEN); + copystring(cfgname, name, MAXSTRLEN); + } + validmapname(mapname, fname, strpbrk(fname, "/\\") ? NULL : "base/"); +} + +static void fixent(entity &e, int version) +{ + if(version <= 10 && e.type >= 7) e.type++; + if(version <= 12 && e.type >= 8) e.type++; + if(version <= 14 && e.type >= ET_MAPMODEL && e.type <= 16) + { + if(e.type == 16) e.type = ET_MAPMODEL; + else e.type++; + } + if(version <= 20 && e.type >= ET_ENVMAP) e.type++; + if(version <= 21 && e.type >= ET_PARTICLES) e.type++; + if(version <= 22 && e.type >= ET_SOUND) e.type++; + if(version <= 23 && e.type >= ET_SPOTLIGHT) e.type++; + if(version <= 30 && (e.type == ET_MAPMODEL || e.type == ET_PLAYERSTART)) e.attr1 = (int(e.attr1)+180)%360; + if(version <= 31 && e.type == ET_MAPMODEL) { int yaw = (int(e.attr1)%360 + 360)%360 + 7; e.attr1 = yaw - yaw%15; } +} + +bool loadents(const char *fname, vector &ents, uint *crc) +{ + string pakname, mapname, mcfgname, ogzname; + getmapfilenames(fname, NULL, pakname, mapname, mcfgname); + formatstring(ogzname, "packages/%s.ogz", mapname); + path(ogzname); + stream *f = opengzfile(ogzname, "rb"); + if(!f) return false; + octaheader hdr; + if(f->read(&hdr, 7*sizeof(int)) != 7*sizeof(int)) { conoutf(CON_ERROR, "map %s has malformatted header", ogzname); delete f; return false; } + lilswap(&hdr.version, 6); + if(memcmp(hdr.magic, "OCTA", 4) || hdr.worldsize <= 0|| hdr.numents < 0) { conoutf(CON_ERROR, "map %s has malformatted header", ogzname); delete f; return false; } + if(hdr.version>MAPVERSION) { conoutf(CON_ERROR, "map %s requires a newer version of Cube 2: Sauerbraten", ogzname); delete f; return false; } + compatheader chdr; + if(hdr.version <= 28) + { + if(f->read(&chdr.lightprecision, sizeof(chdr) - 7*sizeof(int)) != sizeof(chdr) - 7*sizeof(int)) { conoutf(CON_ERROR, "map %s has malformatted header", ogzname); delete f; return false; } + } + else + { + int extra = 0; + if(hdr.version <= 29) extra++; + if(f->read(&hdr.blendmap, sizeof(hdr) - (7+extra)*sizeof(int)) != sizeof(hdr) - (7+extra)*sizeof(int)) { conoutf(CON_ERROR, "map %s has malformatted header", ogzname); delete f; return false; } + } + + if(hdr.version <= 28) + { + lilswap(&chdr.lightprecision, 3); + hdr.blendmap = chdr.blendmap; + hdr.numvars = 0; + hdr.numvslots = 0; + } + else + { + lilswap(&hdr.blendmap, 2); + if(hdr.version <= 29) hdr.numvslots = 0; + else lilswap(&hdr.numvslots, 1); + } + + loopi(hdr.numvars) + { + int type = f->getchar(), ilen = f->getlil(); + f->seek(ilen, SEEK_CUR); + switch(type) + { + case ID_VAR: f->getlil(); break; + case ID_FVAR: f->getlil(); break; + case ID_SVAR: { int slen = f->getlil(); f->seek(slen, SEEK_CUR); break; } + } + } + + string gametype; + copystring(gametype, "fps"); + bool samegame = true; + int eif = 0; + if(hdr.version>=16) + { + int len = f->getchar(); + f->read(gametype, len+1); + } + if(strcmp(gametype, game::gameident())) + { + samegame = false; + conoutf(CON_WARN, "WARNING: loading map from %s game, ignoring entities except for lights/mapmodels", gametype); + } + if(hdr.version>=16) + { + eif = f->getlil(); + int extrasize = f->getlil(); + f->seek(extrasize, SEEK_CUR); + } + + if(hdr.version<14) + { + f->seek(256, SEEK_CUR); + } + else + { + ushort nummru = f->getlil(); + f->seek(nummru*sizeof(ushort), SEEK_CUR); + } + + loopi(min(hdr.numents, MAXENTS)) + { + entity &e = ents.add(); + f->read(&e, sizeof(entity)); + lilswap(&e.o.x, 3); + lilswap(&e.attr1, 5); + fixent(e, hdr.version); + if(eif > 0) f->seek(eif, SEEK_CUR); + if(samegame) + { + entities::readent(e, NULL, hdr.version); + } + else if(e.type>=ET_GAMESPECIFIC || hdr.version<=14) + { + ents.pop(); + continue; + } + } + + if(crc) + { + f->seek(0, SEEK_END); + *crc = f->getcrc(); + } + + delete f; + + return true; +} + +#ifndef STANDALONE +string ogzname, bakname, cfgname, picname; + +VARP(savebak, 0, 2, 2); + +void setmapfilenames(const char *fname, const char *cname = NULL) +{ + string pakname, mapname, mcfgname; + getmapfilenames(fname, cname, pakname, mapname, mcfgname); + + formatstring(ogzname, "packages/%s.ogz", mapname); + if(savebak==1) formatstring(bakname, "packages/%s.BAK", mapname); + else formatstring(bakname, "packages/%s_%d.BAK", mapname, totalmillis); + formatstring(cfgname, "packages/%s/%s.cfg", pakname, mcfgname); + formatstring(picname, "packages/%s.jpg", mapname); + + path(ogzname); + path(bakname); + path(cfgname); + path(picname); +} + +void mapcfgname() +{ + const char *mname = game::getclientmap(); + string pakname, mapname, mcfgname; + getmapfilenames(mname, NULL, pakname, mapname, mcfgname); + defformatstring(cfgname, "packages/%s/%s.cfg", pakname, mcfgname); + path(cfgname); + result(cfgname); +} + +COMMAND(mapcfgname, ""); + +void backup(char *name, char *backupname) +{ + string backupfile; + copystring(backupfile, findfile(backupname, "wb")); + remove(backupfile); + rename(findfile(name, "wb"), backupfile); +} + +enum { OCTSAV_CHILDREN = 0, OCTSAV_EMPTY, OCTSAV_SOLID, OCTSAV_NORMAL, OCTSAV_LODCUBE }; + +static int savemapprogress = 0; + +void savec(cube *c, const ivec &o, int size, stream *f, bool nolms) +{ + if((savemapprogress++&0xFFF)==0) renderprogress(float(savemapprogress)/allocnodes, "saving octree..."); + + loopi(8) + { + ivec co(i, o, size); + if(c[i].children) + { + f->putchar(OCTSAV_CHILDREN); + savec(c[i].children, co, size>>1, f, nolms); + } + else + { + int oflags = 0, surfmask = 0, totalverts = 0; + if(c[i].material!=MAT_AIR) oflags |= 0x40; + if(isempty(c[i])) f->putchar(oflags | OCTSAV_EMPTY); + else + { + if(!nolms) + { + if(c[i].merged) oflags |= 0x80; + if(c[i].ext) loopj(6) + { + const surfaceinfo &surf = c[i].ext->surfaces[j]; + if(!surf.used()) continue; + oflags |= 0x20; + surfmask |= 1<putchar(oflags | OCTSAV_SOLID); + else + { + f->putchar(oflags | OCTSAV_NORMAL); + f->write(c[i].edges, 12); + } + } + + loopj(6) f->putlil(c[i].texture[j]); + + if(oflags&0x40) f->putlil(c[i].material); + if(oflags&0x80) f->putchar(c[i].merged); + if(oflags&0x20) + { + f->putchar(surfmask); + f->putchar(totalverts); + loopj(6) if(surfmask&(1<surfaces[j]; + vertinfo *verts = c[i].ext->verts() + surf.verts; + int layerverts = surf.numverts&MAXFACEVERTS, numverts = surf.totalverts(), + vertmask = 0, vertorder = 0, uvorder = 0, + dim = dimension(j), vc = C[dim], vr = R[dim]; + if(numverts) + { + if(c[i].merged&(1<write(&surf, sizeof(surfaceinfo)); + bool hasxyz = (vertmask&0x04)!=0, hasuv = (vertmask&0x40)!=0, hasnorm = (vertmask&0x80)!=0; + if(layerverts == 4) + { + if(hasxyz && vertmask&0x01) + { + ivec v0 = verts[vertorder].getxyz(), v2 = verts[(vertorder+2)&3].getxyz(); + f->putlil(v0[vc]); f->putlil(v0[vr]); + f->putlil(v2[vc]); f->putlil(v2[vr]); + hasxyz = false; + } + if(hasuv && vertmask&0x02) + { + const vertinfo &v0 = verts[uvorder], &v2 = verts[(uvorder+2)&3]; + f->putlil(v0.u); f->putlil(v0.v); + f->putlil(v2.u); f->putlil(v2.v); + if(surf.numverts&LAYER_DUP) + { + const vertinfo &b0 = verts[4+uvorder], &b2 = verts[4+((uvorder+2)&3)]; + f->putlil(b0.u); f->putlil(b0.v); + f->putlil(b2.u); f->putlil(b2.v); + } + hasuv = false; + } + } + if(hasnorm && vertmask&0x08) { f->putlil(verts[0].norm); hasnorm = false; } + if(hasxyz || hasuv || hasnorm) loopk(layerverts) + { + const vertinfo &v = verts[(k+vertorder)%layerverts]; + if(hasxyz) + { + ivec xyz = v.getxyz(); + f->putlil(xyz[vc]); f->putlil(xyz[vr]); + } + if(hasuv) { f->putlil(v.u); f->putlil(v.v); } + if(hasnorm) f->putlil(v.norm); + } + if(surf.numverts&LAYER_DUP) loopk(layerverts) + { + const vertinfo &v = verts[layerverts + (k+vertorder)%layerverts]; + if(hasuv) { f->putlil(v.u); f->putlil(v.v); } + } + } + } + } + } +} + +struct surfacecompat +{ + uchar texcoords[8]; + uchar w, h; + ushort x, y; + uchar lmid, layer; +}; + +struct normalscompat +{ + bvec normals[4]; +}; + +struct mergecompat +{ + ushort u1, u2, v1, v2; +}; + +cube *loadchildren(stream *f, const ivec &co, int size, bool &failed); + +void convertoldsurfaces(cube &c, const ivec &co, int size, surfacecompat *srcsurfs, int hassurfs, normalscompat *normals, int hasnorms, mergecompat *merges, int hasmerges) +{ + surfaceinfo dstsurfs[6]; + vertinfo verts[6*2*MAXFACEVERTS]; + int totalverts = 0, numsurfs = 6; + memset(dstsurfs, 0, sizeof(dstsurfs)); + loopi(6) if((hassurfs|hasnorms|hasmerges)&(1<layer&2) + { + blend = &srcsurfs[numsurfs++]; + dst.lmid[0] = src->lmid; + dst.lmid[1] = blend->lmid; + dst.numverts |= LAYER_BLEND; + if(blend->lmid >= LMID_RESERVED && (src->x != blend->x || src->y != blend->y || src->w != blend->w || src->h != blend->h || memcmp(src->texcoords, blend->texcoords, sizeof(src->texcoords)))) + dst.numverts |= LAYER_DUP; + } + else if(src->layer == 1) { dst.lmid[1] = src->lmid; dst.numverts |= LAYER_BOTTOM; } + else { dst.lmid[0] = src->lmid; dst.numverts |= LAYER_TOP; } + } + else dst.numverts |= LAYER_TOP; + bool uselms = hassurfs&(1<= LMID_RESERVED || dst.lmid[1] >= LMID_RESERVED || dst.numverts&~LAYER_TOP), + usemerges = hasmerges&(1< 0 && (pos[k] == pos[0] || pos[k] == pos[k-1])) continue; + vertinfo &dv = curverts[numverts++]; + dv.setxyz(pos[k]); + if(uselms) + { + float u = src->x + (src->texcoords[k*2] / 255.0f) * (src->w - 1), + v = src->y + (src->texcoords[k*2+1] / 255.0f) * (src->h - 1); + dv.u = ushort(floor(clamp((u) * float(USHRT_MAX+1)/LM_PACKW + 0.5f, 0.0f, float(USHRT_MAX)))); + dv.v = ushort(floor(clamp((v) * float(USHRT_MAX+1)/LM_PACKH + 0.5f, 0.0f, float(USHRT_MAX)))); + } + else dv.u = dv.v = 0; + dv.norm = usenorms && normals[i].normals[k] != bvec(128, 128, 128) ? encodenormal(normals[i].normals[k].tonormal().normalize()) : 0; + } + dst.verts = totalverts; + dst.numverts |= numverts; + totalverts += numverts; + if(dst.numverts&LAYER_DUP) loopk(4) + { + if(k > 0 && (pos[k] == pos[0] || pos[k] == pos[k-1])) continue; + vertinfo &bv = verts[totalverts++]; + bv.setxyz(pos[k]); + bv.u = ushort(floor(clamp((blend->x + (blend->texcoords[k*2] / 255.0f) * (blend->w - 1)) * float(USHRT_MAX+1)/LM_PACKW, 0.0f, float(USHRT_MAX)))); + bv.v = ushort(floor(clamp((blend->y + (blend->texcoords[k*2+1] / 255.0f) * (blend->h - 1)) * float(USHRT_MAX+1)/LM_PACKH, 0.0f, float(USHRT_MAX)))); + bv.norm = usenorms && normals[i].normals[k] != bvec(128, 128, 128) ? encodenormal(normals[i].normals[k].tonormal().normalize()) : 0; + } + } + } + setsurfaces(c, dstsurfs, verts, totalverts); +} + +static inline int convertoldmaterial(int mat) +{ + return ((mat&7)<>3)&3)<>5)&7)<getchar(); + switch(octsav&0x7) + { + case OCTSAV_CHILDREN: + c.children = loadchildren(f, co, size>>1, failed); + return; + + case OCTSAV_LODCUBE: haschildren = true; break; + case OCTSAV_EMPTY: emptyfaces(c); break; + case OCTSAV_SOLID: solidfaces(c); break; + case OCTSAV_NORMAL: f->read(c.edges, 12); break; + default: failed = true; return; + } + loopi(6) c.texture[i] = mapversion<14 ? f->getchar() : f->getlil(); + if(mapversion < 7) f->seek(3, SEEK_CUR); + else if(mapversion <= 31) + { + uchar mask = f->getchar(); + if(mask & 0x80) + { + int mat = f->getchar(); + if(mapversion < 27) + { + static const ushort matconv[] = { MAT_AIR, MAT_WATER, MAT_CLIP, MAT_GLASS|MAT_CLIP, MAT_NOCLIP, MAT_LAVA|MAT_DEATH, MAT_GAMECLIP, MAT_DEATH }; + c.material = size_t(mat) < sizeof(matconv)/sizeof(matconv[0]) ? matconv[mat] : MAT_AIR; + } + else c.material = convertoldmaterial(mat); + } + surfacecompat surfaces[12]; + normalscompat normals[6]; + mergecompat merges[6]; + int hassurfs = 0, hasnorms = 0, hasmerges = 0; + if(mask & 0x3F) + { + int numsurfs = 6; + loopi(numsurfs) + { + if(i >= 6 || mask & (1 << i)) + { + f->read(&surfaces[i], sizeof(surfacecompat)); + lilswap(&surfaces[i].x, 2); + if(mapversion < 10) ++surfaces[i].lmid; + if(mapversion < 18) + { + if(surfaces[i].lmid >= LMID_AMBIENT1) ++surfaces[i].lmid; + if(surfaces[i].lmid >= LMID_BRIGHT1) ++surfaces[i].lmid; + } + if(mapversion < 19) + { + if(surfaces[i].lmid >= LMID_DARK) surfaces[i].lmid += 2; + } + if(i < 6) + { + if(mask & 0x40) { hasnorms |= 1<read(&normals[i], sizeof(normalscompat)); } + if(surfaces[i].layer != 0 || surfaces[i].lmid != LMID_AMBIENT) + hassurfs |= 1<>4) | ((hassurfs&0x03)<<4); + } + } + if(mapversion >= 20) + { + if(octsav&0x80) + { + int merged = f->getchar(); + c.merged = merged&0x3F; + if(merged&0x80) + { + int mask = f->getchar(); + if(mask) + { + hasmerges = mask&0x3F; + loopi(6) if(mask&(1<read(m, sizeof(mergecompat)); + lilswap(&m->u1, 4); + if(mapversion <= 25) + { + int uorigin = m->u1 & 0xE000, vorigin = m->v1 & 0xE000; + m->u1 = (m->u1 - uorigin) << 2; + m->u2 = (m->u2 - uorigin) << 2; + m->v1 = (m->v1 - vorigin) << 2; + m->v2 = (m->v2 - vorigin) << 2; + } + } + } + } + } + } + if(hassurfs || hasnorms || hasmerges) + convertoldsurfaces(c, co, size, surfaces, hassurfs, normals, hasnorms, merges, hasmerges); + } + else + { + if(octsav&0x40) + { + if(mapversion <= 32) + { + int mat = f->getchar(); + c.material = convertoldmaterial(mat); + } + else c.material = f->getlil(); + } + if(octsav&0x80) c.merged = f->getchar(); + if(octsav&0x20) + { + int surfmask, totalverts; + surfmask = f->getchar(); + totalverts = max(f->getchar(), 0); + newcubeext(c, totalverts, false); + memset(c.ext->surfaces, 0, sizeof(c.ext->surfaces)); + memset(c.ext->verts(), 0, totalverts*sizeof(vertinfo)); + int offset = 0; + loopi(6) if(surfmask&(1<surfaces[i]; + f->read(&surf, sizeof(surfaceinfo)); + int vertmask = surf.verts, numverts = surf.totalverts(); + if(!numverts) { surf.verts = 0; continue; } + surf.verts = offset; + vertinfo *verts = c.ext->verts() + offset; + offset += numverts; + ivec v[4], n, vo = ivec(co).mask(0xFFF).shl(3); + int layerverts = surf.numverts&MAXFACEVERTS, dim = dimension(i), vc = C[dim], vr = R[dim], bias = 0; + genfaceverts(c, i, v); + bool hasxyz = (vertmask&0x04)!=0, hasuv = (vertmask&0x40)!=0, hasnorm = (vertmask&0x80)!=0; + if(hasxyz) + { + ivec e1, e2, e3; + n.cross((e1 = v[1]).sub(v[0]), (e2 = v[2]).sub(v[0])); + if(n.iszero()) n.cross(e2, (e3 = v[3]).sub(v[0])); + bias = -n.dot(ivec(v[0]).mul(size).add(vo)); + } + else + { + int vis = layerverts < 4 ? (vertmask&0x02 ? 2 : 1) : 3, order = vertmask&0x01 ? 1 : 0, k = 0; + verts[k++].setxyz(v[order].mul(size).add(vo)); + if(vis&1) verts[k++].setxyz(v[order+1].mul(size).add(vo)); + verts[k++].setxyz(v[order+2].mul(size).add(vo)); + if(vis&2) verts[k++].setxyz(v[(order+3)&3].mul(size).add(vo)); + } + if(layerverts == 4) + { + if(hasxyz && vertmask&0x01) + { + ushort c1 = f->getlil(), r1 = f->getlil(), c2 = f->getlil(), r2 = f->getlil(); + ivec xyz; + xyz[vc] = c1; xyz[vr] = r1; xyz[dim] = n[dim] ? -(bias + n[vc]*xyz[vc] + n[vr]*xyz[vr])/n[dim] : vo[dim]; + verts[0].setxyz(xyz); + xyz[vc] = c1; xyz[vr] = r2; xyz[dim] = n[dim] ? -(bias + n[vc]*xyz[vc] + n[vr]*xyz[vr])/n[dim] : vo[dim]; + verts[1].setxyz(xyz); + xyz[vc] = c2; xyz[vr] = r2; xyz[dim] = n[dim] ? -(bias + n[vc]*xyz[vc] + n[vr]*xyz[vr])/n[dim] : vo[dim]; + verts[2].setxyz(xyz); + xyz[vc] = c2; xyz[vr] = r1; xyz[dim] = n[dim] ? -(bias + n[vc]*xyz[vc] + n[vr]*xyz[vr])/n[dim] : vo[dim]; + verts[3].setxyz(xyz); + hasxyz = false; + } + if(hasuv && vertmask&0x02) + { + int uvorder = (vertmask&0x30)>>4; + vertinfo &v0 = verts[uvorder], &v1 = verts[(uvorder+1)&3], &v2 = verts[(uvorder+2)&3], &v3 = verts[(uvorder+3)&3]; + v0.u = f->getlil(); v0.v = f->getlil(); + v2.u = f->getlil(); v2.v = f->getlil(); + v1.u = v0.u; v1.v = v2.v; + v3.u = v2.u; v3.v = v0.v; + if(surf.numverts&LAYER_DUP) + { + vertinfo &b0 = verts[4+uvorder], &b1 = verts[4+((uvorder+1)&3)], &b2 = verts[4+((uvorder+2)&3)], &b3 = verts[4+((uvorder+3)&3)]; + b0.u = f->getlil(); b0.v = f->getlil(); + b2.u = f->getlil(); b2.v = f->getlil(); + b1.u = b0.u; b1.v = b2.v; + b3.u = b2.u; b3.v = b0.v; + } + hasuv = false; + } + } + if(hasnorm && vertmask&0x08) + { + ushort norm = f->getlil(); + loopk(layerverts) verts[k].norm = norm; + hasnorm = false; + } + if(hasxyz || hasuv || hasnorm) loopk(layerverts) + { + vertinfo &v = verts[k]; + if(hasxyz) + { + ivec xyz; + xyz[vc] = f->getlil(); xyz[vr] = f->getlil(); + xyz[dim] = n[dim] ? -(bias + n[vc]*xyz[vc] + n[vr]*xyz[vr])/n[dim] : vo[dim]; + v.setxyz(xyz); + } + if(hasuv) { v.u = f->getlil(); v.v = f->getlil(); } + if(hasnorm) v.norm = f->getlil(); + } + if(surf.numverts&LAYER_DUP) loopk(layerverts) + { + vertinfo &v = verts[k+layerverts], &t = verts[k]; + v.setxyz(t.x, t.y, t.z); + if(hasuv) { v.u = f->getlil(); v.v = f->getlil(); } + v.norm = t.norm; + } + } + } + } + + c.children = (haschildren ? loadchildren(f, co, size>>1, failed) : NULL); +} + +cube *loadchildren(stream *f, const ivec &co, int size, bool &failed) +{ + cube *c = newcubes(); + loopi(8) + { + loadc(f, c[i], ivec(i, co, size), size, failed); + if(failed) break; + } + return c; +} + +VAR(dbgvars, 0, 0, 1); + +void savevslot(stream *f, VSlot &vs, int prev) +{ + f->putlil(vs.changed); + f->putlil(prev); + if(vs.changed & (1<putlil(vs.params.length()); + loopv(vs.params) + { + SlotShaderParam &p = vs.params[i]; + f->putlil(strlen(p.name)); + f->write(p.name, strlen(p.name)); + loopk(4) f->putlil(p.val[k]); + } + } + if(vs.changed & (1<putlil(vs.scale); + if(vs.changed & (1<putlil(vs.rotation); + if(vs.changed & (1<putlil(vs.offset.x); + f->putlil(vs.offset.y); + } + if(vs.changed & (1<putlil(vs.scroll.x); + f->putlil(vs.scroll.y); + } + if(vs.changed & (1<putlil(vs.layer); + if(vs.changed & (1<putlil(vs.alphafront); + f->putlil(vs.alphaback); + } + if(vs.changed & (1<putlil(vs.colorscale[k]); + } +} + +void savevslots(stream *f, int numvslots) +{ + if(vslots.empty()) return; + int *prev = new int[numvslots]; + memset(prev, -1, numvslots*sizeof(int)); + loopi(numvslots) + { + VSlot *vs = vslots[i]; + if(vs->changed) continue; + for(;;) + { + VSlot *cur = vs; + do vs = vs->next; while(vs && vs->index >= numvslots); + if(!vs) break; + prev[vs->index] = cur->index; + } + } + int lastroot = 0; + loopi(numvslots) + { + VSlot &vs = *vslots[i]; + if(!vs.changed) continue; + if(lastroot < i) f->putlil(-(i - lastroot)); + savevslot(f, vs, prev[i]); + lastroot = i+1; + } + if(lastroot < numvslots) f->putlil(-(numvslots - lastroot)); + delete[] prev; +} + +void loadvslot(stream *f, VSlot &vs, int changed) +{ + vs.changed = changed; + if(vs.changed & (1<getlil(); + string name; + loopi(numparams) + { + SlotShaderParam &p = vs.params.add(); + int nlen = f->getlil(); + f->read(name, min(nlen, MAXSTRLEN-1)); + name[min(nlen, MAXSTRLEN-1)] = '\0'; + if(nlen >= MAXSTRLEN) f->seek(nlen - (MAXSTRLEN-1), SEEK_CUR); + p.name = getshaderparamname(name); + p.loc = -1; + loopk(4) p.val[k] = f->getlil(); + } + } + if(vs.changed & (1<getlil(); + if(vs.changed & (1<getlil(), 0, 7); + if(vs.changed & (1<getlil(); + vs.offset.y = f->getlil(); + } + if(vs.changed & (1<getlil(); + vs.scroll.y = f->getlil(); + } + if(vs.changed & (1<getlil(); + if(vs.changed & (1<getlil(); + vs.alphaback = f->getlil(); + } + if(vs.changed & (1<getlil(); + } +} + +void loadvslots(stream *f, int numvslots) +{ + int *prev = new (false) int[numvslots]; + if(!prev) return; + memset(prev, -1, numvslots*sizeof(int)); + while(numvslots > 0) + { + int changed = f->getlil(); + if(changed < 0) + { + loopi(-changed) vslots.add(new VSlot(NULL, vslots.length())); + numvslots += changed; + } + else + { + prev[vslots.length()] = f->getlil(); + loadvslot(f, *vslots.add(new VSlot(NULL, vslots.length())), changed); + numvslots--; + } + } + loopv(vslots) if(vslots.inrange(prev[i])) vslots[prev[i]]->next = vslots[i]; + delete[] prev; +} + +bool save_world(const char *mname, bool nolms) +{ + if(!*mname) mname = game::getclientmap(); + setmapfilenames(mname); + if(savebak) backup(ogzname, bakname); + stream *f = opengzfile(ogzname, "wb"); + if(!f) { conoutf(CON_WARN, "could not write map to %s", ogzname); return false; } + + int numvslots = vslots.length(); + if(!nolms && !multiplayer(false)) + { + numvslots = compactvslots(); + allchanged(); + } + + savemapprogress = 0; + renderprogress(0, "saving map..."); + + octaheader hdr; + memcpy(hdr.magic, "OCTA", 4); + hdr.version = MAPVERSION; + hdr.headersize = sizeof(hdr); + hdr.worldsize = worldsize; + hdr.numents = 0; + const vector &ents = entities::getents(); + loopv(ents) if(ents[i]->type!=ET_EMPTY || nolms) hdr.numents++; + hdr.numpvs = nolms ? 0 : getnumviewcells(); + hdr.lightmaps = nolms ? 0 : lightmaps.length(); + hdr.blendmap = shouldsaveblendmap(); + hdr.numvars = 0; + hdr.numvslots = numvslots; + enumerate(idents, ident, id, + { + if((id.type == ID_VAR || id.type == ID_FVAR || id.type == ID_SVAR) && id.flags&IDF_OVERRIDE && !(id.flags&IDF_READONLY) && id.flags&IDF_OVERRIDDEN) hdr.numvars++; + }); + lilswap(&hdr.version, 9); + f->write(&hdr, sizeof(hdr)); + + enumerate(idents, ident, id, + { + if((id.type!=ID_VAR && id.type!=ID_FVAR && id.type!=ID_SVAR) || !(id.flags&IDF_OVERRIDE) || id.flags&IDF_READONLY || !(id.flags&IDF_OVERRIDDEN)) continue; + f->putchar(id.type); + f->putlil(strlen(id.name)); + f->write(id.name, strlen(id.name)); + switch(id.type) + { + case ID_VAR: + if(dbgvars) conoutf(CON_DEBUG, "wrote var %s: %d", id.name, *id.storage.i); + f->putlil(*id.storage.i); + break; + + case ID_FVAR: + if(dbgvars) conoutf(CON_DEBUG, "wrote fvar %s: %f", id.name, *id.storage.f); + f->putlil(*id.storage.f); + break; + + case ID_SVAR: + if(dbgvars) conoutf(CON_DEBUG, "wrote svar %s: %s", id.name, *id.storage.s); + f->putlil(strlen(*id.storage.s)); + f->write(*id.storage.s, strlen(*id.storage.s)); + break; + } + }); + + if(dbgvars) conoutf(CON_DEBUG, "wrote %d vars", hdr.numvars); + + f->putchar((int)strlen(game::gameident())); + f->write(game::gameident(), (int)strlen(game::gameident())+1); + f->putlil(entities::extraentinfosize()); + vector extras; + game::writegamedata(extras); + f->putlil(extras.length()); + f->write(extras.getbuf(), extras.length()); + + f->putlil(texmru.length()); + loopv(texmru) f->putlil(texmru[i]); + char *ebuf = new char[entities::extraentinfosize()]; + loopv(ents) + { + if(ents[i]->type!=ET_EMPTY || nolms) + { + entity tmp = *ents[i]; + lilswap(&tmp.o.x, 3); + lilswap(&tmp.attr1, 5); + f->write(&tmp, sizeof(entity)); + entities::writeent(*ents[i], ebuf); + if(entities::extraentinfosize()) f->write(ebuf, entities::extraentinfosize()); + } + } + delete[] ebuf; + + savevslots(f, numvslots); + + renderprogress(0, "saving octree..."); + savec(worldroot, ivec(0, 0, 0), worldsize>>1, f, nolms); + + if(!nolms) + { + if(lightmaps.length()) renderprogress(0, "saving lightmaps..."); + loopv(lightmaps) + { + LightMap &lm = lightmaps[i]; + f->putchar(lm.type | (lm.unlitx>=0 ? 0x80 : 0)); + if(lm.unlitx>=0) + { + f->putlil(ushort(lm.unlitx)); + f->putlil(ushort(lm.unlity)); + } + f->write(lm.data, lm.bpp*LM_PACKW*LM_PACKH); + renderprogress(float(i+1)/lightmaps.length(), "saving lightmaps..."); + } + if(getnumviewcells()>0) { renderprogress(0, "saving pvs..."); savepvs(f); } + } + if(shouldsaveblendmap()) { renderprogress(0, "saving blendmap..."); saveblendmap(f); } + + delete f; + conoutf("wrote map file %s", ogzname); + return true; +} + +static uint mapcrc = 0; + +uint getmapcrc() { return mapcrc; } +void clearmapcrc() { mapcrc = 0; } + +bool load_world(const char *mname, const char *cname) // still supports all map formats that have existed since the earliest cube betas! +{ + int loadingstart = SDL_GetTicks(); + setmapfilenames(mname, cname); + stream *f = opengzfile(ogzname, "rb"); + if(!f) { conoutf(CON_ERROR, "could not read map %s", ogzname); return false; } + octaheader hdr; + if(f->read(&hdr, 7*sizeof(int)) != 7*sizeof(int)) { conoutf(CON_ERROR, "map %s has malformatted header", ogzname); delete f; return false; } + lilswap(&hdr.version, 6); + if(memcmp(hdr.magic, "OCTA", 4) || hdr.worldsize <= 0|| hdr.numents < 0) { conoutf(CON_ERROR, "map %s has malformatted header", ogzname); delete f; return false; } + if(hdr.version>MAPVERSION) { conoutf(CON_ERROR, "map %s requires a newer version of Cube 2: Sauerbraten", ogzname); delete f; return false; } + compatheader chdr; + if(hdr.version <= 28) + { + if(f->read(&chdr.lightprecision, sizeof(chdr) - 7*sizeof(int)) != sizeof(chdr) - 7*sizeof(int)) { conoutf(CON_ERROR, "map %s has malformatted header", ogzname); delete f; return false; } + } + else + { + int extra = 0; + if(hdr.version <= 29) extra++; + if(f->read(&hdr.blendmap, sizeof(hdr) - (7+extra)*sizeof(int)) != sizeof(hdr) - (7+extra)*sizeof(int)) { conoutf(CON_ERROR, "map %s has malformatted header", ogzname); delete f; return false; } + } + + resetmap(); + + Texture *mapshot = textureload(picname, 3, true, false); + renderbackground("loading...", mapshot, mname, game::getmapinfo()); + + game::loadingmap(cname ? cname : mname); + + setvar("mapversion", hdr.version, true, false); + + if(hdr.version <= 28) + { + lilswap(&chdr.lightprecision, 3); + if(chdr.lightprecision) setvar("lightprecision", chdr.lightprecision); + if(chdr.lighterror) setvar("lighterror", chdr.lighterror); + if(chdr.bumperror) setvar("bumperror", chdr.bumperror); + setvar("lightlod", chdr.lightlod); + if(chdr.ambient) setvar("ambient", chdr.ambient); + setvar("skylight", (int(chdr.skylight[0])<<16) | (int(chdr.skylight[1])<<8) | int(chdr.skylight[2])); + setvar("watercolour", (int(chdr.watercolour[0])<<16) | (int(chdr.watercolour[1])<<8) | int(chdr.watercolour[2]), true); + setvar("waterfallcolour", (int(chdr.waterfallcolour[0])<<16) | (int(chdr.waterfallcolour[1])<<8) | int(chdr.waterfallcolour[2])); + setvar("lavacolour", (int(chdr.lavacolour[0])<<16) | (int(chdr.lavacolour[1])<<8) | int(chdr.lavacolour[2])); + setvar("fullbright", 0, true); + if(chdr.lerpsubdivsize || chdr.lerpangle) setvar("lerpangle", chdr.lerpangle); + if(chdr.lerpsubdivsize) + { + setvar("lerpsubdiv", chdr.lerpsubdiv); + setvar("lerpsubdivsize", chdr.lerpsubdivsize); + } + setsvar("maptitle", chdr.maptitle); + hdr.blendmap = chdr.blendmap; + hdr.numvars = 0; + hdr.numvslots = 0; + } + else + { + lilswap(&hdr.blendmap, 2); + if(hdr.version <= 29) hdr.numvslots = 0; + else lilswap(&hdr.numvslots, 1); + } + + renderprogress(0, "clearing world..."); + + freeocta(worldroot); + worldroot = NULL; + + int worldscale = 0; + while(1<getchar(), ilen = f->getlil(); + string name; + f->read(name, min(ilen, MAXSTRLEN-1)); + name[min(ilen, MAXSTRLEN-1)] = '\0'; + if(ilen >= MAXSTRLEN) f->seek(ilen - (MAXSTRLEN-1), SEEK_CUR); + ident *id = getident(name); + bool exists = id && id->type == type && id->flags&IDF_OVERRIDE; + switch(type) + { + case ID_VAR: + { + int val = f->getlil(); + if(exists && id->minval <= id->maxval) setvar(name, val); + if(dbgvars) conoutf(CON_DEBUG, "read var %s: %d", name, val); + break; + } + + case ID_FVAR: + { + float val = f->getlil(); + if(exists && id->minvalf <= id->maxvalf) setfvar(name, val); + if(dbgvars) conoutf(CON_DEBUG, "read fvar %s: %f", name, val); + break; + } + + case ID_SVAR: + { + int slen = f->getlil(); + string val; + f->read(val, min(slen, MAXSTRLEN-1)); + val[min(slen, MAXSTRLEN-1)] = '\0'; + if(slen >= MAXSTRLEN) f->seek(slen - (MAXSTRLEN-1), SEEK_CUR); + if(exists) setsvar(name, val); + if(dbgvars) conoutf(CON_DEBUG, "read svar %s: %s", name, val); + break; + } + } + } + if(dbgvars) conoutf(CON_DEBUG, "read %d vars", hdr.numvars); + + string gametype; + copystring(gametype, "fps"); + bool samegame = true; + int eif = 0; + if(hdr.version>=16) + { + int len = f->getchar(); + f->read(gametype, len+1); + } + if(strcmp(gametype, game::gameident())!=0) + { + samegame = false; + conoutf(CON_WARN, "WARNING: loading map from %s game, ignoring entities except for lights/mapmodels", gametype); + } + if(hdr.version>=16) + { + eif = f->getlil(); + int extrasize = f->getlil(); + vector extras; + f->read(extras.pad(extrasize), extrasize); + if(samegame) game::readgamedata(extras); + } + + texmru.shrink(0); + if(hdr.version<14) + { + uchar oldtl[256]; + f->read(oldtl, sizeof(oldtl)); + loopi(256) texmru.add(oldtl[i]); + } + else + { + ushort nummru = f->getlil(); + loopi(nummru) texmru.add(f->getlil()); + } + + renderprogress(0, "loading entities..."); + + vector &ents = entities::getents(); + int einfosize = entities::extraentinfosize(); + char *ebuf = einfosize > 0 ? new char[einfosize] : NULL; + loopi(min(hdr.numents, MAXENTS)) + { + extentity &e = *entities::newentity(); + ents.add(&e); + f->read(&e, sizeof(entity)); + lilswap(&e.o.x, 3); + lilswap(&e.attr1, 5); + fixent(e, hdr.version); + if(samegame) + { + if(einfosize > 0) f->read(ebuf, einfosize); + entities::readent(e, ebuf, mapversion); + } + else + { + if(eif > 0) f->seek(eif, SEEK_CUR); + if(e.type>=ET_GAMESPECIFIC || hdr.version<=14) + { + entities::deleteentity(ents.pop()); + continue; + } + } + if(!insideworld(e.o)) + { + if(e.type != ET_LIGHT && e.type != ET_SPOTLIGHT) + { + conoutf(CON_WARN, "warning: ent outside of world: enttype[%s] index %d (%f, %f, %f)", entities::entname(e.type), i, e.o.x, e.o.y, e.o.z); + } + } + if(hdr.version <= 14 && e.type == ET_MAPMODEL) + { + e.o.z += e.attr3; + if(e.attr4) conoutf(CON_WARN, "warning: mapmodel ent (index %d) uses texture slot %d", i, e.attr4); + e.attr3 = e.attr4 = 0; + } + } + if(ebuf) delete[] ebuf; + + if(hdr.numents > MAXENTS) + { + conoutf(CON_WARN, "warning: map has %d entities", hdr.numents); + f->seek((hdr.numents-MAXENTS)*(samegame ? sizeof(entity) + einfosize : eif), SEEK_CUR); + } + + renderprogress(0, "loading slots..."); + loadvslots(f, hdr.numvslots); + + renderprogress(0, "loading octree..."); + bool failed = false; + worldroot = loadchildren(f, ivec(0, 0, 0), hdr.worldsize>>1, failed); + if(failed) conoutf(CON_ERROR, "garbage in map"); + + renderprogress(0, "validating..."); + validatec(worldroot, hdr.worldsize>>1); + + if(!failed) + { + if(hdr.version >= 7) loopi(hdr.lightmaps) + { + renderprogress(i/(float)hdr.lightmaps, "loading lightmaps..."); + LightMap &lm = lightmaps.add(); + if(hdr.version >= 17) + { + int type = f->getchar(); + lm.type = type&0x7F; + if(hdr.version >= 20 && type&0x80) + { + lm.unlitx = f->getlil(); + lm.unlity = f->getlil(); + } + } + if(lm.type&LM_ALPHA && (lm.type&LM_TYPE)!=LM_BUMPMAP1) lm.bpp = 4; + lm.data = new uchar[lm.bpp*LM_PACKW*LM_PACKH]; + f->read(lm.data, lm.bpp * LM_PACKW * LM_PACKH); + lm.finalize(); + } + + if(hdr.version >= 25 && hdr.numpvs > 0) loadpvs(f, hdr.numpvs); + if(hdr.version >= 28 && hdr.blendmap) loadblendmap(f, hdr.blendmap); + } + + mapcrc = f->getcrc(); + delete f; + + conoutf("read map %s (%.1f seconds)", ogzname, (SDL_GetTicks()-loadingstart)/1000.0f); + + clearmainmenu(); + + identflags |= IDF_OVERRIDDEN; + execfile("data/default_map_settings.cfg", false); + execfile(cfgname, false); + identflags &= ~IDF_OVERRIDDEN; + + extern void fixlightmapnormals(); + if(hdr.version <= 25) fixlightmapnormals(); + extern void fixrotatedlightmaps(); + if(hdr.version <= 31) fixrotatedlightmaps(); + + preloadusedmapmodels(true); + + game::preload(); + flushpreloadedmodels(); + + preloadmapsounds(); + + entitiesinoctanodes(); + attachentities(); + initlights(); + allchanged(true); + + renderbackground("loading...", mapshot, mname, game::getmapinfo()); + + if(maptitle[0] && strcmp(maptitle, "Untitled Map by Unknown")) conoutf(CON_ECHO, "%s", maptitle); + + startmap(cname ? cname : mname); + + return true; +} + +void savecurrentmap() { save_world(game::getclientmap()); } +void savemap(char *mname) { save_world(mname); } + +COMMAND(savemap, "s"); +COMMAND(savecurrentmap, ""); + +void writeobj(char *name) +{ + defformatstring(fname, "%s.obj", name); + stream *f = openfile(path(fname), "w"); + if(!f) return; + f->printf("# obj file of Cube 2 level\n\n"); + defformatstring(mtlname, "%s.mtl", name); + path(mtlname); + f->printf("mtllib %s\n\n", mtlname); + vector verts; + vector texcoords; + hashtable shareverts(1<<16); + hashtable sharetc(1<<16); + hashtable > mtls(1<<8); + vector usedmtl; + vec bbmin(1e16f, 1e16f, 1e16f), bbmax(-1e16f, -1e16f, -1e16f); + loopv(valist) + { + vtxarray &va = *valist[i]; + ushort *edata = NULL; + vertex *vdata = NULL; + if(!readva(&va, edata, vdata)) continue; + ushort *idx = edata; + loopj(va.texs) + { + elementset &es = va.eslist[j]; + if(usedmtl.find(es.texture) < 0) usedmtl.add(es.texture); + vector &keys = mtls[es.texture]; + loopk(es.length[1]) + { + int n = idx[k] - va.voffset; + const vertex &v = vdata[n]; + const vec &pos = v.pos; + const vec2 &tc = v.tc; + ivec2 &key = keys.add(); + key.x = shareverts.access(pos, verts.length()); + if(key.x == verts.length()) + { + verts.add(pos); + loopl(3) + { + bbmin[l] = min(bbmin[l], pos[l]); + bbmax[l] = max(bbmax[l], pos[l]); + } + } + key.y = sharetc.access(tc, texcoords.length()); + if(key.y == texcoords.length()) texcoords.add(tc); + } + idx += es.length[1]; + } + delete[] edata; + delete[] vdata; + } + + vec center(-(bbmax.x + bbmin.x)/2, -(bbmax.y + bbmin.y)/2, -bbmin.z); + loopv(verts) + { + vec v = verts[i]; + v.add(center); + if(v.y != floor(v.y)) f->printf("v %.3f ", -v.y); else f->printf("v %d ", int(-v.y)); + if(v.z != floor(v.z)) f->printf("%.3f ", v.z); else f->printf("%d ", int(v.z)); + if(v.x != floor(v.x)) f->printf("%.3f\n", v.x); else f->printf("%d\n", int(v.x)); + } + f->printf("\n"); + loopv(texcoords) + { + const vec2 &tc = texcoords[i]; + f->printf("vt %.6f %.6f\n", tc.x, 1-tc.y); + } + f->printf("\n"); + + usedmtl.sort(); + loopv(usedmtl) + { + vector &keys = mtls[usedmtl[i]]; + f->printf("g slot%d\n", usedmtl[i]); + f->printf("usemtl slot%d\n\n", usedmtl[i]); + for(int i = 0; i < keys.length(); i += 3) + { + f->printf("f"); + loopk(3) f->printf(" %d/%d", keys[i+2-k].x+1, keys[i+2-k].y+1); + f->printf("\n"); + } + f->printf("\n"); + } + delete f; + + f = openfile(mtlname, "w"); + if(!f) return; + f->printf("# mtl file of Cube 2 level\n\n"); + loopv(usedmtl) + { + VSlot &vslot = lookupvslot(usedmtl[i], false); + f->printf("newmtl slot%d\n", usedmtl[i]); + f->printf("map_Kd %s\n", vslot.slot->sts.empty() ? notexture->name : path(makerelpath("packages", vslot.slot->sts[0].name))); + f->printf("\n"); + } + delete f; +} + +COMMAND(writeobj, "s"); + +#endif + -- cgit v1.2.3