func (r *wavefrontSceneReader) emitError(file string, line int, msgFormat string, args ...interface{}) error {
	msg := fmt.Sprintf(msgFormat, args...)
	var errMsg string
	if file != "" {
		errMsg = strings.Trim(
			fmt.Sprintf("[%s: %d] error: %s\n%s", file, line, msg, strings.Join(r.errStack, "\n")),
			"\n",
		)
	} else {
		errMsg = strings.Trim(
			fmt.Sprintf("error: %s\n%s", msg, strings.Join(r.errStack, "\n")),
			"\n",
		)
	}
	return fmt.Errorf(errMsg)
}
Generate an error message that also includes any data in the error stack.
package reader
import (
	"bufio"
	"fmt"
	"math"
	"strconv"
	"strings"
	"time"
	"github.com/achilleasa/polaris/asset"
	"github.com/achilleasa/polaris/asset/compiler"
	"github.com/achilleasa/polaris/asset/compiler/input"
	"github.com/achilleasa/polaris/asset/material"
	"github.com/achilleasa/polaris/asset/scene"
	"github.com/achilleasa/polaris/log"
	"github.com/achilleasa/polaris/types"
)
type wavefrontMaterial struct {
	Name string
	Kd types.Vec3
	Ks types.Vec3
	Ke       types.Vec3
	KeScaler float32
	Tf types.Vec3
	Ni float32
	KdTex     string
	KsTex     string
	KeTex     string
	TfTex     string
	BumpTex   string
	NormalTex string
	MaterialExpression string
	AssetRelPath *asset.Resource
	Used bool
}
func (wf *wavefrontMaterial) GetExpression() string {
	if wf.MaterialExpression != "" {
		return wf.MaterialExpression
	}
	isSpecularReflection := wf.Ks.MaxComponent() > 0.0 || wf.KsTex != ""
	isEmissive := wf.Ke.MaxComponent() > 0.0 || wf.KeTex != ""
	var bxdf material.BxdfType
	var exprArgs = make([]string, 0)
	switch {
	case isSpecularReflection && wf.Ni == 0.0:
		bxdf = material.BxdfConductor
		if wf.KsTex != "" {
			exprArgs = append(exprArgs, fmt.Sprintf("%s: %q", material.ParamSpecularity, wf.KsTex))
		} else if wf.Ks.MaxComponent() > 0.0 {
			exprArgs = append(exprArgs, fmt.Sprintf("%s: %v", material.ParamSpecularity, wf.Ks))
		}
	case isSpecularReflection && wf.Ni != 0.0:
		bxdf = material.BxdfDielectric
		if wf.KsTex != "" {
			exprArgs = append(exprArgs, fmt.Sprintf("%s: %q", material.ParamSpecularity, wf.KsTex))
		} else if wf.Ks.MaxComponent() > 0.0 {
			exprArgs = append(exprArgs, fmt.Sprintf("%s: %v", material.ParamSpecularity, wf.Ks))
		}
		if wf.TfTex != "" {
			exprArgs = append(exprArgs, fmt.Sprintf("%s: %q", material.ParamTransmittance, wf.TfTex))
		} else if wf.Tf.MaxComponent() > 0.0 {
			exprArgs = append(exprArgs, fmt.Sprintf("%s: %v", material.ParamTransmittance, wf.Tf))
		}
		exprArgs = append(exprArgs, fmt.Sprintf("%s: %v", material.ParamIntIOR, wf.Ni))
	case isEmissive:
		bxdf = material.BxdfEmissive
		if wf.KeTex != "" {
			exprArgs = append(exprArgs, fmt.Sprintf("%s: %q", material.ParamRadiance, wf.KeTex))
		} else if wf.Ke.MaxComponent() > 0.0 {
			exprArgs = append(exprArgs, fmt.Sprintf("%s: %v", material.ParamRadiance, wf.Ke))
		}
		if wf.KeScaler != 0 {
			exprArgs = append(exprArgs, fmt.Sprintf("%s: %v", material.ParamScale, wf.KeScaler))
		}
	default:
		bxdf = material.BxdfDiffuse
		if wf.KdTex != "" {
			exprArgs = append(exprArgs, fmt.Sprintf("%s: %q", material.ParamReflectance, wf.KdTex))
		} else if wf.Kd.MaxComponent() > 0.0 {
			exprArgs = append(exprArgs, fmt.Sprintf("%s: %v", material.ParamReflectance, wf.Kd))
		}
	}
	materialExpr := bxdf.String() + "(" + strings.Join(exprArgs, ", ") + ")"
	if wf.NormalTex != "" {
		materialExpr = fmt.Sprintf("normalMap(%s, %q)", materialExpr, wf.NormalTex)
	} else if wf.BumpTex != "" {
		materialExpr = fmt.Sprintf("bumpMap(%s, %q)", materialExpr, wf.BumpTex)
	}
	return materialExpr
}
type wavefrontSceneReader struct {
	logger log.Logger
	rawScene *input.Scene
	matNameToIndex map[string]int
	curMaterial *wavefrontMaterial
	materials []*wavefrontMaterial
	vertexList []types.Vec3
	normalList []types.Vec3
	uvList     []types.Vec2
	errStack []string
}
func newWavefrontReader() *wavefrontSceneReader {
	return &wavefrontSceneReader{
		logger:         log.New("wavefront scene reader"),
		rawScene:       input.NewScene(),
		matNameToIndex: make(map[string]int, 0),
		vertexList:     make([]types.Vec3, 0),
		normalList:     make([]types.Vec3, 0),
		uvList:         make([]types.Vec2, 0),
		errStack:       make([]string, 0),
	}
}
func (r *wavefrontSceneReader) Read(sceneRes *asset.Resource) (*scene.Scene, error) {
	r.logger.Noticef(`parsing scene from "%s"`, sceneRes.Path())
	start := time.Now()
	err := r.parse(sceneRes)
	if err != nil {
		return nil, err
	}
	if len(r.rawScene.MeshInstances) == 0 {
		r.createDefaultMeshInstances()
	}
	r.processMaterials()
	r.logger.Noticef("parsed scene in %d ms", time.Since(start).Nanoseconds()/1e6)
	return compiler.Compile(r.rawScene)
}
func (r *wavefrontSceneReader) processMaterials() {
	wfMaterialToSceneMaterial := make(map[int]int, 0)
	prunedMaterials := make([]*input.Material, 0)
	pruned := 0
	for wfIndex, wfMat := range r.materials {
		if wfMat.Name == compiler.SceneDiffuseMaterialName || wfMat.Name == compiler.SceneEmissiveMaterialName {
			wfMat.Used = true
		}
		if !wfMat.Used {
			r.logger.Infof("skipping unused material %q", wfMat.Name)
			prunedMaterials = append(
				prunedMaterials,
				&input.Material{
					Name:         wfMat.Name,
					Expression:   wfMat.GetExpression(),
					AssetRelPath: wfMat.AssetRelPath,
				},
			)
			pruned++
			continue
		}
		r.rawScene.Materials = append(
			r.rawScene.Materials,
			&input.Material{
				Name:         wfMat.Name,
				Expression:   wfMat.GetExpression(),
				AssetRelPath: wfMat.AssetRelPath,
				Used:         true,
			},
		)
		wfMaterialToSceneMaterial[wfIndex] = len(r.rawScene.Materials) - 1
	}
	for _, mesh := range r.rawScene.Meshes {
		for _, prim := range mesh.Primitives {
			prim.MaterialIndex = wfMaterialToSceneMaterial[prim.MaterialIndex]
		}
	}
	r.rawScene.Materials = append(r.rawScene.Materials, prunedMaterials...)
	if pruned > 0 {
		r.logger.Noticef("pruned %d unused materials", pruned)
	}
}
func (r *wavefrontSceneReader) createDefaultMeshInstances() {
	for meshIndex, mesh := range r.rawScene.Meshes {
		bbox := mesh.BBox()
		inst := &input.MeshInstance{
			MeshIndex: uint32(meshIndex),
			Transform: types.Ident4(),
		}
		inst.SetBBox(bbox)
		inst.SetCenter(bbox[0].Add(bbox[1]).Mul(0.5))
		r.rawScene.MeshInstances = append(r.rawScene.MeshInstances, inst)
	}
}